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..c9f392fd 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -54,6 +54,24 @@ 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 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/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` | 해당하는 공지사항이 없는 경우 +|=== 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..ff4200c2 --- /dev/null +++ b/src/main/java/com/cvsgo/controller/NoticeController.java @@ -0,0 +1,33 @@ +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.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; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/notices") +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping + public SuccessResponse> readNoticeList(Pageable pageable) { + return SuccessResponse.from(noticeService.readNoticeList(pageable)); + } + + @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..2de04a5f --- /dev/null +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeDetailResponseDto.java @@ -0,0 +1,34 @@ +package com.cvsgo.dto.notice; + +import com.cvsgo.entity.Notice; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; + +@Getter +public class ReadNoticeDetailResponseDto { + + private final Long id; + + private final String title; + + private final String content; + + private final List noticeImageUrls; + + @JsonFormat(pattern = "yy.MM.dd", timezone = "Asia/Seoul") + private final LocalDateTime createdAt; + + 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 of(Notice notice, List noticeImageUrls) { + return new ReadNoticeDetailResponseDto(notice, noticeImageUrls); + } +} 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..64aedb70 --- /dev/null +++ b/src/main/java/com/cvsgo/dto/notice/ReadNoticeResponseDto.java @@ -0,0 +1,34 @@ +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; + } + + public static ReadNoticeResponseDto from(Notice notice) { + return new ReadNoticeResponseDto(notice); + } + +} 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..b28b371f --- /dev/null +++ b/src/main/java/com/cvsgo/entity/Notice.java @@ -0,0 +1,54 @@ +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; +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; + + @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, 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/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/repository/NoticeImageRepository.java b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java new file mode 100644 index 00000000..a49e8863 --- /dev/null +++ b/src/main/java/com/cvsgo/repository/NoticeImageRepository.java @@ -0,0 +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/repository/NoticeRepository.java b/src/main/java/com/cvsgo/repository/NoticeRepository.java new file mode 100644 index 00000000..253aa021 --- /dev/null +++ b/src/main/java/com/cvsgo/repository/NoticeRepository.java @@ -0,0 +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 new file mode 100644 index 00000000..a47e1ea1 --- /dev/null +++ b/src/main/java/com/cvsgo/service/NoticeService.java @@ -0,0 +1,54 @@ +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.entity.NoticeImage; +import com.cvsgo.exception.NotFoundException; +import com.cvsgo.repository.NoticeImageRepository; +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; + +@Service +@RequiredArgsConstructor +public class NoticeService { + + private final NoticeRepository noticeRepository; + private final NoticeImageRepository noticeImageRepository; + + /** + * 공지사항 목록을 조회한다. + * + * @return 공지사항 목록 + */ + @Transactional(readOnly = true) + public Page readNoticeList(Pageable pageable) { + return noticeRepository.findAllByOrderByCreatedAtDesc(pageable) + .map(ReadNoticeResponseDto::from); + } + + /** + * 공지사항을 상세 조회한다. + * + * @param noticeId 공지사항 ID + * @return 공지사항 상세 정보 + * @throws NotFoundException 해당하는 아이디를 가진 공지사항이 없는 경우 + */ + @Transactional(readOnly = true) + public ReadNoticeDetailResponseDto readNotice(Long noticeId) { + Notice notice = noticeRepository.findById(noticeId).orElseThrow(() -> NOT_FOUND_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 new file mode 100644 index 00000000..04aaf026 --- /dev/null +++ b/src/test/java/com/cvsgo/controller/NoticeControllerTest.java @@ -0,0 +1,137 @@ +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.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.entity.NoticeImage; +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.data.domain.PageImpl; +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(any())).willReturn(new PageImpl<>(responseDto)); + + mockMvc.perform(get("/api/notices").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document(documentIdentifier, + getDocumentRequest(), + getDocumentResponse(), + relaxedResponseFields( + 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() + ) + )); + } + + @Test + @DisplayName("공지사항을 정상적으로 조회하면 HTTP 200을 응답한다") + void respond_200_when_read_notice_successfully() throws Exception { + ReadNoticeDetailResponseDto responseDto = ReadNoticeDetailResponseDto.of(notice1, List.of(noticeImage.getImageUrl())); + 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.noticeImageUrls").type(JsonFieldType.ARRAY).description("공지사항 이미지 URL").optional(), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("공지사항 생성 시간").optional() + ) + )); + } + + Notice notice1 = Notice.builder() + .id(1L) + .title("긴급 공지") + .content("긴급 상황입니다") + .build(); + + Notice notice2 = Notice.builder() + .id(2L) + .title("이벤트 공지") + .content("이벤트 배너를 확인하세요") + .build(); + + NoticeImage noticeImage = NoticeImage.builder() + .id(1L) + .imageUrl("imageUrl") + .notice(notice1) + .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..1cdee4ac --- /dev/null +++ b/src/test/java/com/cvsgo/service/NoticeServiceTest.java @@ -0,0 +1,99 @@ +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.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; +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.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) +class NoticeServiceTest { + + @Mock + private NoticeRepository noticeRepository; + + @Mock + private NoticeImageRepository noticeImageRepository; + + @InjectMocks + NoticeService noticeService; + + @Test + @DisplayName("공지사항 목록을 정상적으로 조회한다") + void succeed_to_read_notice_list() { + Pageable pageable = PageRequest.of(0, 20); + given(noticeRepository.findAllByOrderByCreatedAtDesc(any())).willReturn(getNoticeList()); + + Page result = noticeService.readNoticeList(pageable); + + 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); + + 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(); + + 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)); + } + +}