From a4b138921cdcba413db6e76a7bb2917ec5dbdfbf Mon Sep 17 00:00:00 2001 From: chaewss Date: Fri, 28 Jul 2023 15:52:30 +0900 Subject: [PATCH 01/10] =?UTF-8?q?chore:=20notice=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/data.sql | 3 ++ sql/schema.sql | 9 +++++ src/main/java/com/cvsgo/entity/Notice.java | 34 +++++++++++++++++++ .../cvsgo/repository/NoticeRepository.java | 8 +++++ 4 files changed, 54 insertions(+) create mode 100644 src/main/java/com/cvsgo/entity/Notice.java create mode 100644 src/main/java/com/cvsgo/repository/NoticeRepository.java diff --git a/sql/data.sql b/sql/data.sql index 05bb3773..cecb09d9 100644 --- a/sql/data.sql +++ b/sql/data.sql @@ -14,6 +14,9 @@ insert into manufacturer (name, created_at, modified_at) values('삼각', now(), insert into manufacturer (name, created_at, modified_at) values('주', now(), now()); insert into manufacturer (name, created_at, modified_at) values('도', now(), now()); +-- notice +insert into notice (title, content, created_at, modified_at) values('편해 출시', '편의점 리뷰 서비스 편해가 출시되었습니다.', now(), now()); + -- product insert into product (name, price, version, category_id, manufacturer_id, created_at, modified_at) values ('진라면순한맛', 950, 0, 2, 1, now(), now()); insert into product (name, price, version, category_id, manufacturer_id, created_at, modified_at) values ('마이쮸포도', 800, 0, 3, 2, now(), now()); diff --git a/sql/schema.sql b/sql/schema.sql index ca64a850..d551eaa0 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -54,6 +54,15 @@ create table manufacturer ( primary key (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +create table notice ( + id bigint not null auto_increment, + title varchar(255) not null, + content varchar(255) not null, + created_at datetime, + modified_at datetime, + primary key (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + create table product ( id bigint not null auto_increment, created_at datetime, diff --git a/src/main/java/com/cvsgo/entity/Notice.java b/src/main/java/com/cvsgo/entity/Notice.java new file mode 100644 index 00000000..8b46352e --- /dev/null +++ b/src/main/java/com/cvsgo/entity/Notice.java @@ -0,0 +1,34 @@ +package com.cvsgo.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Notice extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String title; + + @NotNull + private String content; + + @Builder + public Notice(Long id, String title, String content) { + this.id = id; + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/cvsgo/repository/NoticeRepository.java b/src/main/java/com/cvsgo/repository/NoticeRepository.java new file mode 100644 index 00000000..b3933c29 --- /dev/null +++ b/src/main/java/com/cvsgo/repository/NoticeRepository.java @@ -0,0 +1,8 @@ +package com.cvsgo.repository; + +import com.cvsgo.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeRepository extends JpaRepository { + +} From 6c484dbcfa138df48439ebe9212c759cedd007a3 Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:04:42 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cvsgo/controller/NoticeController.java | 24 +++++++++++++ .../dto/notice/ReadNoticeResponseDto.java | 35 +++++++++++++++++++ .../java/com/cvsgo/service/NoticeService.java | 29 +++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 src/main/java/com/cvsgo/controller/NoticeController.java create mode 100644 src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java create mode 100644 src/main/java/com/cvsgo/service/NoticeService.java diff --git a/src/main/java/com/cvsgo/controller/NoticeController.java b/src/main/java/com/cvsgo/controller/NoticeController.java new file mode 100644 index 00000000..98c8d4ca --- /dev/null +++ b/src/main/java/com/cvsgo/controller/NoticeController.java @@ -0,0 +1,24 @@ +package com.cvsgo.controller; + +import com.cvsgo.dto.SuccessResponse; +import com.cvsgo.dto.notice.ReadNoticeResponseDto; +import com.cvsgo.service.NoticeService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/notices") +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping + public SuccessResponse> readNoticeList() { + return SuccessResponse.from(noticeService.readNoticeList()); + } + +} diff --git a/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java b/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java new file mode 100644 index 00000000..9c6b1758 --- /dev/null +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java @@ -0,0 +1,35 @@ +package com.cvsgo.dto.notice; + +import com.cvsgo.entity.Notice; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import lombok.Getter; + +@Getter +public class ReadNoticeResponseDto { + + private final Long id; + + private final String title; + + @JsonFormat(pattern = "yy.MM.dd", timezone = "Asia/Seoul") + private final LocalDateTime createdAt; + + private final Boolean isNew; + + public ReadNoticeResponseDto(Notice notice) { + this.id = notice.getId(); + this.title = notice.getTitle(); + this.createdAt = notice.getCreatedAt(); + this.isNew = notice.getCreatedAt() != null + && ChronoUnit.DAYS.between(notice.getCreatedAt().toLocalDate(), LocalDate.now()) <= 7 + ? Boolean.TRUE : Boolean.FALSE; + } + + public static ReadNoticeResponseDto from(Notice notice) { + return new ReadNoticeResponseDto(notice); + } + +} diff --git a/src/main/java/com/cvsgo/service/NoticeService.java b/src/main/java/com/cvsgo/service/NoticeService.java new file mode 100644 index 00000000..ec1ee13c --- /dev/null +++ b/src/main/java/com/cvsgo/service/NoticeService.java @@ -0,0 +1,29 @@ +package com.cvsgo.service; + +import com.cvsgo.dto.notice.ReadNoticeResponseDto; +import com.cvsgo.entity.Notice; +import com.cvsgo.repository.NoticeRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NoticeService { + + private final NoticeRepository noticeRepository; + + /** + * 공지사항 목록을 조회한다. + * + * @return 공지사항 목록 + */ + @Transactional(readOnly = true) + public List readNoticeList() { + List notices = noticeRepository.findAll(Sort.by(Sort.Direction.DESC, "createdAt")); + return notices.stream().map(ReadNoticeResponseDto::from).toList(); + } + +} From d3920be1abf9faa98caf8943cb98b4a10910cd11 Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:05:14 +0900 Subject: [PATCH 03/10] =?UTF-8?q?test:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NoticeControllerTest.java | 99 +++++++++++++++++++ .../com/cvsgo/service/NoticeServiceTest.java | 56 +++++++++++ 2 files changed, 155 insertions(+) create mode 100644 src/test/java/com/cvsgo/controller/NoticeControllerTest.java create mode 100644 src/test/java/com/cvsgo/service/NoticeServiceTest.java diff --git a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java new file mode 100644 index 00000000..aa130d50 --- /dev/null +++ b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java @@ -0,0 +1,99 @@ +package com.cvsgo.controller; + +import static com.cvsgo.ApiDocumentUtils.documentIdentifier; +import static com.cvsgo.ApiDocumentUtils.getDocumentRequest; +import static com.cvsgo.ApiDocumentUtils.getDocumentResponse; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.sharedHttpSession; + +import com.cvsgo.argumentresolver.LoginUserArgumentResolver; +import com.cvsgo.config.WebConfig; +import com.cvsgo.dto.notice.ReadNoticeResponseDto; +import com.cvsgo.entity.Notice; +import com.cvsgo.interceptor.AuthInterceptor; +import com.cvsgo.service.NoticeService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +@ExtendWith(RestDocumentationExtension.class) +@WebMvcTest(NoticeController.class) +class NoticeControllerTest { + + @MockBean + LoginUserArgumentResolver loginUserArgumentResolver; + + @MockBean + WebConfig webConfig; + + @MockBean + AuthInterceptor authInterceptor; + + @MockBean + private NoticeService noticeService; + + private MockMvc mockMvc; + + @BeforeEach + void setup(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) + .apply(sharedHttpSession()) + .addFilters(new CharacterEncodingFilter("UTF-8", true)) + .build(); + } + + @Test + @DisplayName("공지사항 목록을 정상적으로 조회하면 HTTP 200을 응답한다") + void respond_200_when_read_notice_list_successfully() throws Exception { + List responseDto = List.of(new ReadNoticeResponseDto(notice1), new ReadNoticeResponseDto(notice2)); + given(noticeService.readNoticeList()).willReturn(responseDto); + + mockMvc.perform(get("/api/notices").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document(documentIdentifier, + getDocumentRequest(), + getDocumentResponse(), + relaxedResponseFields( + fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("공지사항 ID"), + fieldWithPath("data[].title").type(JsonFieldType.STRING).description("공지사항 제목"), + fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional(), + fieldWithPath("data[].isNew").type(JsonFieldType.BOOLEAN).description("새 공지사항 여부").optional() + ) + )); + } + + Notice notice1 = Notice.builder() + .id(1L) + .title("긴급 공지") + .content("긴급 상황입니다") + .build(); + + Notice notice2 = Notice.builder() + .id(2L) + .title("이벤트 공지") + .content("이벤트 배너를 확인하세요") + .build(); + +} diff --git a/src/test/java/com/cvsgo/service/NoticeServiceTest.java b/src/test/java/com/cvsgo/service/NoticeServiceTest.java new file mode 100644 index 00000000..1b8dabad --- /dev/null +++ b/src/test/java/com/cvsgo/service/NoticeServiceTest.java @@ -0,0 +1,56 @@ +package com.cvsgo.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import com.cvsgo.dto.notice.ReadNoticeResponseDto; +import com.cvsgo.entity.Notice; +import com.cvsgo.repository.NoticeRepository; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; + +@ExtendWith(MockitoExtension.class) +class NoticeServiceTest { + + @Mock + private NoticeRepository noticeRepository; + + @InjectMocks + NoticeService noticeService; + + @Test + @DisplayName("공지사항 목록을 정상적으로 조회한다") + void succeed_to_read_notice_list() { + given(noticeRepository.findAll(any(Sort.class))).willReturn(getNoticeList()); + + List result = noticeService.readNoticeList(); + + assertEquals(getNoticeList().size(), result.size()); + then(noticeRepository).should(times(1)).findAll(any(Sort.class)); + } + + private List getNoticeList() { + Notice notice1 = Notice.builder() + .id(1L) + .title("긴급 공지") + .content("긴급 상황입니다") + .build(); + Notice notice2 = Notice.builder() + .id(2L) + .title("이벤트 공지") + .content("이벤트 배너를 확인하세요") + .build(); + + return List.of(notice1, notice2); + } + +} From d8e4877da292f14578947fde79159256ec7a55ca Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:38:30 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cvsgo/controller/NoticeController.java | 7 +++++ .../notice/ReadNoticeDetailResponseDto.java | 30 +++++++++++++++++++ .../java/com/cvsgo/exception/ErrorCode.java | 1 + .../cvsgo/exception/ExceptionConstants.java | 1 + .../java/com/cvsgo/service/NoticeService.java | 17 +++++++++++ 5 files changed, 56 insertions(+) create mode 100644 src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java diff --git a/src/main/java/com/cvsgo/controller/NoticeController.java b/src/main/java/com/cvsgo/controller/NoticeController.java index 98c8d4ca..bf1247d4 100644 --- a/src/main/java/com/cvsgo/controller/NoticeController.java +++ b/src/main/java/com/cvsgo/controller/NoticeController.java @@ -1,11 +1,13 @@ package com.cvsgo.controller; import com.cvsgo.dto.SuccessResponse; +import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.service.NoticeService; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,4 +23,9 @@ public SuccessResponse> readNoticeList() { return SuccessResponse.from(noticeService.readNoticeList()); } + @GetMapping("/{noticeId}") + public SuccessResponse readNotice(@PathVariable Long noticeId) { + return SuccessResponse.from(noticeService.readNotice(noticeId)); + } + } diff --git a/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java b/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java new file mode 100644 index 00000000..a1a923fa --- /dev/null +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java @@ -0,0 +1,30 @@ +package com.cvsgo.dto.notice; + +import com.cvsgo.entity.Notice; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class ReadNoticeDetailResponseDto { + + private final Long id; + + private final String title; + + private final String content; + + @JsonFormat(pattern = "yy.MM.dd", timezone = "Asia/Seoul") + private final LocalDateTime createdAt; + + public ReadNoticeDetailResponseDto(Notice notice) { + this.id = notice.getId(); + this.title = notice.getTitle(); + this.content = notice.getContent(); + this.createdAt = notice.getCreatedAt(); + } + + public static ReadNoticeDetailResponseDto from(Notice notice) { + return new ReadNoticeDetailResponseDto(notice); + } +} diff --git a/src/main/java/com/cvsgo/exception/ErrorCode.java b/src/main/java/com/cvsgo/exception/ErrorCode.java index 1bad64c6..5caa832b 100644 --- a/src/main/java/com/cvsgo/exception/ErrorCode.java +++ b/src/main/java/com/cvsgo/exception/ErrorCode.java @@ -37,6 +37,7 @@ public enum ErrorCode { NOT_FOUND_PRODUCT_BOOKMARK("존재하지 않는 상품 북마크입니다."), NOT_FOUND_REVIEW("존재하지 않는 리뷰입니다."), NOT_FOUND_REVIEW_LIKE("해당 리뷰의 좋아요가 없습니다."), + NOT_FOUND_NOTICE("존재하지 않는 공지사항입니다."), /* 500 INTERNAL_SEVER_ERROR */ UNEXPECTED_ERROR("시스템에 문제가 발생했습니다."); diff --git a/src/main/java/com/cvsgo/exception/ExceptionConstants.java b/src/main/java/com/cvsgo/exception/ExceptionConstants.java index a16c4b2c..d1538683 100644 --- a/src/main/java/com/cvsgo/exception/ExceptionConstants.java +++ b/src/main/java/com/cvsgo/exception/ExceptionConstants.java @@ -18,6 +18,7 @@ public interface ExceptionConstants { NotFoundException NOT_FOUND_PRODUCT_BOOKMARK = new NotFoundException(ErrorCode.NOT_FOUND_PRODUCT_BOOKMARK); NotFoundException NOT_FOUND_REVIEW = new NotFoundException(ErrorCode.NOT_FOUND_REVIEW); NotFoundException NOT_FOUND_REVIEW_LIKE = new NotFoundException(ErrorCode.NOT_FOUND_REVIEW_LIKE); + NotFoundException NOT_FOUND_NOTICE = new NotFoundException(ErrorCode.NOT_FOUND_NOTICE); DuplicateException DUPLICATE_EMAIL = new DuplicateException(ErrorCode.DUPLICATE_EMAIL); DuplicateException DUPLICATE_NICKNAME = new DuplicateException(ErrorCode.DUPLICATE_NICKNAME); diff --git a/src/main/java/com/cvsgo/service/NoticeService.java b/src/main/java/com/cvsgo/service/NoticeService.java index ec1ee13c..32bd08e2 100644 --- a/src/main/java/com/cvsgo/service/NoticeService.java +++ b/src/main/java/com/cvsgo/service/NoticeService.java @@ -1,7 +1,11 @@ package com.cvsgo.service; +import static com.cvsgo.exception.ExceptionConstants.NOT_FOUND_NOTICE; + +import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; +import com.cvsgo.exception.NotFoundException; import com.cvsgo.repository.NoticeRepository; import java.util.List; import lombok.RequiredArgsConstructor; @@ -26,4 +30,17 @@ public List readNoticeList() { return notices.stream().map(ReadNoticeResponseDto::from).toList(); } + /** + * 공지사항을 상세 조회한다. + * + * @param noticeId 공지사항 ID + * @return 공지사항 상세 정보 + * @throws NotFoundException 해당하는 아이디를 가진 공지사항이 없는 경우 + */ + @Transactional(readOnly = true) + public ReadNoticeDetailResponseDto readNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId).orElseThrow(() -> NOT_FOUND_NOTICE); + return ReadNoticeDetailResponseDto.from(notice); + } + } From 3aabf0f4d0f9b48459469c5288f7f46141c5dfe7 Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:38:57 +0900 Subject: [PATCH 05/10] =?UTF-8?q?test:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NoticeControllerTest.java | 29 ++++++++++++ .../com/cvsgo/service/NoticeServiceTest.java | 47 ++++++++++++++----- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java index aa130d50..01926be9 100644 --- a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java +++ b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java @@ -3,18 +3,22 @@ import static com.cvsgo.ApiDocumentUtils.documentIdentifier; import static com.cvsgo.ApiDocumentUtils.getDocumentRequest; import static com.cvsgo.ApiDocumentUtils.getDocumentResponse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.sharedHttpSession; import com.cvsgo.argumentresolver.LoginUserArgumentResolver; import com.cvsgo.config.WebConfig; +import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; import com.cvsgo.interceptor.AuthInterceptor; @@ -84,6 +88,31 @@ void respond_200_when_read_notice_list_successfully() throws Exception { )); } + @Test + @DisplayName("공지사항을 상적으로 조회하면 HTTP 200을 응답한다") + void respond_200_when_read_notice_successfully() throws Exception { + ReadNoticeDetailResponseDto responseDto = ReadNoticeDetailResponseDto.from(notice1); + given(noticeService.readNotice(any())).willReturn(responseDto); + + mockMvc.perform(get("/api/notices/{noticeId}", 1L) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document(documentIdentifier, + getDocumentRequest(), + getDocumentResponse(), + pathParameters( + parameterWithName("noticeId").description("공지사항 ID") + ), + relaxedResponseFields( + fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("공지사항 ID"), + fieldWithPath("data.title").type(JsonFieldType.STRING).description("공지사항 제목"), + fieldWithPath("data.content").type(JsonFieldType.STRING).description("공지사항 내용"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional() + ) + )); + } + Notice notice1 = Notice.builder() .id(1L) .title("긴급 공지") diff --git a/src/test/java/com/cvsgo/service/NoticeServiceTest.java b/src/test/java/com/cvsgo/service/NoticeServiceTest.java index 1b8dabad..92ff43a1 100644 --- a/src/test/java/com/cvsgo/service/NoticeServiceTest.java +++ b/src/test/java/com/cvsgo/service/NoticeServiceTest.java @@ -1,15 +1,19 @@ package com.cvsgo.service; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; +import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; +import com.cvsgo.exception.NotFoundException; import com.cvsgo.repository.NoticeRepository; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,18 +42,39 @@ void succeed_to_read_notice_list() { then(noticeRepository).should(times(1)).findAll(any(Sort.class)); } - private List getNoticeList() { - Notice notice1 = Notice.builder() - .id(1L) - .title("긴급 공지") - .content("긴급 상황입니다") - .build(); - Notice notice2 = Notice.builder() - .id(2L) - .title("이벤트 공지") - .content("이벤트 배너를 확인하세요") - .build(); + @Test + @DisplayName("공지사항을 정상적으로 조회한다") + void succeed_to_read_notice() { + given(noticeRepository.findById(any())).willReturn(Optional.of(notice1)); + + ReadNoticeDetailResponseDto result = noticeService.readNotice(1L); + + then(noticeRepository).should(times(1)).findById(any()); + } + + @Test + @DisplayName("공지사항 조회 시 존재하지 않는 상품이면 NotFoundException이 발생한다") + void should_throw_NotFoundException_when_read_notice_but_notice_does_not_exist() { + given(noticeRepository.findById(any())).willReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> noticeService.readNotice(100L)); + + then(noticeRepository).should(times(1)).findById(any()); + } + + Notice notice1 = Notice.builder() + .id(1L) + .title("긴급 공지") + .content("긴급 상황입니다") + .build(); + + Notice notice2 = Notice.builder() + .id(2L) + .title("이벤트 공지") + .content("이벤트 배너를 확인하세요") + .build(); + + private List getNoticeList() { return List.of(notice1, notice2); } From 29963def2d7a54edbb555ba7247bf59715c9fe3d Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:39:19 +0900 Subject: [PATCH 06/10] =?UTF-8?q?docs:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20api=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-doc.adoc | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/docs/asciidoc/api-doc.adoc b/src/docs/asciidoc/api-doc.adoc index e0988d32..a3aa0b75 100644 --- a/src/docs/asciidoc/api-doc.adoc +++ b/src/docs/asciidoc/api-doc.adoc @@ -375,3 +375,28 @@ include::{snippets}/image-controller-test/respond_201_when_succeed_to_upload_ima include::{snippets}/image-controller-test/respond_201_when_succeed_to_upload_images/http-request.adoc[] ==== Sample Response include::{snippets}/image-controller-test/respond_201_when_succeed_to_upload_images/http-response.adoc[] + +== 6. 공지사항 +=== 6-1. 공지사항 목록 조회 +==== Sample Request +include::{snippets}/notice-controller-test/respond_200_when_read_notice_list_successfully/http-request.adoc[] +==== Response Fields +include::{snippets}/notice-controller-test/respond_200_when_read_notice_list_successfully/response-fields.adoc[] +==== Sample Response +include::{snippets}/notice-controller-test/respond_200_when_read_notice_list_successfully/http-response.adoc[] + +=== 6-2. 공지사항 상세 조회 +==== Path Parameters +include::{snippets}/notice-controller-test/respond_200_when_read_notice_successfully/path-parameters.adoc[] +==== Sample Request +include::{snippets}/notice-controller-test/respond_200_when_read_notice_successfully/http-request.adoc[] +==== Response Fields +include::{snippets}/notice-controller-test/respond_200_when_read_notice_successfully/response-fields.adoc[] +==== Sample Response +include::{snippets}/notice-controller-test/respond_200_when_read_notice_successfully/http-response.adoc[] +==== Error Response +|=== +| HTTP Status | Error Code | Detail + +| `404 NOT FOUND` | `NOT_FOUND_NOTICE` | 해당하는 공지사항이 없는 경우 +|=== From c2e52555053c62e02b49f2b0c75d55881b9dc854 Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 15:54:36 +0900 Subject: [PATCH 07/10] =?UTF-8?q?chore:=20noticeImage=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/schema.sql | 9 +++++ src/main/java/com/cvsgo/entity/Notice.java | 22 ++++++++++- .../java/com/cvsgo/entity/NoticeImage.java | 38 +++++++++++++++++++ .../repository/NoticeImageRepository.java | 8 ++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cvsgo/entity/NoticeImage.java create mode 100644 src/main/java/com/cvsgo/repository/NoticeImageRepository.java diff --git a/sql/schema.sql b/sql/schema.sql index d551eaa0..c9f392fd 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -63,6 +63,15 @@ create table notice ( primary key (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +create table notice_image ( + id bigint not null auto_increment, + image_url varchar(255) not null, + notice_id bigint not null, + created_at datetime, + modified_at datetime, + primary key (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + create table product ( id bigint not null auto_increment, created_at datetime, diff --git a/src/main/java/com/cvsgo/entity/Notice.java b/src/main/java/com/cvsgo/entity/Notice.java index 8b46352e..b28b371f 100644 --- a/src/main/java/com/cvsgo/entity/Notice.java +++ b/src/main/java/com/cvsgo/entity/Notice.java @@ -1,10 +1,14 @@ package com.cvsgo.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -25,10 +29,26 @@ public class Notice extends BaseTimeEntity { @NotNull private String content; + @OneToMany(mappedBy = "notice", cascade = CascadeType.ALL, orphanRemoval = true) + private final List noticeImages = new ArrayList<>(); + + public void addImage(String imageUrl) { + NoticeImage noticeImage = NoticeImage.builder() + .notice(this) + .imageUrl(imageUrl) + .build(); + noticeImages.add(noticeImage); + } + @Builder - public Notice(Long id, String title, String content) { + public Notice(Long id, String title, String content, List imageUrls) { this.id = id; this.title = title; this.content = content; + if (imageUrls != null) { + for (String imageUrl : imageUrls) { + addImage(imageUrl); + } + } } } diff --git a/src/main/java/com/cvsgo/entity/NoticeImage.java b/src/main/java/com/cvsgo/entity/NoticeImage.java new file mode 100644 index 00000000..ae32e0c4 --- /dev/null +++ b/src/main/java/com/cvsgo/entity/NoticeImage.java @@ -0,0 +1,38 @@ +package com.cvsgo.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class NoticeImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String imageUrl; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "notice_id") + private Notice notice; + + @Builder + public NoticeImage(Long id, String imageUrl, Notice notice) { + this.id = id; + this.imageUrl = imageUrl; + this.notice = notice; + } +} diff --git a/src/main/java/com/cvsgo/repository/NoticeImageRepository.java b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java new file mode 100644 index 00000000..47e838e7 --- /dev/null +++ b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java @@ -0,0 +1,8 @@ +package com.cvsgo.repository; + +import com.cvsgo.entity.NoticeImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeImageRepository extends JpaRepository { + +} From dfc6bd073f5370a7fd73165c157e766d2a815f50 Mon Sep 17 00:00:00 2001 From: chaewss Date: Sat, 29 Jul 2023 16:25:13 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20response?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/notice/ReadNoticeDetailResponseDto.java | 10 +++++++--- .../com/cvsgo/repository/NoticeImageRepository.java | 3 +++ src/main/java/com/cvsgo/service/NoticeService.java | 8 +++++++- .../com/cvsgo/controller/NoticeControllerTest.java | 12 ++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java b/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java index a1a923fa..2de04a5f 100644 --- a/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java @@ -3,6 +3,7 @@ import com.cvsgo.entity.Notice; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; +import java.util.List; import lombok.Getter; @Getter @@ -14,17 +15,20 @@ public class ReadNoticeDetailResponseDto { private final String content; + private final List noticeImageUrls; + @JsonFormat(pattern = "yy.MM.dd", timezone = "Asia/Seoul") private final LocalDateTime createdAt; - public ReadNoticeDetailResponseDto(Notice notice) { + public ReadNoticeDetailResponseDto(Notice notice, List noticeImageUrls) { this.id = notice.getId(); this.title = notice.getTitle(); this.content = notice.getContent(); + this.noticeImageUrls = noticeImageUrls; this.createdAt = notice.getCreatedAt(); } - public static ReadNoticeDetailResponseDto from(Notice notice) { - return new ReadNoticeDetailResponseDto(notice); + public static ReadNoticeDetailResponseDto of(Notice notice, List noticeImageUrls) { + return new ReadNoticeDetailResponseDto(notice, noticeImageUrls); } } diff --git a/src/main/java/com/cvsgo/repository/NoticeImageRepository.java b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java index 47e838e7..a49e8863 100644 --- a/src/main/java/com/cvsgo/repository/NoticeImageRepository.java +++ b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java @@ -1,8 +1,11 @@ package com.cvsgo.repository; +import com.cvsgo.entity.Notice; import com.cvsgo.entity.NoticeImage; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface NoticeImageRepository extends JpaRepository { + List findByNotice(Notice notice); } diff --git a/src/main/java/com/cvsgo/service/NoticeService.java b/src/main/java/com/cvsgo/service/NoticeService.java index 32bd08e2..66b4f3e9 100644 --- a/src/main/java/com/cvsgo/service/NoticeService.java +++ b/src/main/java/com/cvsgo/service/NoticeService.java @@ -5,7 +5,9 @@ import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; +import com.cvsgo.entity.NoticeImage; import com.cvsgo.exception.NotFoundException; +import com.cvsgo.repository.NoticeImageRepository; import com.cvsgo.repository.NoticeRepository; import java.util.List; import lombok.RequiredArgsConstructor; @@ -18,6 +20,7 @@ public class NoticeService { private final NoticeRepository noticeRepository; + private final NoticeImageRepository noticeImageRepository; /** * 공지사항 목록을 조회한다. @@ -40,7 +43,10 @@ public List readNoticeList() { @Transactional(readOnly = true) public ReadNoticeDetailResponseDto readNotice(Long noticeId) { Notice notice = noticeRepository.findById(noticeId).orElseThrow(() -> NOT_FOUND_NOTICE); - return ReadNoticeDetailResponseDto.from(notice); + List noticeImages = noticeImageRepository.findByNotice(notice).stream() + .map(NoticeImage::getImageUrl).toList(); + + return ReadNoticeDetailResponseDto.of(notice, noticeImages); } } diff --git a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java index 01926be9..7f27eb35 100644 --- a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java +++ b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java @@ -21,6 +21,7 @@ import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; +import com.cvsgo.entity.NoticeImage; import com.cvsgo.interceptor.AuthInterceptor; import com.cvsgo.service.NoticeService; import java.util.List; @@ -89,9 +90,9 @@ void respond_200_when_read_notice_list_successfully() throws Exception { } @Test - @DisplayName("공지사항을 상적으로 조회하면 HTTP 200을 응답한다") + @DisplayName("공지사항을 정상적으로 조회하면 HTTP 200을 응답한다") void respond_200_when_read_notice_successfully() throws Exception { - ReadNoticeDetailResponseDto responseDto = ReadNoticeDetailResponseDto.from(notice1); + ReadNoticeDetailResponseDto responseDto = ReadNoticeDetailResponseDto.of(notice1, List.of(noticeImage.getImageUrl())); given(noticeService.readNotice(any())).willReturn(responseDto); mockMvc.perform(get("/api/notices/{noticeId}", 1L) @@ -108,6 +109,7 @@ void respond_200_when_read_notice_successfully() throws Exception { fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("공지사항 ID"), fieldWithPath("data.title").type(JsonFieldType.STRING).description("공지사항 제목"), fieldWithPath("data.content").type(JsonFieldType.STRING).description("공지사항 내용"), + fieldWithPath("data.noticeImageUrls").type(JsonFieldType.ARRAY).description("공지사항 이미지 URL").optional(), fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional() ) )); @@ -125,4 +127,10 @@ void respond_200_when_read_notice_successfully() throws Exception { .content("이벤트 배너를 확인하세요") .build(); + NoticeImage noticeImage = NoticeImage.builder() + .id(1L) + .imageUrl("imageUrl") + .notice(notice1) + .build(); + } From fadcaff274a51af4d3d0962cb2f29e4f47175cdb Mon Sep 17 00:00:00 2001 From: chaewss Date: Fri, 18 Aug 2023 11:09:14 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20type=20=EB=B3=80=EA=B2=BD=20#?= =?UTF-8?q?85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java b/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java index 9c6b1758..64aedb70 100644 --- a/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java @@ -17,15 +17,14 @@ public class ReadNoticeResponseDto { @JsonFormat(pattern = "yy.MM.dd", timezone = "Asia/Seoul") private final LocalDateTime createdAt; - private final Boolean isNew; + private final boolean isNew; public ReadNoticeResponseDto(Notice notice) { this.id = notice.getId(); this.title = notice.getTitle(); this.createdAt = notice.getCreatedAt(); this.isNew = notice.getCreatedAt() != null - && ChronoUnit.DAYS.between(notice.getCreatedAt().toLocalDate(), LocalDate.now()) <= 7 - ? Boolean.TRUE : Boolean.FALSE; + && ChronoUnit.DAYS.between(notice.getCreatedAt().toLocalDate(), LocalDate.now()) <= 7; } public static ReadNoticeResponseDto from(Notice notice) { From b28da888cd6425363490aec91303a50f4112f3c5 Mon Sep 17 00:00:00 2001 From: chaewss Date: Fri, 18 Aug 2023 11:10:15 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20=ED=8E=98=EC=9D=B4=EC=A7=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cvsgo/controller/NoticeController.java | 6 ++-- .../cvsgo/repository/NoticeRepository.java | 4 +++ .../java/com/cvsgo/service/NoticeService.java | 8 +++-- .../controller/NoticeControllerTest.java | 11 +++---- .../com/cvsgo/service/NoticeServiceTest.java | 30 +++++++++++++++---- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/cvsgo/controller/NoticeController.java b/src/main/java/com/cvsgo/controller/NoticeController.java index bf1247d4..ff4200c2 100644 --- a/src/main/java/com/cvsgo/controller/NoticeController.java +++ b/src/main/java/com/cvsgo/controller/NoticeController.java @@ -6,6 +6,8 @@ import com.cvsgo.service.NoticeService; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,8 +21,8 @@ public class NoticeController { private final NoticeService noticeService; @GetMapping - public SuccessResponse> readNoticeList() { - return SuccessResponse.from(noticeService.readNoticeList()); + public SuccessResponse> readNoticeList(Pageable pageable) { + return SuccessResponse.from(noticeService.readNoticeList(pageable)); } @GetMapping("/{noticeId}") diff --git a/src/main/java/com/cvsgo/repository/NoticeRepository.java b/src/main/java/com/cvsgo/repository/NoticeRepository.java index b3933c29..253aa021 100644 --- a/src/main/java/com/cvsgo/repository/NoticeRepository.java +++ b/src/main/java/com/cvsgo/repository/NoticeRepository.java @@ -1,8 +1,12 @@ package com.cvsgo.repository; import com.cvsgo.entity.Notice; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface NoticeRepository extends JpaRepository { + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + } diff --git a/src/main/java/com/cvsgo/service/NoticeService.java b/src/main/java/com/cvsgo/service/NoticeService.java index 66b4f3e9..a47e1ea1 100644 --- a/src/main/java/com/cvsgo/service/NoticeService.java +++ b/src/main/java/com/cvsgo/service/NoticeService.java @@ -11,6 +11,8 @@ import com.cvsgo.repository.NoticeRepository; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,9 +30,9 @@ public class NoticeService { * @return 공지사항 목록 */ @Transactional(readOnly = true) - public List readNoticeList() { - List notices = noticeRepository.findAll(Sort.by(Sort.Direction.DESC, "createdAt")); - return notices.stream().map(ReadNoticeResponseDto::from).toList(); + public Page readNoticeList(Pageable pageable) { + return noticeRepository.findAllByOrderByCreatedAtDesc(pageable) + .map(ReadNoticeResponseDto::from); } /** diff --git a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java index 7f27eb35..04aaf026 100644 --- a/src/test/java/com/cvsgo/controller/NoticeControllerTest.java +++ b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -72,7 +73,7 @@ void setup(WebApplicationContext webApplicationContext, @DisplayName("공지사항 목록을 정상적으로 조회하면 HTTP 200을 응답한다") void respond_200_when_read_notice_list_successfully() throws Exception { List responseDto = List.of(new ReadNoticeResponseDto(notice1), new ReadNoticeResponseDto(notice2)); - given(noticeService.readNoticeList()).willReturn(responseDto); + given(noticeService.readNoticeList(any())).willReturn(new PageImpl<>(responseDto)); mockMvc.perform(get("/api/notices").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -81,10 +82,10 @@ void respond_200_when_read_notice_list_successfully() throws Exception { getDocumentRequest(), getDocumentResponse(), relaxedResponseFields( - fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("공지사항 ID"), - fieldWithPath("data[].title").type(JsonFieldType.STRING).description("공지사항 제목"), - fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional(), - fieldWithPath("data[].isNew").type(JsonFieldType.BOOLEAN).description("새 공지사항 여부").optional() + fieldWithPath("data.content[].id").type(JsonFieldType.NUMBER).description("공지사항 ID"), + fieldWithPath("data.content[].title").type(JsonFieldType.STRING).description("공지사항 제목"), + fieldWithPath("data.content[].createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional(), + fieldWithPath("data.content[].isNew").type(JsonFieldType.BOOLEAN).description("새 공지사항 여부").optional() ) )); } diff --git a/src/test/java/com/cvsgo/service/NoticeServiceTest.java b/src/test/java/com/cvsgo/service/NoticeServiceTest.java index 92ff43a1..1cdee4ac 100644 --- a/src/test/java/com/cvsgo/service/NoticeServiceTest.java +++ b/src/test/java/com/cvsgo/service/NoticeServiceTest.java @@ -10,8 +10,12 @@ import com.cvsgo.dto.notice.ReadNoticeDetailResponseDto; import com.cvsgo.dto.notice.ReadNoticeResponseDto; import com.cvsgo.entity.Notice; +import com.cvsgo.entity.NoticeImage; +import com.cvsgo.entity.Review; import com.cvsgo.exception.NotFoundException; +import com.cvsgo.repository.NoticeImageRepository; import com.cvsgo.repository.NoticeRepository; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -20,6 +24,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @ExtendWith(MockitoExtension.class) @@ -28,24 +36,29 @@ class NoticeServiceTest { @Mock private NoticeRepository noticeRepository; + @Mock + private NoticeImageRepository noticeImageRepository; + @InjectMocks NoticeService noticeService; @Test @DisplayName("공지사항 목록을 정상적으로 조회한다") void succeed_to_read_notice_list() { - given(noticeRepository.findAll(any(Sort.class))).willReturn(getNoticeList()); + Pageable pageable = PageRequest.of(0, 20); + given(noticeRepository.findAllByOrderByCreatedAtDesc(any())).willReturn(getNoticeList()); - List result = noticeService.readNoticeList(); + Page result = noticeService.readNoticeList(pageable); - assertEquals(getNoticeList().size(), result.size()); - then(noticeRepository).should(times(1)).findAll(any(Sort.class)); + assertEquals(getNoticeList().getTotalElements(), result.getTotalElements()); + then(noticeRepository).should(times(1)).findAllByOrderByCreatedAtDesc(any()); } @Test @DisplayName("공지사항을 정상적으로 조회한다") void succeed_to_read_notice() { given(noticeRepository.findById(any())).willReturn(Optional.of(notice1)); + given(noticeImageRepository.findByNotice(any())).willReturn(List.of(noticeImage)); ReadNoticeDetailResponseDto result = noticeService.readNotice(1L); @@ -74,8 +87,13 @@ void should_throw_NotFoundException_when_read_notice_but_notice_does_not_exist() .content("이벤트 배너를 확인하세요") .build(); - private List getNoticeList() { - return List.of(notice1, notice2); + NoticeImage noticeImage = NoticeImage.builder() + .notice(Notice.builder().id(1L).imageUrls(new ArrayList<>()).build()) + .imageUrl("https://어쩌구저쩌구/notice/공지이미지.png") + .build(); + + private Page getNoticeList() { + return new PageImpl<>(List.of(notice1, notice2)); } }