Skip to content

Commit 9f8720e

Browse files
authored
feat(report): 월간 리포트 V2 비교·소셜·이미지 다양화 및 조회 계약 확장 (#186)
* feat(report): 월간 리포트 V2 비교 대상 확정 로직 추가 현재 월 이전의 가장 최근 COMPLETED V2 리포트 조회 및 previousReportId 이벤트 전달, BASELINE·COMPARISON 분기 단위 테스트 추가 * feat(report): 월간 리포트 V2 비교 입력 조립 직전 리포트의 기간·콘텐츠·감정 통계 수집, 긍정 감정 비율 차이와 주요 감정 변화 계산 및 LLM 입력 전달 * feat(report): 월간 리포트 V2 비교 프롬프트 분기 추가 COMPARISON 프롬프트 로딩과 비교 입력 JSON 치환, 감정 비교 응답 검증 및 dev·prod 배포 환경 변수 설정 추가 * feat(report): 월간 리포트 V2에 감정 비교 결과 저장 및 조회 응답 수정 emotion_comparison JSONB 스냅샷 저장과 조회 응답 확장, LLM segment 정규화 및 V2 길이 검증 에러 코드 세분화 * feat(report): 월간 리포트 V2에 소셜 요약 저장 모델 추가 monthly_reports_v2에 social_summary JSONB 컬럼을 추가하고 기존 리포트의 비노출 기본값을 backfill함 * feat(report): 월간 소셜 반응 집계 쿼리 추가 사용자의 DailyReport에 받은 좋아요와 댓글·대댓글을 월별 친구 단위로 집계함. ACCEPTED 친구 관계를 기준으로 비친구, 탈퇴 사용자, 양방향 차단 관계와 삭제 댓글은 제외함 * feat(report): 월간 소셜 노출 및 랭킹 계산 추가 좋아요와 댓글 합계 및 참여 친구 조건으로 소셜 요약 노출 여부를 계산함. 반응 횟수 내림차순과 한국어 가나다순으로 최대 3명을 선정하고 displayOrder와 topRank를 분리해 공동 1위 강조를 지원함 * feat(report): 월간 소셜 요약 생성 파이프라인 연결 KST 월 범위의 소셜 반응을 집계해 MonthlySocialSummaryContent를 생성하고 confirmMonthlyText에서 기존 분석 결과와 함께 원자적으로 저장함. 소셜 집계 실패 시 리포트 실패 및 크리스탈 환불 흐름을 적용함 * feat(report): 월간 소셜 요약 조회 응답 추가 MonthlyReportResponseV2에 소셜 페이지 노출 정보와 좋아요·댓글 랭킹을 추가함 * refactor(report): 월간 리포트 locator 응답 계약 확장 V1/V2 공통 MonthlyReportLocatorResponse를 도입하고 id를 reportId로 명확화함. 나의 월간 리포트 조회 응답을 report와 previousReport 두 슬롯으로 확장해 버전별 상세 API 라우팅 기반을 마련함 * feat(report): 월간 리포트 현재·이전 locator 조회 추가 지난달은 상태와 무관하게, 지지난달은 COMPLETED 상태만 V2 우선·V1 fallback으로 탐색하는 resolver를 추가함 * fix(report): 월간 리포트 V1 조회 보안과 locator 계약 보완 V1 상세 조회에 인증 사용자 기반 소유권 검증과 403 응답을 추가함. locator API의 null·상태·version별 상세 경로를 문서화하고, 이전 월을 현재 범위에서 파생해 연도 경계를 안정적으로 처리함 * feat(report): 월간 리포트 V2 이미지 화풍 프리셋 선택 기능 추가 이미지 화풍 프리셋과 저장 컬럼을 추가하고, 최근 완료 리포트의 화풍을 제외해 결정적으로 프리셋을 선택·저장하도록 구현. 재시도 시 동일한 화풍을 유지하도록 보장하고 Repository, 선택기, 할당 서비스 테스트 추가. * feat(ai): 월간 이미지 프롬프트 조립기 추가 리포트 내용과 기간, 화풍 프리셋을 MonthlyImagePromptContext로 정규화하고 공통 안전 제약과 8개 화풍별 시각 지침을 합성하는 MonthlyImagePromptComposer 추가. 빈 입력 처리와 프리셋별 프롬프트 분기를 단위 테스트로 검증 * feat(ai): 이미지 클라이언트에 프롬프트 context 연동 OpenAiImageClient가 MonthlyImagePromptContext를 받아 composer로 프롬프트를 조립하도록 확장하고 기존 HTTP 요청 경로를 재사용 * feat(report): 월간 이미지 화풍 생성 파이프라인 연결 MonthlyReportGenerationListenerV2에서 프리셋을 할당하고 MonthlyImagePromptContext를 구성해 context 기반 이미지 생성을 호출하도록 전환 * refactor(ai): 월간 이미지 프롬프트 중복 경로 제거 MonthlyReportPromptLoader의 고정 이미지 템플릿과 OpenAiImageClient의 구형 DTO 기반 진입점을 제거하고, 이미지 프롬프트 생성을 MonthlyImagePromptComposer로 단일화 * feat(report): 월간 이미지 색상 팔레트 선택 기반 추가 8종의 색상 팔레트와 저장 컬럼을 추가하고 최근 완료 리포트의 팔레트를 제외하는 결정적 선택 로직 구현. 화풍과 팔레트를 함께 할당하며 기존 부분 저장 상태에서는 누락된 값만 보완하도록 확장 * feat(ai): 월간 이미지 색상 팔레트 생성 파이프라인 연결 저장된 화풍과 색상 팔레트를 MonthlyImagePromptContext로 전달하고 팔레트별 배경·주조색·강조색 지침을 이미지 프롬프트에 합성. 공통 색감 편향과 기본 파스텔 회귀를 제한하고 listener를 통합 visual preset 계약으로 전환 * test(ai): 월간 이미지 조합 및 팔레트 검증 강화 8개 화풍과 8개 팔레트의 전체 64개 프롬프트 조합이 서로 구분되는지 검증하고, 팔레트별 핵심 색상 표현이 유지되는지 테스트 추가 * refactor(report): 불필요한 월간 리포트 current 조회 제거 GET /api/v2/monthly-report/current와 전용 응답 DTO·query service 메서드·테스트를 제거하고 월간 리포트 locator 조회 경로를 기본 API로 단일화 * chore(global): 개인 Codex 스킬 추적 제거
1 parent f2f3261 commit 9f8720e

66 files changed

Lines changed: 3610 additions & 162 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-to-dev-ec2.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ jobs:
8686
MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
8787
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
8888
export MONTHLY_V2_BASELINE_PROMPT
89+
90+
MONTHLY_V2_COMPARISON_PROMPT_B64="${{ secrets.MONTHLY_V2_COMPARISON_PROMPT_B64 }}"
91+
MONTHLY_V2_COMPARISON_PROMPT="$(printf '%s' "$MONTHLY_V2_COMPARISON_PROMPT_B64" | base64 -d | tr -d '\r')"
92+
export MONTHLY_V2_COMPARISON_PROMPT
8993
9094
EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
9195
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"

.github/workflows/deploy-to-prod-ec2.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ jobs:
8686
MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
8787
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
8888
export MONTHLY_V2_BASELINE_PROMPT
89+
90+
MONTHLY_V2_COMPARISON_PROMPT_B64="${{ secrets.MONTHLY_V2_COMPARISON_PROMPT_B64 }}"
91+
MONTHLY_V2_COMPARISON_PROMPT="$(printf '%s' "$MONTHLY_V2_COMPARISON_PROMPT_B64" | base64 -d | tr -d '\r')"
92+
export MONTHLY_V2_COMPARISON_PROMPT
8993
9094
EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
9195
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportController.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportResponse>> getMyMonthlyRepor
166166
description = "월간 리포트 조회 성공",
167167
content = @Content(schema = @Schema(implementation = MonthlyReportResponse.class), mediaType = "application/json")
168168
),
169+
@ApiResponse(
170+
responseCode = "403",
171+
description = "- ErrorCode: MONTHLY_REPORT_ACCESS_FORBIDDEN - 본인 월간 리포트만 조회 가능",
172+
content = @Content
173+
),
169174
@ApiResponse(
170175
responseCode = "404",
171176
description = "- ErrorCode: MONTHLY_REPORT_NOT_FOUND - 월간 리포트를 찾을 수 없음",
@@ -174,9 +179,10 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportResponse>> getMyMonthlyRepor
174179
}
175180
)
176181
public ResponseEntity<ApiResponseDto<MonthlyReportResponse>> getMonthlyReportById(
182+
@AuthenticationPrincipal UserPrincipal principal,
177183
@PathVariable Long id
178184
) {
179-
MonthlyReportResponse response = monthlyReportQueryService.getMonthlyReportById(id);
185+
MonthlyReportResponse response = monthlyReportQueryService.getMonthlyReportById(principal.getId(), id);
180186
return ApiResponseEntity.ok(response);
181187
}
182188
}

