From 3a960e331971036b490c1caaee8017c85c3f5c90 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 17 May 2026 16:53:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(report):=20=EC=9B=94=EA=B0=84=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20V2=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EC=9D=84=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EC=A0=95=EB=B3=B4(report)=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/MonthlyReportControllerV2.java | 44 ++++--------------- .../response/MyMonthlyReportLookupItemV2.java | 17 +++++++ .../MyMonthlyReportLookupResponseV2.java | 10 +++++ .../MonthlyReportQueryServiceV2.java | 32 ++++++++++++-- .../test/application/TestReportService.java | 5 +++ 5 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupItemV2.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupResponseV2.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java index 26ed041..32ea5a3 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java @@ -1,6 +1,7 @@ package com.devkor.ifive.nadab.domain.monthlyreport.api; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.AllReportItemResponseV2; +import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MyMonthlyReportLookupResponseV2; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportResponseV2; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportStartResponse; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.ReportListTypeV2; @@ -136,47 +137,20 @@ public ResponseEntity>> getAllRepor @Operation( summary = "나의 월간 리포트 조회 V2", description = """ - 사용자의 (지난 달에 대한) 월간 리포트 V2와 이전 월간 리포트 V2를 조회합니다.
- **<```report```의 state>**
- 생성 대기 중인 경우 ```status = "PENDING"``` 으로 반환됩니다.
- 생성 진행 중인 경우 ```status = "IN_PROGRESS"``` 로 반환됩니다.
- 생성에 성공한 경우 ```status = "COMPLETED"``` 로 반환됩니다.
- 생성에 실패한 경우 ```status = "FAILED"``` 로 반환됩니다. 이때 크리스탈이 환불되기 때문에 잔액 조회를 해야합니다. - - **<텍스트 스타일(styled) 지원>**
- 월간 리포트 본문은 강조 표현을 위해 해당 필드에 구조화된 형태로 함께 제공됩니다.
- 각 필드는 ```segments``` 배열을 가지며,
- 각 segment는 ```text```와 ```marks```를 포함합니다.
- ```marks```에는 ```BOLD```, ```HIGHLIGHT```만 포함될 수 있습니다.
- 클라이언트는 ```segments```를 순서대로 이어 붙여 렌더링하고, ```marks```에 따라 볼드/하이라이트를 적용하면 됩니다.
- - 다음은 각 페이지에서 활용되는 필드의 값에 대한 설명입니다.
- comparisonType: 최초 생성인지 이전 리포트가 존재하는지 여부입니다.
- 현재는 모두 최초 생성이기 때문에 "BASELINE"으로 고정되어 있고, 이전 리포트가 존재하는 경우에는 "COMPARISON"으로 반환될 예정입니다.
- - **<페이지 1>**
- summary : 월간 기록 요약
- imageUrl : AI 생성 이미지
- discovered.segments : 월간 분석 텍스트
+ 사용자의 (지난 달에 대한) 월간 리포트를 조회합니다.
- **<페이지 2>**
- dominantKeyword : 이번 달 요약 단어
- emotionStats.emotions : 감정에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다.
- emotionSummaryContent.styledText.segments : 감정 분석 텍스트
- - emotionTrend : "NOT_SUPPORTED" (현재는 고정. 변동 양상은 최초 생성 월간 리포트에서는 지원되지 않음. 이후 업데이트 예정)
+ report: 존재하지 않으면 null, 존재하면 id와 version을 반환합니다.
- **<페이지 3>**
- commentSummary : 나답의 한 마디 요약
- comment.segments : 나답의 한 마디 텍스트
- interestStats.interests : 카테고리(관심사)에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다.
+ version 규칙: + - monthly_reports - 1인 경우 : GET /api/v1/monthly-report/{id}로 조회 (기존의 레거시 버전) + - monthly_reports - 2인 경우 : GET /api/v2/monthly-report/{id}로 조회 (새로운 V2 버전) """, security = @SecurityRequirement(name = "bearerAuth"), responses = { @ApiResponse( responseCode = "200", description = "나의 월간 리포트 조회 성공", - content = @Content(schema = @Schema(implementation = MonthlyReportResponseV2.class), mediaType = "application/json") + content = @Content(schema = @Schema(implementation = MyMonthlyReportLookupResponseV2.class), mediaType = "application/json") ), @ApiResponse( responseCode = "401", @@ -192,10 +166,10 @@ public ResponseEntity>> getAllRepor ) } ) - public ResponseEntity> getMyMonthlyReport( + public ResponseEntity> getMyMonthlyReport( @AuthenticationPrincipal UserPrincipal principal ) { - MonthlyReportResponseV2 response = monthlyReportQueryServiceV2.getMyMonthlyReport(principal.getId()); + MyMonthlyReportLookupResponseV2 response = monthlyReportQueryServiceV2.getMyMonthlyReport(principal.getId()); return ApiResponseEntity.ok(response); } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupItemV2.java b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupItemV2.java new file mode 100644 index 0000000..f5df497 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupItemV2.java @@ -0,0 +1,17 @@ +package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response; + +import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "나의 월간 리포트 라우팅 정보") +public record MyMonthlyReportLookupItemV2( + @Schema(description = "리포트 ID") + Long id, + @Schema(description = "리포트 버전", example = "2") + int version, + @Schema(description = "리포트 대상 월") + int month, + @Schema(description = "리포트 상태", example = "COMPLETED") + MonthlyReportStatus status +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupResponseV2.java b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupResponseV2.java new file mode 100644 index 0000000..00328b0 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupResponseV2.java @@ -0,0 +1,10 @@ +package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "나의 월간 리포트 단건 조회 응답") +public record MyMonthlyReportLookupResponseV2( + @Schema(description = "월간 리포트 라우팅 정보. 리포트가 없으면 null", nullable = true) + MyMonthlyReportLookupItemV2 report +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/application/MonthlyReportQueryServiceV2.java b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/application/MonthlyReportQueryServiceV2.java index 1c2d61f..c2037c8 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/application/MonthlyReportQueryServiceV2.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/application/MonthlyReportQueryServiceV2.java @@ -1,6 +1,8 @@ package com.devkor.ifive.nadab.domain.monthlyreport.application; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.AllReportItemResponseV2; +import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MyMonthlyReportLookupItemV2; +import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MyMonthlyReportLookupResponseV2; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportResponseV2; import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.ReportListTypeV2; import com.devkor.ifive.nadab.domain.monthlyreport.core.content.MonthlyContentFactory; @@ -112,15 +114,17 @@ public List getAllReports(Long userId, ReportListTypeV2 .toList(); } - public MonthlyReportResponseV2 getMyMonthlyReport(Long userId) { + public MyMonthlyReportLookupResponseV2 getMyMonthlyReport(Long userId) { if (!userRepository.existsById(userId)) { throw new NotFoundException(ErrorCode.USER_NOT_FOUND); } MonthRangeDto range = MonthRangeCalculator.getLastMonthRange(); return monthlyReportV2Repository.findByUserIdAndMonthStartDate(userId, range.monthStartDate()) - .map(this::toResponse) - .orElse(null); + .map(this::toLookupResponse) + .or(() -> monthlyReportRepository.findByUserIdAndMonthStartDate(userId, range.monthStartDate()) + .map(this::toLookupResponse)) + .orElseGet(() -> new MyMonthlyReportLookupResponseV2(null)); } public MonthlyReportResponseV2 getMonthlyReportById(Long userId, Long id) { @@ -156,6 +160,28 @@ private MonthlyReportResponseV2 toResponse(MonthlyReportV2 report) { ); } + private MyMonthlyReportLookupResponseV2 toLookupResponse(MonthlyReportV2 report) { + return new MyMonthlyReportLookupResponseV2( + new MyMonthlyReportLookupItemV2( + report.getId(), + 2, + report.getMonthStartDate().getMonthValue(), + report.getStatus() == null ? MonthlyReportStatus.PENDING : report.getStatus() + ) + ); + } + + private MyMonthlyReportLookupResponseV2 toLookupResponse(MonthlyReport report) { + return new MyMonthlyReportLookupResponseV2( + new MyMonthlyReportLookupItemV2( + report.getId(), + 1, + report.getMonthStartDate().getMonthValue(), + report.getStatus() == null ? MonthlyReportStatus.PENDING : report.getStatus() + ) + ); + } + private record ReportListRow( Long id, String type, diff --git a/src/main/java/com/devkor/ifive/nadab/domain/test/application/TestReportService.java b/src/main/java/com/devkor/ifive/nadab/domain/test/application/TestReportService.java index 5444376..89413bb 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/test/application/TestReportService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/test/application/TestReportService.java @@ -14,6 +14,7 @@ import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode; import com.devkor.ifive.nadab.domain.user.core.entity.User; import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository; +import com.devkor.ifive.nadab.domain.user.core.service.ProfileImageService; import com.devkor.ifive.nadab.domain.wallet.core.entity.CrystalLog; import com.devkor.ifive.nadab.domain.wallet.core.entity.CrystalLogReason; import com.devkor.ifive.nadab.domain.wallet.core.entity.CrystalLogStatus; @@ -59,6 +60,8 @@ public class TestReportService { private final TestCrystalLogRepository testCrystalLogRepository; private final UserWalletRepository userWalletRepository; + private final ProfileImageService profileImageService; + private static final long WEEKLY_REPORT_COST = 20L; private static final long MONTHLY_REPORT_COST = 40L; private static final long TYPE_REPORT_COST = 100L; @@ -212,6 +215,8 @@ public void deleteThisMonthMonthlyReport(Long userId) { throw new BadRequestException(ErrorCode.MONTHLY_REPORT_NOT_COMPLETED); } + profileImageService.deleteProfileImage(report.getImageKey()); + CrystalLog purchaseLog = testCrystalLogRepository .findByUserIdAndRefTypeAndRefIdAndReasonAndStatus( userId,