diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java index 9909e187..bdb5bc29 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java @@ -101,14 +101,24 @@ public BaseResponse> getDailyCoordina return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response); } - @GetMapping("/daily/today") + @GetMapping("/daily/today/preview") @Operation( - operationId = "Coordinate_getTodayDailyCoordinateClothes", - summary = "오늘의 코디 옷 정보 조회", - description = "오늘의 코디에 포함된 옷 정보를 조회하는 API입니다.") - public BaseResponse> getTodayDailyCoordinateClothes() { - List response = - coordinateService.getTodayDailyCoordinateClothes(); + operationId = "Coordinate_getTodayCoordinatePreview", + summary = "오늘의 코디 Preview 조회", + description = "오늘의 코디의 Preview를 조회하는 API입니다.") + public BaseResponse getTodayCoordinatePreview() { + DailyCoordinatePreviewResponse response = coordinateService.getTodayCoordinatePreview(); + return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response); + } + + @GetMapping("/daily/today/details") + @Operation( + operationId = "Coordinate_getTodayCoordinateDetails", + summary = "오늘의 코디 Details 조회", + description = "오늘의 코디의 Details를 조회하는 API입니다.") + public BaseResponse> getTodayCoordinateDetails() { + List response = + coordinateService.getTodayCoordinateDetails(); return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response); } diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinateClothResponse.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinateClothResponse.java deleted file mode 100644 index 5056f71f..00000000 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinateClothResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.clokey.domain.coordinate.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(name = "DailyCoordinateClothResponse", description = "오늘의 코디 옷 정보") -public record DailyCoordinateClothResponse( - @Schema(description = "옷 이미지 URL", example = "https://example.jpg") String imageUrl, - @Schema(description = "브랜드", example = "나이키") String brand, - @Schema(description = "옷 이름", example = "맨투맨") String name, - @Schema(description = "하위 카테고리", example = "맨투맨") String category, - @Schema(description = "상위 카테고리", example = "상의") String parentCategory) { - public static DailyCoordinateClothResponse from(CoordinateDetailsListResponse details) { - return new DailyCoordinateClothResponse( - details.imageUrl(), - details.brand(), - details.name(), - details.category(), - details.parentCategory()); - } -} diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinatePreviewResponse.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinatePreviewResponse.java new file mode 100644 index 00000000..cb655322 --- /dev/null +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/response/DailyCoordinatePreviewResponse.java @@ -0,0 +1,17 @@ +package org.clokey.domain.coordinate.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import org.clokey.coordinate.entity.Coordinate; + +public record DailyCoordinatePreviewResponse( + @Schema(description = "오늘의 코디의 ID", example = "1") Long coordinateId, + @Schema(description = "오늘의 코디의 imageUrl", example = "https://example.jpg") String imageUrl, + @Schema(description = "오늘의 코디의 날짜", example = "2026-02-04") LocalDate date) { + public static DailyCoordinatePreviewResponse from(Coordinate coordinate) { + return new DailyCoordinatePreviewResponse( + coordinate.getId(), + coordinate.getImageUrl(), + coordinate.getUpdatedAt().toLocalDate()); + } +} diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/exception/CoordinateErrorCode.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/exception/CoordinateErrorCode.java index ff1eb770..9da201d7 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/exception/CoordinateErrorCode.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/exception/CoordinateErrorCode.java @@ -15,9 +15,10 @@ public enum CoordinateErrorCode implements BaseErrorCode { COORDINATE_NOT_IN_LOOK_BOOK(400, "COORDINATE_4005", "룩북에 속해있지 않은 (오늘의) 코디입니다."), COORDINATE_LIKE_LIMIT(400, "COORDINATE_4006", "최대 5개의 코디에 좋아요를 누를 수 있습니다."), - NOT_COORDINATE_OWNER(400, "COORDINATE_4031", "나의 코디가 아닙니다. 권한이 없습니다."), + NOT_COORDINATE_OWNER(403, "COORDINATE_4031", "나의 코디가 아닙니다. 권한이 없습니다."), - COORDINATE_NOT_FOUND(400, "COORDINATE_4041", "존재하지 않는 코디입니다."); + COORDINATE_NOT_FOUND(404, "COORDINATE_4041", "존재하지 않는 코디입니다."), + DAILY_COORDINATE_NOT_FOUND(404, "COORDINATE_4042", "오늘의 코디가 존재하지 않습니다."); private final int status; private final String code; diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java index 1165a19f..c616509a 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java @@ -24,7 +24,9 @@ public interface CoordinateService { SliceResponse getDailyCoordinates( Long lastCoordinateId, int size, SortDirection direction); - List getTodayDailyCoordinateClothes(); + DailyCoordinatePreviewResponse getTodayCoordinatePreview(); + + List getTodayCoordinateDetails(); CoordinatePreviewResponse getCoordinatePreview(Long coordinateId); diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java index 63d41751..914ee78a 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java @@ -322,25 +322,19 @@ public SliceResponse getDailyCoordinates( } @Override - public List getTodayDailyCoordinateClothes() { + public DailyCoordinatePreviewResponse getTodayCoordinatePreview() { final Member currentMember = memberUtil.getCurrentMember(); - Optional coordinate = - coordinateRepository.findDailyCoordinateByDateAndMemberId( - LocalDate.now(), currentMember.getId()); + final Coordinate coordinate = getTodayDailyCoordinate(currentMember); - if (coordinate.isEmpty()) { - return List.of(); - } - - List details = - coordinateRepository.findAllCoordinateDetailsByCoordinateId( - coordinate.get().getId()); + return DailyCoordinatePreviewResponse.from(coordinate); + } - if (details.isEmpty()) { - return List.of(); - } + @Override + public List getTodayCoordinateDetails() { + final Member currentMember = memberUtil.getCurrentMember(); + final Coordinate coordinate = getTodayDailyCoordinate(currentMember); - return details.stream().map(DailyCoordinateClothResponse::from).toList(); + return coordinateRepository.findAllCoordinateDetailsByCoordinateId(coordinate.getId()); } @Override @@ -487,4 +481,13 @@ private Coordinate getCoordinateById(Long coordinateId) { .orElseThrow( () -> new BaseCustomException(CoordinateErrorCode.COORDINATE_NOT_FOUND)); } + + private Coordinate getTodayDailyCoordinate(Member member) { + return coordinateRepository + .findDailyCoordinateByDateAndMemberId(LocalDate.now(), member.getId()) + .orElseThrow( + () -> + new BaseCustomException( + CoordinateErrorCode.DAILY_COORDINATE_NOT_FOUND)); + } } diff --git a/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java b/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java index 19a97b37..b14ad183 100644 --- a/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java @@ -7,6 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.clokey.domain.coordinate.dto.request.CoordinateAutoCreateRequest; @@ -1663,47 +1664,106 @@ class 최애_코디_조회_요청_시 { } @Nested - class 오늘의_코디_조회_요청_시 { + class 오늘의_코디_Preview_조회_요청_시 { @Test - void 유효한_요청이면_오늘의_코디_정보를_반환한다() throws Exception { + void 유효한_요청이면_오늘의_코디_Preview를_반환한다() throws Exception { // given - List response = + LocalDate today = LocalDate.now(); + DailyCoordinatePreviewResponse response = + new DailyCoordinatePreviewResponse(1L, "testImageUrl", LocalDate.now()); + given(coordinateService.getTodayCoordinatePreview()).willReturn(response); + + // when & then + ResultActions perform = + mockMvc.perform( + get("/coordinate/daily/today/preview") + .contentType(MediaType.APPLICATION_JSON)); + + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("성공입니다.")) + .andExpect(jsonPath("$.result.coordinateId").value(1)) + .andExpect(jsonPath("$.result.imageUrl").value("testImageUrl")) + .andExpect(jsonPath("$.result.date").value(today.toString())); + } + } + + @Nested + class 오늘의_코디_Details_조회_요청_시 { + + @Test + void 유효한_요청이면_오늘의_코디_Details를_반환한다() throws Exception { + // given + List response = List.of( - new DailyCoordinateClothResponse( - "https://image.example/cloth1.jpg", - "brand1", - "name1", - "category1", - "parent1"), - new DailyCoordinateClothResponse( - "https://image.example/cloth2.jpg", - "brand2", - "name2", - "category2", - "parent2")); - - given(coordinateService.getTodayDailyCoordinateClothes()).willReturn(response); - - ResultActions perform = mockMvc.perform(get("/coordinate/daily/today")); + new CoordinateDetailsListResponse( + 1L, + 50.2, + 60.1, + 1.5, + 240.1, + 1, + 14L, + "testImageUrl1", + "testBrand1", + "testName1", + "testCategoryName1", + "testParentCategoryName1"), + new CoordinateDetailsListResponse( + 2L, + 50.2, + 60.1, + 1.5, + 240.1, + 2, + 15L, + "testImageUrl2", + "testBrand2", + "testName2", + "testCategoryName2", + "testParentCategoryName2")); + + given(coordinateService.getTodayCoordinateDetails()).willReturn(response); + // when & then + ResultActions perform = + mockMvc.perform( + get("/coordinate/daily/today/details") + .contentType(MediaType.APPLICATION_JSON)); + perform.andExpect(status().isOk()) .andExpect(jsonPath("$.isSuccess").value(true)) .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("성공입니다.")) + .andExpect(jsonPath("$.result[0].coordinateClothId").value(1)) + .andExpect(jsonPath("$.result[0].locationX").value(50.2)) + .andExpect(jsonPath("$.result[0].locationY").value(60.1)) + .andExpect(jsonPath("$.result[0].ratio").value(1.5)) + .andExpect(jsonPath("$.result[0].degree").value(240.1)) + .andExpect(jsonPath("$.result[0].order").value(1)) + .andExpect(jsonPath("$.result[0].clothId").value(14)) + .andExpect(jsonPath("$.result[0].imageUrl").value("testImageUrl1")) + .andExpect(jsonPath("$.result[0].brand").value("testBrand1")) + .andExpect(jsonPath("$.result[0].name").value("testName1")) + .andExpect(jsonPath("$.result[0].category").value("testCategoryName1")) .andExpect( - jsonPath("$.result[0].imageUrl") - .value("https://image.example/cloth1.jpg")) - .andExpect(jsonPath("$.result[0].brand").value("brand1")) - .andExpect(jsonPath("$.result[0].name").value("name1")) - .andExpect(jsonPath("$.result[0].category").value("category1")) - .andExpect(jsonPath("$.result[0].parentCategory").value("parent1")) + jsonPath("$.result[0].parentCategory").value("testParentCategoryName1")) + .andExpect(jsonPath("$.result[1].coordinateClothId").value(2)) + .andExpect(jsonPath("$.result[1].locationX").value(50.2)) + .andExpect(jsonPath("$.result[1].locationY").value(60.1)) + .andExpect(jsonPath("$.result[1].ratio").value(1.5)) + .andExpect(jsonPath("$.result[1].degree").value(240.1)) + .andExpect(jsonPath("$.result[1].order").value(2)) + .andExpect(jsonPath("$.result[1].clothId").value(15)) + .andExpect(jsonPath("$.result[1].imageUrl").value("testImageUrl2")) + .andExpect(jsonPath("$.result[1].brand").value("testBrand2")) + .andExpect(jsonPath("$.result[1].name").value("testName2")) + .andExpect(jsonPath("$.result[1].category").value("testCategoryName2")) .andExpect( - jsonPath("$.result[1].imageUrl") - .value("https://image.example/cloth2.jpg")) - .andExpect(jsonPath("$.result[1].brand").value("brand2")) - .andExpect(jsonPath("$.result[1].name").value("name2")) - .andExpect(jsonPath("$.result[1].category").value("category2")) - .andExpect(jsonPath("$.result[1].parentCategory").value("parent2")); + jsonPath("$.result[1].parentCategory") + .value("testParentCategoryName2")); } } } diff --git a/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java b/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java index cd71bf83..ca2675c5 100644 --- a/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java @@ -24,11 +24,7 @@ import org.clokey.domain.coordinate.dto.request.CoordinateManualCreateRequest; import org.clokey.domain.coordinate.dto.request.CoordinateUpdateRequest; import org.clokey.domain.coordinate.dto.request.DailyCoordinateCreateRequest; -import org.clokey.domain.coordinate.dto.response.CoordinateDetailsListResponse; -import org.clokey.domain.coordinate.dto.response.CoordinatePreviewResponse; -import org.clokey.domain.coordinate.dto.response.DailyCoordinateClothResponse; -import org.clokey.domain.coordinate.dto.response.DailyCoordinateListResponse; -import org.clokey.domain.coordinate.dto.response.FavoriteCoordinateResponse; +import org.clokey.domain.coordinate.dto.response.*; import org.clokey.domain.coordinate.exception.CoordinateErrorCode; import org.clokey.domain.coordinate.repository.CoordinateClothRepository; import org.clokey.domain.coordinate.repository.CoordinateRepository; @@ -1971,88 +1967,169 @@ void setUp() { } @Nested - class 오늘의_코디를_조회할_때 { + class 오늘의_코디_Preview를_조회할_때 { @BeforeEach void setUp() { Member member1 = Member.createMember( - "todayEmail1", - "todayNickName1", - OauthInfo.createOauthInfo("todayOauthId1", OauthProvider.KAKAO)); + "testEmail1", + "testNickName1", + OauthInfo.createOauthInfo("testOauthId1", OauthProvider.KAKAO)); + Member member2 = + Member.createMember( + "testEmail2", + "testNickName2", + OauthInfo.createOauthInfo("testOauthId2", OauthProvider.KAKAO)); + + memberRepository.saveAll(List.of(member1, member2)); + given(memberUtil.getCurrentMember()).willReturn(member1); + + Coordinate coordinate1 = Coordinate.createDailyCoordinate("testImageUrl1", member1); + coordinateRepository.save(coordinate1); + } + + @Test + void 유효한_요청이면_Preview를_반환한다() { + // when + DailyCoordinatePreviewResponse response = coordinateService.getTodayCoordinatePreview(); + + // then + assertThat(response) + .extracting("coordinateId", "imageUrl", "date") + .containsExactly(1L, "testImageUrl1", LocalDate.now()); + } + + @Test + void 오늘의_코디가_존재하지_않으면_예외가_발생한다() { + // given + Member member = memberRepository.findById(2L).orElseThrow(); + given(memberUtil.getCurrentMember()).willReturn(member); + + // when & then + assertThatThrownBy(() -> coordinateService.getTodayCoordinatePreview()) + .isInstanceOf(BaseCustomException.class) + .hasMessage(CoordinateErrorCode.DAILY_COORDINATE_NOT_FOUND.getMessage()); + } + } + + @Nested + class 오늘의_코디_Details를_조회할_때 { + + @BeforeEach + void setUp() { + Member member1 = + Member.createMember( + "testEmail1", + "testNickName1", + OauthInfo.createOauthInfo("testOauthId1", OauthProvider.KAKAO)); Member member2 = Member.createMember( - "todayEmail2", - "todayNickName2", - OauthInfo.createOauthInfo("todayOauthId2", OauthProvider.KAKAO)); + "testEmail2", + "testNickName2", + OauthInfo.createOauthInfo("testOauthId2", OauthProvider.KAKAO)); + memberRepository.saveAll(List.of(member1, member2)); given(memberUtil.getCurrentMember()).willReturn(member1); - Category parentCategory = Category.createCategory("parentCategory", null); - Category category = Category.createCategory("category", parentCategory); + Coordinate coordinate1 = Coordinate.createDailyCoordinate("testImageUrl1", member1); + coordinateRepository.save(coordinate1); + + Category parentCategory = Category.createCategory("testParentCategory", null); + Category category = Category.createCategory("testCategory", parentCategory); categoryRepository.saveAll(List.of(parentCategory, category)); Cloth cloth1 = Cloth.createCloth( - "imageUrl1", + "testImageUrl1", + null, + null, null, - "name1", - "brand1", List.of(Season.SPRING), category, member1); Cloth cloth2 = Cloth.createCloth( - "imageUrl2", + "testImageUrl2", + null, + null, null, - "name2", - "brand2", List.of(Season.SPRING), category, member1); clothRepository.saveAll(List.of(cloth1, cloth2)); - Coordinate coordinate = Coordinate.createDailyCoordinate("coordImage", member1); - coordinateRepository.save(coordinate); - CoordinateCloth coordinateCloth1 = CoordinateCloth.createCoordinateCloth( - 10.0, 20.0, 1.0, 30.0, 1, coordinate, cloth1); + 50.1, 120.1, 1.5, 240.1, 1, coordinate1, cloth1); + CoordinateCloth coordinateCloth2 = CoordinateCloth.createCoordinateCloth( - 11.0, 21.0, 1.0, 31.0, 2, coordinate, cloth2); + 50.1, 120.1, 1.5, 240.1, 2, coordinate1, cloth2); + coordinateClothRepository.saveAll(List.of(coordinateCloth1, coordinateCloth2)); } @Test - void 유효한_요청이면_오늘의_코디를_반환한다() { + void 유효한_요청이면_코디_Details를_반환한다() { // when - List responses = - coordinateService.getTodayDailyCoordinateClothes(); + List response = + coordinateService.getTodayCoordinateDetails(); // then - assertThat(responses) + assertThat(response) .extracting( - DailyCoordinateClothResponse::imageUrl, - DailyCoordinateClothResponse::brand, - DailyCoordinateClothResponse::name, - DailyCoordinateClothResponse::category, - DailyCoordinateClothResponse::parentCategory) + CoordinateDetailsListResponse::coordinateClothId, + CoordinateDetailsListResponse::locationX, + CoordinateDetailsListResponse::locationY, + CoordinateDetailsListResponse::ratio, + CoordinateDetailsListResponse::degree, + CoordinateDetailsListResponse::order, + CoordinateDetailsListResponse::clothId, + CoordinateDetailsListResponse::imageUrl, + CoordinateDetailsListResponse::brand, + CoordinateDetailsListResponse::name, + CoordinateDetailsListResponse::category, + CoordinateDetailsListResponse::parentCategory) .containsExactly( - tuple("imageUrl1", "brand1", "name1", "category", "parentCategory"), - tuple("imageUrl2", "brand2", "name2", "category", "parentCategory")); + tuple( + 1L, + 50.1, + 120.1, + 1.5, + 240.1, + 1, + 1L, + "testImageUrl1", + null, + null, + "testCategory", + "testParentCategory"), + tuple( + 2L, + 50.1, + 120.1, + 1.5, + 240.1, + 2, + 2L, + "testImageUrl2", + null, + null, + "testCategory", + "testParentCategory")); } @Test - void 오늘의_코디가_없으면_빈_리스트를_반환한다() { + void 오늘의_코디가_존재하지_않으면_예외가_발생한다() { // given - Member member = memberRepository.findByNickname("todayNickName2").orElseThrow(); + Member member = memberRepository.findById(2L).orElseThrow(); given(memberUtil.getCurrentMember()).willReturn(member); - // when - List responses = - coordinateService.getTodayDailyCoordinateClothes(); - // then - assertThat(responses).isEmpty(); + + // when & then + assertThatThrownBy(() -> coordinateService.getTodayCoordinateDetails()) + .isInstanceOf(BaseCustomException.class) + .hasMessage(CoordinateErrorCode.DAILY_COORDINATE_NOT_FOUND.getMessage()); } } }