src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/MonthlyReportControllerV2.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,18 @@ public ResponseEntity<ApiResponseDto<List<AllReportItemResponseV2>>> getAllRepor
137137
@Operation(
138138
summary = "나의 월간 리포트 조회 V2",
139139
description = """
140-
사용자의 (지난 달에 대한) 월간 리포트를 조회합니다. </br>
140+
사용자의 지난달·지지난달 월간 리포트 위치 정보를 조회합니다. </br>
141141
142-
report: 존재하지 않으면 null, 존재하면 id와 version을 반환합니다. </br>
142+
report: 지난달 리포트이며 존재하지 않으면 null입니다. </br>
143+
previousReport: 지지난달 완료 리포트이며 존재하지 않으면 null입니다. </br>
144+
두 슬롯은 서로 독립적으로 null일 수 있습니다. </br>
145+
각 항목은 reportId, version, month, status를 반환합니다. </br>
146+
report.status: PENDING | IN_PROGRESS | TEXT_COMPLETED | COMPLETED | FAILED </br>
147+
previousReport.status: 존재하는 경우 항상 COMPLETED </br>
143148
144149
version 규칙:
145-
- monthly_reports - 1인 경우 : GET /api/v1/monthly-report/{id}로 조회 (기존의 레거시 버전)
146-
- monthly_reports - 2인 경우 : GET /api/v2/monthly-report/{id}로 조회 (새로운 V2 버전)
150+
- version 1인 경우 : GET /api/v1/monthly-report/{reportId}로 조회 (기존의 레거시 버전)
151+
- version 2인 경우 : GET /api/v2/monthly-report/{reportId}로 조회 (새로운 V2 버전)
147152
""",
148153
security = @SecurityRequirement(name = "bearerAuth"),
149154
responses = {
@@ -194,7 +199,7 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportLookupResponseV2>> getMyMont
194199
195200
다음은 각 페이지에서 활용되는 필드의 값에 대한 설명입니다. </br>
196201
comparisonType: 최초 생성인지 이전 리포트가 존재하는지 여부입니다. </br>
197-
현재는 모두 최초 생성이기 때문에 "BASELINE"으로 고정되어 있고, 이전 리포트가 존재하는 경우에는 "COMPARISON"으로 반환될 예정입니다. </br>
202+
이전에 완료된 V2 월간 리포트가 없으면 "BASELINE", 있으면 "COMPARISON"으로 반환됩니다. </br>
198203
199204
**<페이지 1>** </br>
200205
summary : 월간 기록 요약 </br>
@@ -203,15 +208,31 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportLookupResponseV2>> getMyMont
203208
204209
**<페이지 2>** </br>
205210
dominantKeyword : 이번 달 요약 단어 </br>
211+
emotionTrend : BASELINE은 "NOT_SUPPORTED", COMPARISON은 주요 감정의 변동 양상 </br>
206212
emotionStats.emotions : 감정에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br>
213+
emotionComparison : 직전 월간 리포트 감정 비교 스냅샷이며 BASELINE인 경우 null입니다. </br>
214+
- emotionComparison.previousReportId : 비교 대상인 직전 완료 리포트 ID </br>
215+
- emotionComparison.previousMonth : 비교 대상 월 </br>
216+
- emotionComparison.previousEmotionStats : 직전 리포트의 감정 통계 </br>
217+
- emotionComparison.positivePercentPointChange : 현재와 직전 리포트의 긍정 감정 비율 차이(%p) </br>
218+
207219
emotionSummaryContent.styledText.segments : 감정 분석 텍스트 </br>
208-
209-
emotionTrend : "NOT_SUPPORTED" (현재는 고정. 변동 양상은 최초 생성 월간 리포트에서는 지원되지 않음. 이후 업데이트 예정) </br>
210220
211221
**<페이지 3>** </br>
212222
commentSummary : 나답의 한 마디 요약 </br>
213223
comment.segments : 나답의 한 마디 텍스트 </br>
214224
interestStats.interests : 카테고리(관심사)에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br>
225+
226+
**<페이지 4>** </br>
227+
socialSummary.visible : 월간 소셜 페이지 노출 여부 </br>
228+
socialSummary.month : 집계 대상 월 </br>
229+
socialSummary.likeRanking : 내 DailyReport에 좋아요를 많이 누른 친구 최대 3명 </br>
230+
socialSummary.commentRanking : 내 DailyReport에 댓글·대댓글을 많이 작성한 친구 최대 3명 </br>
231+
- displayOrder : 화면 표시 순서(1~3) </br>
232+
- userId : 친구 사용자 ID </br>
233+
- nickname : 친구 닉네임 </br>
234+
- profileImageUrl : 친구 프로필 이미지 URL </br>
235+
- topRank : 공동 1위를 포함한 1위 강조 여부 </br>
215236
""",
216237
security = @SecurityRequirement(name = "bearerAuth"),
217238
responses = {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;
2+
3+
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
@Schema(description = "버전별 월간 리포트 상세 조회를 위한 위치 정보")
7+
public record MonthlyReportLocatorResponse(
8+
@Schema(description = "월간 리포트 ID")
9+
Long reportId,
10+
11+
@Schema(description = "월간 리포트 버전", allowableValues = {"1", "2"}, example = "2")
12+
int version,
13+
14+
@Schema(description = "월간 리포트 대상 월", example = "5")
15+
int month,
16+
17+
@Schema(description = "월간 리포트 상태", example = "COMPLETED")
18+
MonthlyReportStatus status
19+
) {
20+
}

src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MonthlyReportResponseV2.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;
22

33
import com.devkor.ifive.nadab.domain.monthlyreport.core.content.InterestStatsContent;
4+
import com.devkor.ifive.nadab.domain.monthlyreport.core.content.MonthlyEmotionComparisonContent;
45
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportComparisonType;
56
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportStatus;
67
import com.devkor.ifive.nadab.domain.typereport.core.content.TypeEmotionStatsContent;
@@ -32,22 +33,28 @@ public record MonthlyReportResponseV2(
3233
@Schema(description = "핵심 키워드")
3334
String dominantKeyword,
3435

36+
@Schema(description = "감정 흐름 요약")
37+
String emotionTrend,
38+
3539
@Schema(description = "감정 통계")
3640
TypeEmotionStatsContent emotionStats,
3741

42+
@Schema(description = "직전 월간 리포트 감정 비교 스냅샷. BASELINE인 경우 null", nullable = true)
43+
MonthlyEmotionComparisonContent emotionComparison,
44+
3845
@Schema(description = "감정 요약(styled)")
3946
TypeTextContent emotionSummaryContent,
4047

41-
@Schema(description = "감정 흐름 요약")
42-
String emotionTrend,
43-
4448
@Schema(description = "코멘트(styled)")
4549
StyledText comment,
4650

4751
@Schema(description = "코멘트 요약")
4852
String commentSummary,
4953

5054
@Schema(description = "관심사 통계")
51-
InterestStatsContent interestStats
55+
InterestStatsContent interestStats,
56+
57+
@Schema(description = "월간 소셜 반응 요약")
58+
MonthlySocialSummaryResponse socialSummary
5259
) {
5360
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record MonthlySocialRankingItemResponse(
6+
@Schema(description = "화면 표시 순서", example = "1")
7+
int displayOrder,
8+
9+
@Schema(description = "친구 사용자 ID", example = "12")
10+
Long userId,
11+
12+
@Schema(description = "친구 닉네임", example = "가나다")
13+
String nickname,
14+
15+
@Schema(description = "친구 프로필 이미지 URL", nullable = true)
16+
String profileImageUrl,
17+
18+
@Schema(description = "공동 1위를 포함한 1위 강조 여부", example = "true")
19+
boolean topRank
20+
) {
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
import java.util.List;
6+
7+
public record MonthlySocialSummaryResponse(
8+
@Schema(description = "월간 소셜 페이지 노출 여부", example = "true")
9+
boolean visible,
10+
11+
@Schema(description = "집계 대상 월", example = "5")
12+
int month,
13+
14+
@Schema(description = "내 DailyReport에 좋아요를 많이 누른 친구 랭킹")
15+
List<MonthlySocialRankingItemResponse> likeRanking,
16+
17+
@Schema(description = "내 DailyReport에 댓글·대댓글을 많이 작성한 친구 랭킹")
18+
List<MonthlySocialRankingItemResponse> commentRanking
19+
) {
20+
}

src/main/java/com/devkor/ifive/nadab/domain/monthlyreport/api/dto/response/MyMonthlyReportLookupItemV2.java

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)