diff --git a/build.gradle b/build.gradle index ac26df6..ae19a44 100644 --- a/build.gradle +++ b/build.gradle @@ -85,6 +85,9 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + //객체 간 매핑 처리 + implementation 'org.modelmapper:modelmapper:3.1.0' + } tasks.named('test') { diff --git a/src/main/java/com/example/api/announcement/AnnouncementRepository.java b/src/main/java/com/example/api/announcement/AnnouncementRepository.java new file mode 100644 index 0000000..4d899a3 --- /dev/null +++ b/src/main/java/com/example/api/announcement/AnnouncementRepository.java @@ -0,0 +1,12 @@ +package com.example.api.announcement; + +import com.example.api.domain.Announcement; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AnnouncementRepository extends JpaRepository { + List findByAnnouncementTitleContaining(final String keyword); +} diff --git a/src/main/java/com/example/api/announcement/AnnouncementService.java b/src/main/java/com/example/api/announcement/AnnouncementService.java new file mode 100644 index 0000000..05fc357 --- /dev/null +++ b/src/main/java/com/example/api/announcement/AnnouncementService.java @@ -0,0 +1,89 @@ +package com.example.api.announcement; + +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementResponse; +import com.example.api.domain.Announcement; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class AnnouncementService { + private final AnnouncementRepository announcementRepository; + + @Transactional + public AnnouncementResponse createAnnouncement( + @Validated final AnnouncementCommand command + ) { + Announcement announcement = new Announcement(); + announcement.setAnnouncementTitle(command.announcementTitle()); + announcement.setAnnouncementType(command.announcementType()); + announcement.setAnnouncementContent(command.announcementContent()); + Announcement savedAnnouncement = announcementRepository.save(announcement); + return new AnnouncementResponse(savedAnnouncement); + } + + @Transactional + public List getAllAnnouncements() { + final List announcements = announcementRepository.findAll(); + return announcements.stream() + .map(AnnouncementResponse::new) + .collect(Collectors.toList()); + } + + @Transactional + public AnnouncementResponse getAnnouncement( + @Validated final Long announcementId + ) { + final Announcement announcement = findAnnouncementById(announcementId); + return new AnnouncementResponse(announcement); + } + + @Transactional + public AnnouncementResponse updateAnnouncement( + @Validated final Long announcementId, + @Validated final AnnouncementCommand command + ) { + Announcement announcement = findAnnouncementById(announcementId); + announcement.setAnnouncementTitle(command.announcementTitle()); + announcement.setAnnouncementType(command.announcementType()); + announcement.setAnnouncementContent(command.announcementContent()); + Announcement updatedAnnouncement = announcementRepository.save(announcement); + return new AnnouncementResponse(updatedAnnouncement); + } + + @Transactional + public void deleteAnnouncement( + @Validated final Long announcementId + ) { + final Announcement announcement = findAnnouncementById(announcementId); + announcementRepository.delete(announcement); + } + + @Transactional + public List searchAnnouncements( + @Validated final String keyword + ) { + final List announcements = announcementRepository.findByAnnouncementTitleContaining(keyword); + return announcements.stream() + .map(AnnouncementResponse::new) + .collect(Collectors.toList()); + } + + private Announcement findAnnouncementById( + @Validated final Long announcementId + ) { + return announcementRepository.findById(announcementId) + .orElseThrow(() -> new RuntimeException(getErrorMessage("announcement.not.found"))); + } + + private String getErrorMessage(final String key) { + return key; + } +} + + diff --git a/src/main/java/com/example/api/announcement/controller/AnnouncementController.java b/src/main/java/com/example/api/announcement/controller/AnnouncementController.java new file mode 100644 index 0000000..4f65285 --- /dev/null +++ b/src/main/java/com/example/api/announcement/controller/AnnouncementController.java @@ -0,0 +1,68 @@ +package com.example.api.announcement.controller; + +import com.example.api.announcement.AnnouncementService; +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementRequest; +import com.example.api.announcement.dto.AnnouncementResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/support/announcements") +public class AnnouncementController { + private final AnnouncementService announcementService; + + @PostMapping + public ResponseEntity createAnnouncement( + @RequestBody final AnnouncementRequest request + ) { + final AnnouncementCommand command = request.toCommand(); + final AnnouncementResponse response = announcementService.createAnnouncement(command); + return ResponseEntity.ok(response); + } + + @GetMapping + public ResponseEntity> getAnnouncements() { + final List responses = announcementService.getAllAnnouncements(); + return ResponseEntity.ok(responses); + } + + @GetMapping("/{announcementId}") + public ResponseEntity getAnnouncement( + @PathVariable(required = true) final Long announcementId + ) { + final AnnouncementResponse response = announcementService.getAnnouncement(announcementId); + return ResponseEntity.ok(response); + } + + @PutMapping("/{announcementId}") + public ResponseEntity updateAnnouncement( + @PathVariable(required = true) final Long announcementId, + @RequestBody final AnnouncementRequest request + ) { + final AnnouncementCommand command = request.toCommand(); + final AnnouncementResponse response = announcementService.updateAnnouncement( + announcementId, command); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{announcementId}") + public ResponseEntity deleteAnnouncement( + @PathVariable(required = true) final Long announcementId + ) { + announcementService.deleteAnnouncement(announcementId); + return ResponseEntity.ok().build(); + } + + @GetMapping("/search") + public ResponseEntity> searchAnnouncements( + @RequestParam(required = true) final String keyword + ) { + final List responses = announcementService.searchAnnouncements(keyword); + return ResponseEntity.ok(responses); + } +} diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java b/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java new file mode 100644 index 0000000..696ad8a --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java @@ -0,0 +1,10 @@ +package com.example.api.announcement.dto; + +import lombok.NonNull; + +public record AnnouncementCommand( + @NonNull + String announcementTitle, + String announcementType, + String announcementContent +) {} diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java b/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java new file mode 100644 index 0000000..1761a86 --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java @@ -0,0 +1,19 @@ +package com.example.api.announcement.dto; + +import lombok.NonNull; + +public record AnnouncementRequest( + @NonNull + String announcementTitle, + String announcementType, + String announcementContent +) { + public AnnouncementCommand toCommand() { + return new AnnouncementCommand( + this.announcementTitle, + this.announcementType, + this.announcementContent + ); + } +} + diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java b/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java new file mode 100644 index 0000000..0ef7a25 --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java @@ -0,0 +1,22 @@ +package com.example.api.announcement.dto; + +import com.example.api.domain.Announcement; + +public record AnnouncementResponse( + Long announcementId, + String announcementTitle, + String announcementType, + String announcementContent, + int viewCount +) { + public AnnouncementResponse(Announcement announcement) { + this( + announcement.getAnnouncementId(), + announcement.getAnnouncementTitle(), + announcement.getAnnouncementType(), + announcement.getAnnouncementContent(), + announcement.getViewCount() + ); + } +} + diff --git a/src/main/java/com/example/api/domain/Announcement.java b/src/main/java/com/example/api/domain/Announcement.java index a76d0e2..14bd190 100644 --- a/src/main/java/com/example/api/domain/Announcement.java +++ b/src/main/java/com/example/api/domain/Announcement.java @@ -3,13 +3,16 @@ import jakarta.persistence.*; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; @Entity @Getter +@Setter @EqualsAndHashCode(callSuper = false) @Table(name = "ANNOUNCEMENT") public class Announcement extends BaseEntity { + @NonNull @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long announcementId; diff --git a/src/test/java/com/example/api/announcement/AnnouncementControllerTest.java b/src/test/java/com/example/api/announcement/AnnouncementControllerTest.java new file mode 100644 index 0000000..ce93721 --- /dev/null +++ b/src/test/java/com/example/api/announcement/AnnouncementControllerTest.java @@ -0,0 +1,68 @@ +package com.example.api.announcement; + +import com.example.api.announcement.controller.AnnouncementController; +import com.example.api.announcement.dto.AnnouncementRequest; +import com.example.api.announcement.dto.AnnouncementResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class AnnouncementControllerTest { + @InjectMocks + private AnnouncementController announcementController; + @Mock + private AnnouncementService announcementService; + private static final String DEFAULT_TITLE = "공지사항 제목"; + private static final String DEFAULT_TYPE = "공지사항"; + private static final String DEFAULT_CONTENT = "공지사항 내용"; + private static final int DEFAULT_VIEW_COUNT = 100; + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createAnnouncement_success() { + final AnnouncementRequest request = createMockRequest(); + final AnnouncementResponse response = createMockResponse(); + when(announcementService.createAnnouncement(any())) + .thenReturn(response); + ResponseEntity result = announcementController.createAnnouncement(request); + assertCreateAnnouncementResponse(result); + verify(announcementService, times(1)).createAnnouncement(any()); + } + + @Test + void getAnnouncements_success() { + final AnnouncementResponse response = createMockResponse(); + when(announcementService.getAllAnnouncements()) + .thenReturn(List.of(response)); + ResponseEntity> result = announcementController.getAnnouncements(); + assertGetAnnouncementsResponse(result); + verify(announcementService, times(1)).getAllAnnouncements(); + } + + private AnnouncementRequest createMockRequest() { + return new AnnouncementRequest(DEFAULT_TITLE, DEFAULT_TYPE, DEFAULT_CONTENT); + } + + private AnnouncementResponse createMockResponse() { + return new AnnouncementResponse(1L, DEFAULT_TITLE, DEFAULT_TYPE, DEFAULT_CONTENT, DEFAULT_VIEW_COUNT); + } + + private void assertCreateAnnouncementResponse(ResponseEntity result) { + assertThat(result.getStatusCodeValue()).isEqualTo(200); + assertThat(result.getBody().announcementTitle()).isEqualTo(DEFAULT_TITLE); + } + + private void assertGetAnnouncementsResponse(ResponseEntity> result) { + assertThat(result.getStatusCodeValue()).isEqualTo(200); + assertThat(result.getBody()).hasSize(1); + } +} diff --git a/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java b/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java new file mode 100644 index 0000000..d9c078a --- /dev/null +++ b/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java @@ -0,0 +1,96 @@ +package com.example.api.announcement; + +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementResponse; +import com.example.api.domain.Announcement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import java.util.List; +import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class AnnouncementServiceTest { + @InjectMocks + private AnnouncementService announcementService; + @Mock + private AnnouncementRepository announcementRepository; + private static final String DEFAULT_TITLE = "공지사항 제목"; + private static final String DEFAULT_TYPE = "공지사항"; + private static final String DEFAULT_CONTENT = "공지사항 내용"; + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createAnnouncement_success() { + final AnnouncementCommand command = createMockCommand(); + final Announcement announcement = createMockAnnouncement(command); + when(announcementRepository.save(any(Announcement.class))) + .thenReturn(announcement); + AnnouncementResponse response = announcementService.createAnnouncement(command); + assertCreateAnnouncementResponse(response); + verify(announcementRepository, times(1)).save(any(Announcement.class)); + } + + @Test + void getAnnouncement_notFound() { + final Long announcementId = 1L; + when(announcementRepository.findById(announcementId)) + .thenReturn(Optional.empty()); + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + announcementService.getAnnouncement(announcementId); + }); + assertThat(exception.getMessage()).isEqualTo("announcement.not.found"); + verify(announcementRepository, times(1)).findById(announcementId); + } + + @Test + void searchAnnouncements_success() { + final String keyword = "공지"; + final Announcement announcement = createMockAnnouncementWithTitle(keyword); + when(announcementRepository.findByAnnouncementTitleContaining(keyword)) + .thenReturn(List.of(announcement)); + List responses = announcementService.searchAnnouncements(keyword); + assertSearchAnnouncementResponses(responses); + verify(announcementRepository, times(1)).findByAnnouncementTitleContaining(keyword); + } + + private AnnouncementCommand createMockCommand() { + return new AnnouncementCommand(DEFAULT_TITLE, DEFAULT_TYPE, DEFAULT_CONTENT); + } + + private Announcement createMockAnnouncement(AnnouncementCommand command) { + Announcement announcement = new Announcement(); + announcement.setAnnouncementTitle(command.announcementTitle()); + announcement.setAnnouncementType(command.announcementType()); + announcement.setAnnouncementContent(command.announcementContent()); + return announcement; + } + + private Announcement createMockAnnouncementWithTitle(String title) { + Announcement announcement = new Announcement(); + announcement.setAnnouncementTitle(DEFAULT_TITLE); + announcement.setAnnouncementType(DEFAULT_TYPE); + announcement.setAnnouncementContent(DEFAULT_CONTENT); + return announcement; + } + + private void assertCreateAnnouncementResponse(AnnouncementResponse response) { + assertThat(response.announcementTitle()).isEqualTo(DEFAULT_TITLE); + assertThat(response.announcementType()).isEqualTo(DEFAULT_TYPE); + assertThat(response.announcementContent()).isEqualTo(DEFAULT_CONTENT); + } + + private void assertSearchAnnouncementResponses(List responses) { + assertThat(responses).hasSize(1); + assertThat(responses.get(0).announcementTitle()).isEqualTo(DEFAULT_TITLE); + } +} + +