From 1c38a853f44e494090afc3765492001c63577ea9 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Tue, 13 Jan 2026 12:49:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(report):=20AnswerEntryQueryRepository?= =?UTF-8?q?=EC=97=90=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 월 단위 감정 코드 조회, 최근 n개 답변 조회, 특정 날짜 답변 조회 쿼리 메서드 추가, 기존처럼 DTO Projection 사용 --- .../core/dto/MonthlyCalendarDto.java | 14 +++++ .../AnswerEntryQueryRepository.java | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/dto/MonthlyCalendarDto.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/dto/MonthlyCalendarDto.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/dto/MonthlyCalendarDto.java new file mode 100644 index 0000000..b71cb59 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/dto/MonthlyCalendarDto.java @@ -0,0 +1,14 @@ +package com.devkor.ifive.nadab.domain.dailyreport.core.dto; + +import com.devkor.ifive.nadab.domain.dailyreport.core.entity.EmotionCode; + +import java.time.LocalDate; + +/** + * 월별 캘린더 쿼리 결과 DTO + */ +public record MonthlyCalendarDto( + LocalDate date, + EmotionCode emotionCode // null 가능 (리포트가 없거나 PENDING/FAILED 상태) +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java index 13e97bf..b7a4742 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java @@ -1,5 +1,6 @@ package com.devkor.ifive.nadab.domain.dailyreport.core.repository; +import com.devkor.ifive.nadab.domain.dailyreport.core.dto.MonthlyCalendarDto; import com.devkor.ifive.nadab.domain.dailyreport.core.dto.SearchAnswerEntryDto; import com.devkor.ifive.nadab.domain.dailyreport.core.entity.AnswerEntry; import com.devkor.ifive.nadab.domain.dailyreport.core.entity.EmotionCode; @@ -10,6 +11,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.Optional; public interface AnswerEntryQueryRepository extends Repository { @@ -58,4 +60,61 @@ List searchAnswerEntriesWithCursor( @Param("cursorDate") LocalDate cursorDate, Pageable pageable ); + + /** + * 월별 캘린더 데이터 조회 + */ + @Query(""" + select new com.devkor.ifive.nadab.domain.dailyreport.core.dto.MonthlyCalendarDto( + ae.date, e.code + ) + from AnswerEntry ae + left join DailyReport dr on dr.answerEntry = ae and dr.status = com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReportStatus.COMPLETED + left join dr.emotion e + where ae.user.id = :userId + and ae.date >= :startDate + and ae.date <= :endDate + order by ae.date asc + """) + List findCalendarEntriesInMonth( + @Param("userId") Long userId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + + /** + * 최근 N개 답변 조회 (날짜 내림차순) + */ + @Query(""" + select new com.devkor.ifive.nadab.domain.dailyreport.core.dto.SearchAnswerEntryDto( + ae.id, ae.question.interest.code, e.code, ae.question.questionText, ae.content, ae.date + ) + from AnswerEntry ae + left join DailyReport dr on dr.answerEntry = ae and dr.status = com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReportStatus.COMPLETED + left join dr.emotion e + where ae.user.id = :userId + order by ae.date desc + """) + List findRecentAnswers( + @Param("userId") Long userId, + Pageable pageable + ); + + /** + * 특정 날짜 답변 조회 + */ + @Query(""" + select new com.devkor.ifive.nadab.domain.dailyreport.core.dto.SearchAnswerEntryDto( + ae.id, ae.question.interest.code, e.code, ae.question.questionText, ae.content, ae.date + ) + from AnswerEntry ae + left join DailyReport dr on dr.answerEntry = ae and dr.status = com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReportStatus.COMPLETED + left join dr.emotion e + where ae.user.id = :userId + and ae.date = :date + """) + Optional findByUserAndDate( + @Param("userId") Long userId, + @Param("date") LocalDate date + ); } \ No newline at end of file From f5cb7be9ad44e7cf25b8be9ac09e7598d9e14b13 Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Tue, 13 Jan 2026 12:57:01 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(report):=20=EC=BA=98=EB=A6=B0=EB=8D=94?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8B=B5=EB=B3=80/=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20service,=20controller=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비슷한 로직의 경우 기존 response dto를 재활용, 특정 월의 답변이 있는 날짜와 감정 코드 조회, 최근기록 6개까지 미리보기, 특정 날짜 답변 미리보기 API 구현, 예전 감정 코드로 문서가 작성되어 있는 부분 수정 --- .../dailyreport/api/AnswerController.java | 108 +++++++++++++++++- .../request/GetMonthlyCalendarRequest.java | 20 ++++ .../dto/request/SearchAnswerEntryRequest.java | 2 +- .../response/AnswerEntrySummaryResponse.java | 14 ++- .../dto/response/CalendarEntryResponse.java | 22 ++++ .../dto/response/CalendarRecentsResponse.java | 15 +++ .../dto/response/MonthlyCalendarResponse.java | 12 ++ .../application/AnswerQueryService.java | 55 ++++++++- 8 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/GetMonthlyCalendarRequest.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarEntryResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarRecentsResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/MonthlyCalendarResponse.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/AnswerController.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/AnswerController.java index 441223a..e7ddd2b 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/AnswerController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/AnswerController.java @@ -1,7 +1,11 @@ package com.devkor.ifive.nadab.domain.dailyreport.api; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.GetMonthlyCalendarRequest; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.SearchAnswerEntryRequest; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.AnswerEntrySummaryResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.CalendarRecentsResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.DailyReportResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.MonthlyCalendarResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.SearchAnswerEntryResponse; import com.devkor.ifive.nadab.domain.dailyreport.application.AnswerQueryService; import com.devkor.ifive.nadab.domain.dailyreport.application.DailyReportQueryService; @@ -22,7 +26,9 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -@Tag(name = "답변 API", description = "답변 검색 및 조회 관련 API") +import java.time.LocalDate; + +@Tag(name = "답변 API", description = "답변 검색, 조회 및 캘린더 관련 API") @RestController @RequestMapping("${api_prefix}/answers") @RequiredArgsConstructor @@ -79,7 +85,7 @@ public class AnswerController { description = "검색 성공", content = @Content(schema = @Schema(implementation = SearchAnswerEntryResponse.class)) ), - @ApiResponse(responseCode = "400", description = "잘못된 요청 (cursor 형식 오류 등)", content = @Content), + @ApiResponse(responseCode = "400", description = "ErrorCode: VALIDATION_FAILED - 잘못된 요청 (cursor 형식 오류 등)", content = @Content), @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) } ) @@ -145,4 +151,102 @@ public ResponseEntity> getDailyReportByAnswe DailyReportResponse response = dailyReportQueryService.getDailyReportByAnswerId(principal.getId(), answerId); return ApiResponseEntity.ok(response); } + + @GetMapping("/calendar") + @PreAuthorize("isAuthenticated()") + @Operation( + summary = "월별 캘린더 조회", + description = """ + 특정 월의 답변이 있는 날짜와 감정 코드를 조회합니다. + + - 답변이 없는 날짜는 결과에 포함되지 않습니다. + - emotionCode는 리포트가 COMPLETED 상태일 때만 제공됩니다. + - 리포트가 없거나 PENDING/FAILED 상태면 null입니다. + """, + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = MonthlyCalendarResponse.class)) + ), + @ApiResponse(responseCode = "400", description = "ErrorCode: VALIDATION_FAILED - 잘못된 요청 (연도/월 범위 오류)", content = @Content), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) + } + ) + public ResponseEntity> getMonthlyCalendar( + @AuthenticationPrincipal UserPrincipal principal, + @Valid @ModelAttribute GetMonthlyCalendarRequest request + ) { + MonthlyCalendarResponse response = answerQueryService.getMonthlyCalendar( + principal.getId(), + request + ); + return ApiResponseEntity.ok(response); + } + + @GetMapping("/calendar/recents") + @PreAuthorize("isAuthenticated()") + @Operation( + summary = "캘린더 최근 기록 미리보기", + description = """ + 최근 답변을 최대 6개까지 조회합니다. (날짜 내림차순) + + - 답변이 6개 미만인 경우 전체 답변을 반환합니다. + - 답변이 없는 경우 빈 배열을 반환합니다. + """, + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = CalendarRecentsResponse.class)) + ), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) + } + ) + public ResponseEntity> getCalendarRecents( + @AuthenticationPrincipal UserPrincipal principal + ) { + CalendarRecentsResponse response = answerQueryService.getRecentAnswers(principal.getId()); + return ApiResponseEntity.ok(response); + } + + + @GetMapping("/calendar/{date}") + @PreAuthorize("isAuthenticated()") + @Operation( + summary = "특정 날짜 답변 조회", + description = """ + 특정 날짜의 답변 미리보기를 조회합니다. + + - 해당 날짜에 답변이 없으면 404 에러를 반환합니다. + - 날짜 형식: yyyy-MM-dd (예: 2026-01-30) + """, + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = AnswerEntrySummaryResponse.class)) + ), + @ApiResponse(responseCode = "400", description = "ErrorCode: VALIDATION_FAILED - 잘못된 날짜 형식", content = @Content), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content), + @ApiResponse( + responseCode = "404", + description = "ErrorCode: ANSWER_NOT_FOUND - 해당 날짜에 답변이 없음", + content = @Content + ) + } + ) + public ResponseEntity> getAnswerByDate( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable LocalDate date + ) { + AnswerEntrySummaryResponse response = answerQueryService.getAnswerByDate( + principal.getId(), + date + ); + return ApiResponseEntity.ok(response); + } } \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/GetMonthlyCalendarRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/GetMonthlyCalendarRequest.java new file mode 100644 index 0000000..385991b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/GetMonthlyCalendarRequest.java @@ -0,0 +1,20 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "월별 캘린더 조회 요청") +public record GetMonthlyCalendarRequest( + @Schema(description = "연도", example = "2026") + @NotNull(message = "연도는 필수입니다") + Integer year, + + @Schema(description = "월 (1~12)", example = "1") + @NotNull(message = "월은 필수입니다") + @Min(value = 1, message = "월은 1~12 사이의 값이어야 합니다") + @Max(value = 12, message = "월은 1~12 사이의 값이어야 합니다") + Integer month +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/SearchAnswerEntryRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/SearchAnswerEntryRequest.java index 702149b..cae270c 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/SearchAnswerEntryRequest.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/SearchAnswerEntryRequest.java @@ -9,7 +9,7 @@ public record SearchAnswerEntryRequest( @Size(min = 1, max = 100, message = "검색 키워드는 1~100자입니다") String keyword, - @Schema(description = "감정 코드 (JOY, PLEASURE, SADNESS, ANGER, REGRET, FRUSTRATION, GROWTH, ETC)", example = "JOY") + @Schema(description = "감정 코드 (ACHIEVEMENT, INTEREST, PEACE, PLEASURE, WILL, DEPRESSION, REGRET, ETC)", example = "ACHIEVEMENT") String emotionCode, @Schema(description = "다음 페이지 커서 (형식: date)", example = "2025-12-25") diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/AnswerEntrySummaryResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/AnswerEntrySummaryResponse.java index bbd895a..27bde26 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/AnswerEntrySummaryResponse.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/AnswerEntrySummaryResponse.java @@ -14,7 +14,7 @@ public record AnswerEntrySummaryResponse( @Schema(description = "관심분야 코드 (PREFERENCE, EMOTION, ROUTINE, RELATIONSHIP, LOVE, VALUES)", example = "EMOTION") String interestCode, - @Schema(description = "감정 코드 (리포트 생성 완료 시에만 제공, PENDING/FAILED 상태면 null)", example = "JOY") + @Schema(description = "감정 코드 (리포트 생성 완료 시에만 제공, PENDING/FAILED 상태면 null)", example = "ACHIEVEMENT") String emotionCode, @Schema(description = "질문 내용", example = "오늘 가장 기뻤던 순간은?") @@ -26,7 +26,10 @@ public record AnswerEntrySummaryResponse( @Schema(description = "답변 작성일", example = "2025-12-25") LocalDate answerDate ) { - public static AnswerEntrySummaryResponse toResponse(SearchAnswerEntryDto dto, String keyword) { + /** + * 검색 키워드가 있는 경우 (키워드 포함 문장 추출) + */ + public static AnswerEntrySummaryResponse from(SearchAnswerEntryDto dto, String keyword) { String snippet = MatchedSnippetExtractor.extract(dto.answerContent(), keyword); return new AnswerEntrySummaryResponse( @@ -38,4 +41,11 @@ public static AnswerEntrySummaryResponse toResponse(SearchAnswerEntryDto dto, St dto.answerDate() ); } + + /** + * 검색 키워드가 없는 경우 (첫 문장 추출) + */ + public static AnswerEntrySummaryResponse from(SearchAnswerEntryDto dto) { + return from(dto, null); + } } \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarEntryResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarEntryResponse.java new file mode 100644 index 0000000..4c3944a --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarEntryResponse.java @@ -0,0 +1,22 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import com.devkor.ifive.nadab.domain.dailyreport.core.dto.MonthlyCalendarDto; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "캘린더 날짜별 정보") +public record CalendarEntryResponse( + @Schema(description = "답변 날짜", example = "2026-01-30") + LocalDate date, + + @Schema(description = "감정 코드 (답변이 있고 리포트가 COMPLETED 상태일 때만 제공)", example = "ACHIEVEMENT") + String emotionCode +) { + public static CalendarEntryResponse from(MonthlyCalendarDto dto) { + return new CalendarEntryResponse( + dto.date(), + dto.emotionCode() != null ? dto.emotionCode().name() : null + ); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarRecentsResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarRecentsResponse.java new file mode 100644 index 0000000..23f456b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/CalendarRecentsResponse.java @@ -0,0 +1,15 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "캘린더 최근 답변 미리보기 응답") +public record CalendarRecentsResponse( + @Schema(description = "최근 답변 목록 (최대 6개, 날짜 내림차순)") + List items +) { + public static CalendarRecentsResponse from(List items) { + return new CalendarRecentsResponse(items); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/MonthlyCalendarResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/MonthlyCalendarResponse.java new file mode 100644 index 0000000..bf4d867 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/MonthlyCalendarResponse.java @@ -0,0 +1,12 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "월별 캘린더 응답") +public record MonthlyCalendarResponse( + @Schema(description = "답변이 있는 날짜의 정보 목록 (답변 없는 날짜는 포함되지 않음)") + List calendarEntries +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/AnswerQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/AnswerQueryService.java index 77e2144..06a2a66 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/AnswerQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/AnswerQueryService.java @@ -1,12 +1,21 @@ package com.devkor.ifive.nadab.domain.dailyreport.application; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.GetMonthlyCalendarRequest; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.SearchAnswerEntryRequest; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.AnswerEntrySummaryResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.CalendarEntryResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.CalendarRecentsResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.MonthlyCalendarResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.SearchAnswerEntryResponse; +import com.devkor.ifive.nadab.domain.dailyreport.core.dto.MonthlyCalendarDto; import com.devkor.ifive.nadab.domain.dailyreport.core.dto.SearchAnswerEntryDto; import com.devkor.ifive.nadab.domain.dailyreport.core.entity.EmotionCode; import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryQueryRepository; import com.devkor.ifive.nadab.domain.dailyreport.application.helper.CursorParser; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.NotFoundException; +import com.devkor.ifive.nadab.global.shared.util.MonthRangeCalculator; +import com.devkor.ifive.nadab.global.shared.util.dto.MonthRangeDto; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -64,7 +73,7 @@ public SearchAnswerEntryResponse searchAnswers(Long userId, SearchAnswerEntryReq // DTO 변환 List responseItems = items.stream() - .map(dto -> AnswerEntrySummaryResponse.toResponse(dto, request.keyword())) + .map(dto -> AnswerEntrySummaryResponse.from(dto, request.keyword())) .toList(); // nextCursor 생성 @@ -77,6 +86,50 @@ public SearchAnswerEntryResponse searchAnswers(Long userId, SearchAnswerEntryReq return new SearchAnswerEntryResponse(responseItems, nextCursor, hasNext); } + public MonthlyCalendarResponse getMonthlyCalendar(Long userId, GetMonthlyCalendarRequest request) { + // 월의 시작/종료 날짜 계산 + LocalDate anyDayInMonth = LocalDate.of(request.year(), request.month(), 1); + MonthRangeDto range = MonthRangeCalculator.monthRangeOf(anyDayInMonth); + + // 월별 데이터 조회 + List results = answerEntryQueryRepository.findCalendarEntriesInMonth( + userId, + range.monthStartDate(), + range.monthEndDate() + ); + + // DTO 변환 + List calendarEntries = results.stream() + .map(CalendarEntryResponse::from) + .toList(); + + return new MonthlyCalendarResponse(calendarEntries); + } + + public CalendarRecentsResponse getRecentAnswers(Long userId) { + // 최근 6개만 조회 (페이지 번호 0, 크기 6) + Pageable pageable = PageRequest.of(0, 6); + + List results = answerEntryQueryRepository.findRecentAnswers( + userId, + pageable + ); + + // DTO 변환 + List items = results.stream() + .map(AnswerEntrySummaryResponse::from) + .toList(); + + return CalendarRecentsResponse.from(items); + } + + public AnswerEntrySummaryResponse getAnswerByDate(Long userId, LocalDate date) { + SearchAnswerEntryDto dto = answerEntryQueryRepository.findByUserAndDate(userId, date) + .orElseThrow(() -> new NotFoundException(ErrorCode.ANSWER_NOT_FOUND)); + + return AnswerEntrySummaryResponse.from(dto); + } + /** * LIKE 검색용 키워드 준비 (이스케이핑 + % 추가) */ From 776d5e8210fd9aea3442f7d767f6a0bee94d7d6d Mon Sep 17 00:00:00 2001 From: Chanhae Lee Date: Tue, 13 Jan 2026 15:34:44 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(global):=20PathVariable=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=98=A4=EB=A5=98=EA=B0=80=20=EC=A0=81=EC=A0=88?= =?UTF-8?q?=ED=9E=88=20=EC=B2=98=EB=A6=AC=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PathVariable 타입 변환 실패 시 발생하는 MethodArgumentTypeMismatchException를 처리하여 기존 500 에러 대신 400 VALIDATION_FAILED 반환되도록 개선 --- .../nadab/global/exception/ExceptionController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/devkor/ifive/nadab/global/exception/ExceptionController.java b/src/main/java/com/devkor/ifive/nadab/global/exception/ExceptionController.java index e50e715..d052928 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/exception/ExceptionController.java +++ b/src/main/java/com/devkor/ifive/nadab/global/exception/ExceptionController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import java.util.stream.Collectors; @@ -56,6 +57,16 @@ public ResponseEntity> handleMethodArgumentNotValidExc return ApiResponseEntity.error(ErrorCode.VALIDATION_FAILED, validationErrors); } + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { + log.warn("MethodArgumentTypeMismatchException: {}", ex.getMessage(), ex); + + String parameterName = ex.getName(); + String errorMessage = String.format("%s 형식이 올바르지 않습니다. 올바른 형식으로 입력해주세요.", parameterName); + + return ApiResponseEntity.error(ErrorCode.VALIDATION_FAILED, errorMessage); + } + @ExceptionHandler(UnauthorizedException.class) public ResponseEntity> handleUnauthorizedException(UnauthorizedException ex) { log.warn("UnauthorizedException: {}", ex.getMessage(), ex);