Repository navigation
feat(report): 월간 리포트 V2 도입 및 통합 조회 확장 #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
d932287
feat(report): monthly_report_v2 테이블 생성 및 엔티티 구현
1Seob 2a70ab3
feat(report): MonthlyReportV2ContentFactory 클래스 구현
1Seob 4ef6123
feat(report): PENDING 월간 리포트 생성 로직 수정
1Seob 8553422
feat(report): 월간 리포트 V2 텍스트 생성/저장 흐름 및 감정 통계 연동
1Seob 1ca7145
feat(report): 월간 리포트 V2 이미지 생성/저장 파이프라인 추가
1Seob 432142d
feat(report): 월간 리포트 v2에 관심사 통계(interest_stats) 집계·저장 기능 추가
1Seob 31db947
feat(report): 월간 리포트 V2 조회 기능 추가
1Seob 32f259d
feat(report): 이전 리포트 조회 구현
1Seob 2f33459
feat(report): 월간 리포트 V2 id 조회에 본인 소유 검증 및 접근 거부 에러코드 추가
1Seob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
261 changes: 261 additions & 0 deletions
261
src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| 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.MonthlyReportResponseV2; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.MonthlyReportStartResponse; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response.ReportListTypeV2; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.application.MonthlyReportQueryServiceV2; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.application.MonthlyReportService; | ||
| import com.devkor.ifive.nadab.domain.weeklyreport.api.dto.response.CompletedCountResponse; | ||
| import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; | ||
| import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; | ||
| import com.devkor.ifive.nadab.global.security.principal.UserPrincipal; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Tag(name = "월간 리포트 API V2", description = "월간 리포트 생성 및 조회 관련 API V2") | ||
| @RestController | ||
| @RequestMapping("/api/v2/monthly-report") | ||
| @RequiredArgsConstructor | ||
| public class MonthlyReportControllerV2 { | ||
|
|
||
| private final MonthlyReportService monthlyReportService; | ||
| private final MonthlyReportQueryServiceV2 monthlyReportQueryServiceV2; | ||
|
|
||
| @PostMapping("/start") | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "월간 리포트 생성 시작", | ||
| description = """ | ||
| 사용자의 (지난 달에 대한) 월간 리포트 생성을 시작합니다. </br> | ||
| 비동기로 처리되기 때문에, id로 월간 리포트 조회 API를 폴링하여 상태를 확인할 수 있습니다. | ||
| """, | ||
| security = @SecurityRequirement(name = "bearerAuth"), | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "월간 리포트 생성 시작 성공", | ||
| content = @Content(schema = @Schema(implementation = MonthlyReportStartResponse.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "400", | ||
| description = """ | ||
| - ErrorCode: MONTHLY_REPORT_NOT_ENOUGH_REPORTS - 월간 리포트 작성 자격 미달 **(이 경우 data의 completedCount 필드에 지난 주에 작성된 오늘의 리포트 수가 포함됩니다.)** | ||
| - ErrorCode: WALLET_INSUFFICIENT_BALANCE - 크리스탈 잔액 부족 | ||
| """, | ||
| content = @Content(schema = @Schema(implementation = CompletedCountResponse.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "인증 실패", | ||
| content = @Content | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = """ | ||
| - ErrorCode: USER_NOT_FOUND - 사용자를 찾을 수 없음 | ||
| - ErrorCode: WALLET_NOT_FOUND - 지갑을 찾을 수 없음 | ||
| """, | ||
| content = @Content | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "409", | ||
| description = """ | ||
| - ErrorCode: MONTHLY_REPORT_ALREADY_COMPLETED - 이미 작성된 월간 리포트가 존재함 | ||
| - ErrorCode: MONTHLY_REPORT_IN_PROGRESS - 현재 월간 리포트를 생성 중임 | ||
| """, | ||
| content = @Content | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<MonthlyReportStartResponse>> startMonthlyReport( | ||
| @AuthenticationPrincipal UserPrincipal principal | ||
| ) { | ||
| MonthlyReportStartResponse response = monthlyReportService.startMonthlyReport(principal.getId()); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @GetMapping("/all") | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "이전 리포트 목록 조회", | ||
| description = """ | ||
| 주간/월간 이전 리포트를 통합 조회합니다. | ||
|
|
||
| type: ALL | MONTHLY | WEEKLY | ||
|
|
||
| 정렬 순서: | ||
| 1) 년-월 내림차순 | ||
| 2) 동일 월에서는 월간 먼저, 그 다음 주간(주차 내림차순) | ||
|
|
||
| version 규칙: | ||
| - weekly: 값과 상관없이 GET /api/v1/weekly-report/{id} 로 조회 | ||
| - 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 = AllReportItemResponseV2.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "인증 실패", | ||
| content = @Content | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = "- ErrorCode: USER_NOT_FOUND - 사용자를 찾을 수 없음", | ||
| content = @Content | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<List<AllReportItemResponseV2>>> getAllReports( | ||
| @AuthenticationPrincipal UserPrincipal principal, | ||
| @RequestParam(defaultValue = "ALL") ReportListTypeV2 type | ||
| ) { | ||
| List<AllReportItemResponseV2> response = monthlyReportQueryServiceV2.getAllReports(principal.getId(), type); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @GetMapping | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "나의 월간 리포트 조회 V2", | ||
| description = """ | ||
| 사용자의 (지난 달에 대한) 월간 리포트 V2와 이전 월간 리포트 V2를 조회합니다. </br> | ||
| **<```report```의 state>** </br> | ||
| 생성 대기 중인 경우 ```status = "PENDING"``` 으로 반환됩니다. </br> | ||
| 생성 진행 중인 경우 ```status = "IN_PROGRESS"``` 로 반환됩니다. </br> | ||
| 생성에 성공한 경우 ```status = "COMPLETED"``` 로 반환됩니다. </br> | ||
| 생성에 실패한 경우 ```status = "FAILED"``` 로 반환됩니다. 이때 크리스탈이 환불되기 때문에 잔액 조회를 해야합니다. | ||
|
|
||
| **<텍스트 스타일(styled) 지원>** </br> | ||
| 월간 리포트 본문은 강조 표현을 위해 해당 필드에 구조화된 형태로 함께 제공됩니다. </br> | ||
| 각 필드는 ```segments``` 배열을 가지며, </br> | ||
| 각 segment는 ```text```와 ```marks```를 포함합니다. </br> | ||
| ```marks```에는 ```BOLD```, ```HIGHLIGHT```만 포함될 수 있습니다. </br> | ||
| 클라이언트는 ```segments```를 순서대로 이어 붙여 렌더링하고, ```marks```에 따라 볼드/하이라이트를 적용하면 됩니다. </br> | ||
|
|
||
| 다음은 각 페이지에서 활용되는 필드의 값에 대한 설명입니다. </br> | ||
| comparisonType: 최초 생성인지 이전 리포트가 존재하는지 여부입니다. </br> | ||
| 현재는 모두 최초 생성이기 때문에 "BASELINE"으로 고정되어 있고, 이전 리포트가 존재하는 경우에는 "COMPARISON"으로 반환될 예정입니다. </br> | ||
|
|
||
| **<페이지 1>** </br> | ||
| summary : 월간 기록 요약 </br> | ||
| imageUrl : AI 생성 이미지 </br> | ||
| discovered.segments : 월간 분석 텍스트 </br> | ||
|
|
||
| **<페이지 2>** </br> | ||
| dominantKeyword : 이번 달 요약 단어 </br> | ||
| emotionStats.emotions : 감정에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br> | ||
| emotionSummaryContent.styledText.segments : 감정 분석 텍스트 </br> | ||
|
|
||
| emotionTrend : "NOT_SUPPORTED" (현재는 고정. 변동 양상은 최초 생성 월간 리포트에서는 지원되지 않음. 이후 업데이트 예정) </br> | ||
|
|
||
| **<페이지 3>** </br> | ||
| commentSummary : 나답의 한 마디 요약 </br> | ||
| comment.segments : 나답의 한 마디 텍스트 </br> | ||
| interestStats.interests : 카테고리(관심사)에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br> | ||
| """, | ||
| security = @SecurityRequirement(name = "bearerAuth"), | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "나의 월간 리포트 조회 성공", | ||
| content = @Content(schema = @Schema(implementation = MonthlyReportResponseV2.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "인증 실패", | ||
| content = @Content | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = """ | ||
| - ErrorCode: USER_NOT_FOUND - 사용자를 찾을 수 없음 | ||
| """, | ||
| content = @Content | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<MonthlyReportResponseV2>> getMyMonthlyReport( | ||
| @AuthenticationPrincipal UserPrincipal principal | ||
| ) { | ||
| MonthlyReportResponseV2 response = monthlyReportQueryServiceV2.getMyMonthlyReport(principal.getId()); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @GetMapping("/{id}") | ||
| @PreAuthorize("isAuthenticated()") | ||
| @Operation( | ||
| summary = "id로 월간 리포트 조회", | ||
| description = """ | ||
| 월간 리포트 V2를 id로 조회합니다. </br> | ||
| 생성 대기 중인 경우 ```status = "PENDING"``` 으로 반환됩니다. </br> | ||
| 생성 진행 중인 경우 ```status = "IN_PROGRESS"``` 로 반환됩니다. </br> | ||
| 생성에 성공한 경우 ```status = "COMPLETED"``` 로 반환됩니다. </br> | ||
| 생성에 실패한 경우 ```status = "FAILED"``` 로 반환됩니다. 이때 크리스탈이 환불되기 때문에 잔액 조회를 해야합니다. | ||
|
|
||
| **<텍스트 스타일(styled) 지원>** </br> | ||
| 월간 리포트 본문은 강조 표현을 위해 해당 필드에 구조화된 형태로 함께 제공됩니다. </br> | ||
| 각 필드는 ```segments``` 배열을 가지며, </br> | ||
| 각 segment는 ```text```와 ```marks```를 포함합니다. </br> | ||
| ```marks```에는 ```BOLD```, ```HIGHLIGHT```만 포함될 수 있습니다. </br> | ||
| 클라이언트는 ```segments```를 순서대로 이어 붙여 렌더링하고, ```marks```에 따라 볼드/하이라이트를 적용하면 됩니다. </br> | ||
|
|
||
| 다음은 각 페이지에서 활용되는 필드의 값에 대한 설명입니다. </br> | ||
| comparisonType: 최초 생성인지 이전 리포트가 존재하는지 여부입니다. </br> | ||
| 현재는 모두 최초 생성이기 때문에 "BASELINE"으로 고정되어 있고, 이전 리포트가 존재하는 경우에는 "COMPARISON"으로 반환될 예정입니다. </br> | ||
|
|
||
| **<페이지 1>** </br> | ||
| summary : 월간 기록 요약 </br> | ||
| imageUrl : AI 생성 이미지 </br> | ||
| discovered.segments : 월간 분석 텍스트 </br> | ||
|
|
||
| **<페이지 2>** </br> | ||
| dominantKeyword : 이번 달 요약 단어 </br> | ||
| emotionStats.emotions : 감정에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br> | ||
| emotionSummaryContent.styledText.segments : 감정 분석 텍스트 </br> | ||
|
|
||
| emotionTrend : "NOT_SUPPORTED" (현재는 고정. 변동 양상은 최초 생성 월간 리포트에서는 지원되지 않음. 이후 업데이트 예정) </br> | ||
|
|
||
| **<페이지 3>** </br> | ||
| commentSummary : 나답의 한 마디 요약 </br> | ||
| comment.segments : 나답의 한 마디 텍스트 </br> | ||
| interestStats.interests : 카테고리(관심사)에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br> | ||
| """, | ||
| security = @SecurityRequirement(name = "bearerAuth"), | ||
| responses = { | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "월간 리포트 조회 성공", | ||
| content = @Content(schema = @Schema(implementation = MonthlyReportResponseV2.class), mediaType = "application/json") | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = "- ErrorCode: MONTHLY_REPORT_NOT_FOUND - 월간 리포트를 찾을 수 없음", | ||
| content = @Content | ||
| ) | ||
| } | ||
| ) | ||
| public ResponseEntity<ApiResponseDto<MonthlyReportResponseV2>> getMonthlyReportById( | ||
| @PathVariable Long id | ||
| ) { | ||
| MonthlyReportResponseV2 response = monthlyReportQueryServiceV2.getMonthlyReportById(id); | ||
| return ApiResponseEntity.ok(response); | ||
| } | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
...com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/AllReportItemResponseV2.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "전체 리포트 목록 아이템") | ||
| public record AllReportItemResponseV2( | ||
| @Schema(description = "리포트 ID") | ||
| Long id, | ||
| @Schema(description = "리포트 타입", example = "MONTHLY") | ||
| String type, | ||
| @Schema(description = "기간 문자열", example = "1월 4주차") | ||
| String period, | ||
| @Schema(description = "요약") | ||
| String summary, | ||
| @Schema(description = "리포트 버전", example = "2") | ||
| int version | ||
| ) { | ||
| } |
53 changes: 53 additions & 0 deletions
53
...com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MonthlyReportResponseV2.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response; | ||
|
|
||
| import com.devkor.ifive.nadab.domain.monthlyreport.core.content.InterestStatsContent; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportComparisonType; | ||
| import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportStatus; | ||
| import com.devkor.ifive.nadab.domain.typereport.core.content.TypeEmotionStatsContent; | ||
| import com.devkor.ifive.nadab.domain.typereport.core.content.TypeTextContent; | ||
| import com.devkor.ifive.nadab.global.shared.reportcontent.StyledText; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "월간 리포트 V2 조회 응답") | ||
| public record MonthlyReportResponseV2( | ||
|
|
||
| @Schema(description = "리포트 대상 월") | ||
| int month, | ||
|
|
||
| @Schema(description = "리포트 상태", example = "PENDING") | ||
| MonthlyReportStatus status, | ||
|
|
||
| @Schema(description = "비교 타입", example = "BASELINE") | ||
| MonthlyReportComparisonType comparisonType, | ||
|
|
||
| @Schema(description = "요약") | ||
| String summary, | ||
|
|
||
| @Schema(description = "이미지 URL", nullable = true) | ||
| String imageUrl, | ||
|
|
||
| @Schema(description = "발견한 점(styled)") | ||
| StyledText discovered, | ||
|
|
||
| @Schema(description = "핵심 키워드") | ||
| String dominantKeyword, | ||
|
|
||
| @Schema(description = "감정 통계") | ||
| TypeEmotionStatsContent emotionStats, | ||
|
|
||
| @Schema(description = "감정 요약(styled)") | ||
| TypeTextContent emotionSummaryContent, | ||
|
|
||
| @Schema(description = "감정 흐름 요약") | ||
| String emotionTrend, | ||
|
|
||
| @Schema(description = "코멘트(styled)") | ||
| StyledText comment, | ||
|
|
||
| @Schema(description = "코멘트 요약") | ||
| String commentSummary, | ||
|
|
||
| @Schema(description = "관심사 통계") | ||
| InterestStatsContent interestStats | ||
| ) { | ||
| } |
7 changes: 7 additions & 0 deletions
7
...n/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/ReportListTypeV2.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response; | ||
|
|
||
| public enum ReportListTypeV2 { | ||
| ALL, | ||
| MONTHLY, | ||
| WEEKLY | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.