From d4485c3270d733f69a8dc82c01b6e057dc88c80c Mon Sep 17 00:00:00 2001 From: leedy5521 <80202719+dev2yup@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:36:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[refactor/#301]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 댓글 조회 API 대댓글 개수 필드 추가 * test/refactor: 댓글 조회 API 컨트롤러 테스트 수정 * fix: 레포지토리 수정 * test/refactor: 댓글 조회 API 서비스 테스트 수정 --- .../dto/response/CommentListResponse.java | 1 + .../repository/CommentRepositoryImpl.java | 21 +++++++++++++------ .../controller/CommentControllerTest.java | 8 ++++++- .../comment/service/CommentServiceTest.java | 4 ++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/clokey-api/src/main/java/org/clokey/domain/comment/dto/response/CommentListResponse.java b/clokey-api/src/main/java/org/clokey/domain/comment/dto/response/CommentListResponse.java index ecf8dd88..2b2ccb44 100644 --- a/clokey-api/src/main/java/org/clokey/domain/comment/dto/response/CommentListResponse.java +++ b/clokey-api/src/main/java/org/clokey/domain/comment/dto/response/CommentListResponse.java @@ -10,4 +10,5 @@ public record CommentListResponse( String profileImageUrl, @Schema(description = "댓글 내용", example = "이 옷 정보 알려주세요!") String content, @Schema(description = "대댓글 존재 여부", example = "false") boolean replied, + @Schema(description = "총 대댓글 개수", example = "0") long replyCount, @Schema(description = "내가 작성한 댓글인가?", example = "false") boolean isMine) {} diff --git a/clokey-api/src/main/java/org/clokey/domain/comment/repository/CommentRepositoryImpl.java b/clokey-api/src/main/java/org/clokey/domain/comment/repository/CommentRepositoryImpl.java index 32faca15..f18cc699 100644 --- a/clokey-api/src/main/java/org/clokey/domain/comment/repository/CommentRepositoryImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/comment/repository/CommentRepositoryImpl.java @@ -5,9 +5,11 @@ import static org.clokey.history.entity.QHistoryImage.historyImage; import static org.clokey.member.entity.QMember.member; +import com.querydsl.core.Tuple; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.ArrayList; import java.util.List; @@ -52,6 +54,7 @@ public Slice findAllParentCommentByHistoryId( member.profileImageUrl, comment.content, Expressions.constant(false), + Expressions.constant(0L), member.id.eq(currentMemberId))) .from(comment) .join(comment.member, member) @@ -71,9 +74,10 @@ public Slice findAllParentCommentByHistoryId( results = results.subList(0, size); } - List parentIdsWithReplies = + NumberExpression replyCountExpression = comment.id.count(); + List replyCounts = queryFactory - .select(comment.comment.id) + .select(comment.comment.id, replyCountExpression) .from(comment) .where( comment.comment.id.in( @@ -83,9 +87,13 @@ public Slice findAllParentCommentByHistoryId( .groupBy(comment.comment.id) .fetch(); - // 각 부모 댓글이 대댓글을 가지고 있는지 여부 조회 - Map repliedMap = - parentIdsWithReplies.stream().collect(Collectors.toMap(id -> id, id -> true)); + // 각 부모 댓글별 대댓글 개수 조회 + Map replyCountMap = + replyCounts.stream() + .collect( + Collectors.toMap( + tuple -> tuple.get(comment.comment.id), + tuple -> tuple.get(replyCountExpression))); List finalResults = results.stream() @@ -97,7 +105,8 @@ public Slice findAllParentCommentByHistoryId( c.nickname(), c.profileImageUrl(), c.content(), - repliedMap.getOrDefault(c.commentId(), false), + replyCountMap.getOrDefault(c.commentId(), 0L) > 0, + replyCountMap.getOrDefault(c.commentId(), 0L), c.isMine())) .toList(); diff --git a/clokey-api/src/test/java/org/clokey/domain/comment/controller/CommentControllerTest.java b/clokey-api/src/test/java/org/clokey/domain/comment/controller/CommentControllerTest.java index 7ce08240..1f95e3ec 100644 --- a/clokey-api/src/test/java/org/clokey/domain/comment/controller/CommentControllerTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/comment/controller/CommentControllerTest.java @@ -1,6 +1,5 @@ package org.clokey.domain.comment.controller; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -292,6 +291,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent1", false, + 0L, true), new CommentListResponse( 2L, @@ -300,6 +300,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent2", false, + 0L, true)); given(commentService.getHistoryComments(1L, null, 2, SortDirection.ASC)) @@ -333,6 +334,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent2", false, + 0L, true), new CommentListResponse( 1L, @@ -341,6 +343,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent1", false, + 0L, true)); given(commentService.getHistoryComments(1L, null, 2, SortDirection.DESC)) @@ -374,6 +377,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent2", false, + 0L, true)); given(commentService.getHistoryComments(1L, null, 1, SortDirection.ASC)) @@ -406,6 +410,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent1", false, + 0L, true), new CommentListResponse( 2L, @@ -414,6 +419,7 @@ class 기록의_댓글_목록_조회_요청_시 { "testProfile", "testContent2", false, + 0L, true)); given(commentService.getHistoryComments(1L, null, 1, SortDirection.ASC)) diff --git a/clokey-api/src/test/java/org/clokey/domain/comment/service/CommentServiceTest.java b/clokey-api/src/test/java/org/clokey/domain/comment/service/CommentServiceTest.java index a9190d0d..11eab2dd 100644 --- a/clokey-api/src/test/java/org/clokey/domain/comment/service/CommentServiceTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/comment/service/CommentServiceTest.java @@ -363,6 +363,10 @@ void setUp() { // then assertThat(response.content()).extracting("commentId").containsExactly(1L, 2L, 3L); + assertThat(response.content()) + .extracting("commentId", "replied", "replyCount") + .containsExactly( + tuple(1L, true, 1L), tuple(2L, false, 0L), tuple(3L, false, 0L)); } @Test From e85a6fec3cefc41b6b583f3d7aaf22572554bc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EC=9A=A9=EC=A4=80?= <141994188+youngJun99@users.noreply.github.com> Date: Sun, 1 Feb 2026 01:19:35 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20null=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/CoordinateAutoCreateRequest.java | 5 +- .../service/CoordinateServiceImpl.java | 13 ++++- .../controller/CoordinateControllerTest.java | 22 ++++---- .../service/CoordinateServiceImplTest.java | 50 +++++++++++++++++++ 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/request/CoordinateAutoCreateRequest.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/request/CoordinateAutoCreateRequest.java index 2f1211ff..d9683c5d 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/request/CoordinateAutoCreateRequest.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/dto/request/CoordinateAutoCreateRequest.java @@ -1,14 +1,11 @@ package org.clokey.domain.coordinate.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public record CoordinateAutoCreateRequest( - @NotBlank(message = "코디의 이름은 비워둘 수 없습니다.") - @Schema(description = "코디의 이름", example = "데이트 코디") - String name, + @Schema(description = "코디의 이름 (비어있으면 날짜 기반으로 자동 생성)", example = "데이트 코디") String name, @Size(max = 100, message = "메모는 최대 100자까지 입력할 수 있습니다.") @Schema(description = "코디 메모", example = "내일 이거 입고 나가야 함") String memo, 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 d7810aa1..63d41751 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 @@ -1,6 +1,7 @@ package org.clokey.domain.coordinate.service; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -182,11 +183,21 @@ public CoordinateCreateResponse createCoordinateAuto(CoordinateAutoCreateRequest validateLookBookOwner(lookBook, currentMember.getId()); validateDailyCoordinate(dailyCoordinate); - dailyCoordinate.addToDailyCoordinateToLookBook(request.name(), request.memo(), lookBook); + String name = request.name(); + if (name == null || name.isBlank()) { + name = generateDefaultName(dailyCoordinate.getCreatedAt().toLocalDate()); + } + + dailyCoordinate.addToDailyCoordinateToLookBook(name, request.memo(), lookBook); return CoordinateCreateResponse.from(dailyCoordinate); } + private String generateDefaultName(LocalDate date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM.dd.yy"); + return "오늘의 코디 (" + date.format(formatter) + ")"; + } + @Override @Transactional public void updateCoordinate(Long coordinateId, CoordinateUpdateRequest request) { 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 7b43bdd3..a2dd5062 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 @@ -833,14 +833,14 @@ class 코디_자동_생성_요청_시 { .andExpect(jsonPath("$.result.coordinateId").value(1)); } - @ParameterizedTest - @NullSource - @EmptySource - @ValueSource(strings = {" "}) - void 코디의_이름이_null_또는_공백이면_예외가_발생한다(String name) throws Exception { + @Test + void 이름이_비어있어도_정상적으로_생성된다() throws Exception { // given CoordinateAutoCreateRequest request = - new CoordinateAutoCreateRequest(name, "testMemo", 1L, 1L); + new CoordinateAutoCreateRequest(null, "testMemo", 1L, 1L); + + CoordinateCreateResponse response = new CoordinateCreateResponse(1L); + given(coordinateService.createCoordinateAuto(request)).willReturn(response); // when & then ResultActions perform = @@ -849,11 +849,11 @@ class 코디_자동_생성_요청_시 { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); - perform.andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.isSuccess").value(false)) - .andExpect(jsonPath("$.code").value("COMMON400")) - .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")) - .andExpect(jsonPath("$.result.name").value("코디의 이름은 비워둘 수 없습니다.")); + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.code").value("COMMON201")) + .andExpect(jsonPath("$.message").value("요청 성공 및 리소스 생성됨")) + .andExpect(jsonPath("$.result.coordinateId").value(1)); } @Test 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 926bbecf..7682838b 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 @@ -5,6 +5,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.mockito.BDDMockito.given; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; import org.clokey.IntegrationTest; import org.clokey.RedisCleaner; @@ -753,6 +755,54 @@ void setUp() { .containsExactly("testName", "testMemo", 1L)); } + @Test + void 이름이_비어있으면_날짜_기반으로_자동_생성한다() { + // given + Coordinate coordinate = coordinateRepository.findById(1L).orElseThrow(); + LocalDate createdAt = coordinate.getCreatedAt().toLocalDate(); + String expectedName = + "오늘의 코디 (" + createdAt.format(DateTimeFormatter.ofPattern("MM.dd.yy")) + ")"; + + CoordinateAutoCreateRequest request = + new CoordinateAutoCreateRequest(null, "testMemo", 1L, 1L); + + // when + coordinateService.createCoordinateAuto(request); + + // then + Coordinate result = coordinateRepository.findById(1L).orElseThrow(); + + Assertions.assertAll( + () -> + assertThat(result) + .extracting("name", "memo", "lookBook.id") + .containsExactly(expectedName, "testMemo", 1L)); + } + + @Test + void 이름이_공백이면_날짜_기반으로_자동_생성한다() { + // given + Coordinate coordinate = coordinateRepository.findById(1L).orElseThrow(); + LocalDate createdAt = coordinate.getCreatedAt().toLocalDate(); + String expectedName = + "오늘의 코디 (" + createdAt.format(DateTimeFormatter.ofPattern("MM.dd.yy")) + ")"; + + CoordinateAutoCreateRequest request = + new CoordinateAutoCreateRequest(" ", "testMemo", 1L, 1L); + + // when + coordinateService.createCoordinateAuto(request); + + // then + Coordinate result = coordinateRepository.findById(1L).orElseThrow(); + + Assertions.assertAll( + () -> + assertThat(result) + .extracting("name", "memo", "lookBook.id") + .containsExactly(expectedName, "testMemo", 1L)); + } + @Test void 나의_코디가_아닌_경우_예외가_발생한다() { // given