Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8b83738
feat(report): 월간 리포트 V2 비교 대상 확정 로직 추가
1Seob Jun 18, 2026
863696b
feat(report): 월간 리포트 V2 비교 입력 조립
1Seob Jun 18, 2026
c116050
feat(report): 월간 리포트 V2 비교 프롬프트 분기 추가
1Seob Jun 18, 2026
cfe9f24
feat(report): 월간 리포트 V2에 감정 비교 결과 저장 및 조회 응답 수정
1Seob Jun 18, 2026
0a1c682
feat(report): 월간 리포트 V2에 소셜 요약 저장 모델 추가
1Seob Jun 28, 2026
592dfef
feat(report): 월간 소셜 반응 집계 쿼리 추가
1Seob Jun 28, 2026
185c930
feat(report): 월간 소셜 노출 및 랭킹 계산 추가
1Seob Jun 28, 2026
50933fb
feat(report): 월간 소셜 요약 생성 파이프라인 연결
1Seob Jun 28, 2026
7bc6e70
feat(report): 월간 소셜 요약 조회 응답 추가
1Seob Jun 28, 2026
988a643
refactor(report): 월간 리포트 locator 응답 계약 확장
1Seob Jun 28, 2026
da60974
feat(report): 월간 리포트 현재·이전 locator 조회 추가
1Seob Jun 28, 2026
fc2919d
fix(report): 월간 리포트 V1 조회 보안과 locator 계약 보완
1Seob Jun 28, 2026
da02b51
feat(report): 월간 리포트 V2 이미지 화풍 프리셋 선택 기능 추가
1Seob Jun 29, 2026
321de43
feat(ai): 월간 이미지 프롬프트 조립기 추가
1Seob Jun 29, 2026
ead4809
feat(ai): 이미지 클라이언트에 프롬프트 context 연동
1Seob Jun 29, 2026
c783ea1
feat(report): 월간 이미지 화풍 생성 파이프라인 연결
1Seob Jun 29, 2026
02e4adb
refactor(ai): 월간 이미지 프롬프트 중복 경로 제거
1Seob Jun 29, 2026
c162cbc
feat(report): 월간 이미지 색상 팔레트 선택 기반 추가
1Seob Jun 29, 2026
b35c925
feat(ai): 월간 이미지 색상 팔레트 생성 파이프라인 연결
1Seob Jun 29, 2026
0f81123
test(ai): 월간 이미지 조합 및 팔레트 검증 강화
1Seob Jun 29, 2026
b06dc49
refactor(report): 불필요한 월간 리포트 current 조회 제거
1Seob Jun 30, 2026
26a9b76
chore(global): 개인 Codex 스킬 추적 제거
1Seob Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/deploy-to-dev-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ jobs:
MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_BASELINE_PROMPT

MONTHLY_V2_COMPARISON_PROMPT_B64="${{ secrets.MONTHLY_V2_COMPARISON_PROMPT_B64 }}"
MONTHLY_V2_COMPARISON_PROMPT="$(printf '%s' "$MONTHLY_V2_COMPARISON_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_COMPARISON_PROMPT

EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/deploy-to-prod-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ jobs:
MONTHLY_V2_BASELINE_PROMPT_B64="${{ secrets.MONTHLY_V2_BASELINE_PROMPT_B64 }}"
MONTHLY_V2_BASELINE_PROMPT="$(printf '%s' "$MONTHLY_V2_BASELINE_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_BASELINE_PROMPT

MONTHLY_V2_COMPARISON_PROMPT_B64="${{ secrets.MONTHLY_V2_COMPARISON_PROMPT_B64 }}"
MONTHLY_V2_COMPARISON_PROMPT="$(printf '%s' "$MONTHLY_V2_COMPARISON_PROMPT_B64" | base64 -d | tr -d '\r')"
export MONTHLY_V2_COMPARISON_PROMPT

EVIDENCE_CARD_PROMPT_B64="${{ secrets.EVIDENCE_CARD_PROMPT_B64 }}"
EVIDENCE_CARD_PROMPT="$(printf '%s' "$EVIDENCE_CARD_PROMPT_B64" | base64 -d | tr -d '\r')"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportResponse>> getMyMonthlyRepor
description = "월간 리포트 조회 성공",
content = @Content(schema = @Schema(implementation = MonthlyReportResponse.class), mediaType = "application/json")
),
@ApiResponse(
responseCode = "403",
description = "- ErrorCode: MONTHLY_REPORT_ACCESS_FORBIDDEN - 본인 월간 리포트만 조회 가능",
content = @Content
),
@ApiResponse(
responseCode = "404",
description = "- ErrorCode: MONTHLY_REPORT_NOT_FOUND - 월간 리포트를 찾을 수 없음",
Expand All @@ -174,9 +179,10 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportResponse>> getMyMonthlyRepor
}
)
public ResponseEntity<ApiResponseDto<MonthlyReportResponse>> getMonthlyReportById(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id
) {
MonthlyReportResponse response = monthlyReportQueryService.getMonthlyReportById(id);
MonthlyReportResponse response = monthlyReportQueryService.getMonthlyReportById(principal.getId(), id);
return ApiResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ public ResponseEntity<ApiResponseDto<List<AllReportItemResponseV2>>> getAllRepor
@Operation(
summary = "나의 월간 리포트 조회 V2",
description = """
사용자의 (지난 달에 대한) 월간 리포트를 조회합니다. </br>
사용자의 지난달·지지난달 월간 리포트 위치 정보를 조회합니다. </br>

report: 존재하지 않으면 null, 존재하면 id와 version을 반환합니다. </br>
report: 지난달 리포트이며 존재하지 않으면 null입니다. </br>
previousReport: 지지난달 완료 리포트이며 존재하지 않으면 null입니다. </br>
두 슬롯은 서로 독립적으로 null일 수 있습니다. </br>
각 항목은 reportId, version, month, status를 반환합니다. </br>
report.status: PENDING | IN_PROGRESS | TEXT_COMPLETED | COMPLETED | FAILED </br>
previousReport.status: 존재하는 경우 항상 COMPLETED </br>

version 규칙:
- monthly_reports - 1인 경우 : GET /api/v1/monthly-report/{id}로 조회 (기존의 레거시 버전)
- monthly_reports - 2인 경우 : GET /api/v2/monthly-report/{id}로 조회 (새로운 V2 버전)
- version 1인 경우 : GET /api/v1/monthly-report/{reportId}로 조회 (기존의 레거시 버전)
- version 2인 경우 : GET /api/v2/monthly-report/{reportId}로 조회 (새로운 V2 버전)
""",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
Expand Down Expand Up @@ -194,7 +199,7 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportLookupResponseV2>> getMyMont

다음은 각 페이지에서 활용되는 필드의 값에 대한 설명입니다. </br>
comparisonType: 최초 생성인지 이전 리포트가 존재하는지 여부입니다. </br>
현재는 모두 최초 생성이기 때문에 "BASELINE"으로 고정되어 있고, 이전 리포트가 존재하는 경우에는 "COMPARISON"으로 반환될 예정입니다. </br>
이전에 완료된 V2 월간 리포트가 없으면 "BASELINE", 있으면 "COMPARISON"으로 반환됩니다. </br>

**<페이지 1>** </br>
summary : 월간 기록 요약 </br>
Expand All @@ -203,15 +208,31 @@ public ResponseEntity<ApiResponseDto<MyMonthlyReportLookupResponseV2>> getMyMont

**<페이지 2>** </br>
dominantKeyword : 이번 달 요약 단어 </br>
emotionTrend : BASELINE은 "NOT_SUPPORTED", COMPARISON은 주요 감정의 변동 양상 </br>
emotionStats.emotions : 감정에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br>
emotionComparison : 직전 월간 리포트 감정 비교 스냅샷이며 BASELINE인 경우 null입니다. </br>
- emotionComparison.previousReportId : 비교 대상인 직전 완료 리포트 ID </br>
- emotionComparison.previousMonth : 비교 대상 월 </br>
- emotionComparison.previousEmotionStats : 직전 리포트의 감정 통계 </br>
- emotionComparison.positivePercentPointChange : 현재와 직전 리포트의 긍정 감정 비율 차이(%p) </br>

emotionSummaryContent.styledText.segments : 감정 분석 텍스트 </br>

emotionTrend : "NOT_SUPPORTED" (현재는 고정. 변동 양상은 최초 생성 월간 리포트에서는 지원되지 않음. 이후 업데이트 예정) </br>

**<페이지 3>** </br>
commentSummary : 나답의 한 마디 요약 </br>
comment.segments : 나답의 한 마디 텍스트 </br>
interestStats.interests : 카테고리(관심사)에 대한 통계가 빈도 기준 내림차순으로 정렬되어 있습니다. </br>

**<페이지 4>** </br>
socialSummary.visible : 월간 소셜 페이지 노출 여부 </br>
socialSummary.month : 집계 대상 월 </br>
socialSummary.likeRanking : 내 DailyReport에 좋아요를 많이 누른 친구 최대 3명 </br>
socialSummary.commentRanking : 내 DailyReport에 댓글·대댓글을 많이 작성한 친구 최대 3명 </br>
- displayOrder : 화면 표시 순서(1~3) </br>
- userId : 친구 사용자 ID </br>
- nickname : 친구 닉네임 </br>
- profileImageUrl : 친구 프로필 이미지 URL </br>
- topRank : 공동 1위를 포함한 1위 강조 여부 </br>
""",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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 MonthlyReportLocatorResponse(
@Schema(description = "월간 리포트 ID")
Long reportId,

@Schema(description = "월간 리포트 버전", allowableValues = {"1", "2"}, example = "2")
int version,

@Schema(description = "월간 리포트 대상 월", example = "5")
int month,

@Schema(description = "월간 리포트 상태", example = "COMPLETED")
MonthlyReportStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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.content.MonthlyEmotionComparisonContent;
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;
Expand Down Expand Up @@ -32,22 +33,28 @@ public record MonthlyReportResponseV2(
@Schema(description = "핵심 키워드")
String dominantKeyword,

@Schema(description = "감정 흐름 요약")
String emotionTrend,

@Schema(description = "감정 통계")
TypeEmotionStatsContent emotionStats,

@Schema(description = "직전 월간 리포트 감정 비교 스냅샷. BASELINE인 경우 null", nullable = true)
MonthlyEmotionComparisonContent emotionComparison,

@Schema(description = "감정 요약(styled)")
TypeTextContent emotionSummaryContent,

@Schema(description = "감정 흐름 요약")
String emotionTrend,

@Schema(description = "코멘트(styled)")
StyledText comment,

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

@Schema(description = "관심사 통계")
InterestStatsContent interestStats
InterestStatsContent interestStats,

@Schema(description = "월간 소셜 반응 요약")
MonthlySocialSummaryResponse socialSummary
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record MonthlySocialRankingItemResponse(
@Schema(description = "화면 표시 순서", example = "1")
int displayOrder,

@Schema(description = "친구 사용자 ID", example = "12")
Long userId,

@Schema(description = "친구 닉네임", example = "가나다")
String nickname,

@Schema(description = "친구 프로필 이미지 URL", nullable = true)
String profileImageUrl,

@Schema(description = "공동 1위를 포함한 1위 강조 여부", example = "true")
boolean topRank
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.devkor.ifive.nadab.domain.monthlyreport.api.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

public record MonthlySocialSummaryResponse(
@Schema(description = "월간 소셜 페이지 노출 여부", example = "true")
boolean visible,

@Schema(description = "집계 대상 월", example = "5")
int month,

@Schema(description = "내 DailyReport에 좋아요를 많이 누른 친구 랭킹")
List<MonthlySocialRankingItemResponse> likeRanking,

@Schema(description = "내 DailyReport에 댓글·대댓글을 많이 작성한 친구 랭킹")
List<MonthlySocialRankingItemResponse> commentRanking
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "나의 월간 리포트 단건 조회 응답")
@Schema(description = "나의 현재·이전 월간 리포트 위치 조회 응답")
public record MyMonthlyReportLookupResponseV2(
@Schema(description = "월간 리포트 라우팅 정보. 리포트가 없으면 null", nullable = true)
MyMonthlyReportLookupItemV2 report
@Schema(description = "지난달 월간 리포트 위치 정보. 리포트가 없으면 null", nullable = true)
MonthlyReportLocatorResponse report,

@Schema(description = "지지난달 완료 월간 리포트 위치 정보. 리포트가 없으면 null", nullable = true)
MonthlyReportLocatorResponse previousReport
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.devkor.ifive.nadab.domain.monthlyreport.application;

import com.devkor.ifive.nadab.domain.monthlyreport.application.helper.MonthlyImageColorPaletteSelector;
import com.devkor.ifive.nadab.domain.monthlyreport.application.helper.MonthlyImagePresetSelector;
import com.devkor.ifive.nadab.domain.monthlyreport.core.dto.MonthlyImageVisualPreset;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyImageColorPalette;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyImageStylePreset;
import com.devkor.ifive.nadab.domain.monthlyreport.core.entity.MonthlyReportV2;
import com.devkor.ifive.nadab.domain.monthlyreport.core.repository.MonthlyReportV2Repository;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.ForbiddenException;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
public class MonthlyImagePresetAssignmentService {

private static final int RECENT_PRESET_LIMIT = 3;

private final MonthlyReportV2Repository monthlyReportV2Repository;

public MonthlyImageVisualPreset getOrAssignVisualPreset(Long userId, Long reportId) {
MonthlyReportV2 report = monthlyReportV2Repository.findById(reportId)
.orElseThrow(() -> new NotFoundException(ErrorCode.MONTHLY_REPORT_NOT_FOUND));
if (report.getUser() == null || report.getUser().getId() == null
|| !report.getUser().getId().equals(userId)) {
throw new ForbiddenException(ErrorCode.MONTHLY_REPORT_ACCESS_FORBIDDEN);
}
MonthlyImageStylePreset stylePreset = getOrAssignStylePreset(userId, reportId, report);
MonthlyImageColorPalette colorPalette = getOrAssignColorPalette(userId, reportId, report);
return new MonthlyImageVisualPreset(stylePreset, colorPalette);
}

private MonthlyImageStylePreset getOrAssignStylePreset(
Long userId,
Long reportId,
MonthlyReportV2 report
) {
if (report.getImagePromptVariant() == null) {
List<MonthlyImageStylePreset> recentPresets =
monthlyReportV2Repository.findRecentCompletedImagePromptVariants(
userId,
reportId,
PageRequest.of(0, RECENT_PRESET_LIMIT)
);
MonthlyImageStylePreset selected = MonthlyImagePresetSelector.select(
userId,
reportId,
report.getMonthStartDate(),
recentPresets
);
report.assignImagePromptVariant(selected);
return selected;
}
return report.getImagePromptVariant();
}

private MonthlyImageColorPalette getOrAssignColorPalette(
Long userId,
Long reportId,
MonthlyReportV2 report
) {
if (report.getImageColorPalette() == null) {
List<MonthlyImageColorPalette> recentPalettes =
monthlyReportV2Repository.findRecentCompletedImageColorPalettes(
userId,
reportId,
PageRequest.of(0, RECENT_PRESET_LIMIT)
);
MonthlyImageColorPalette selected = MonthlyImageColorPaletteSelector.select(
userId,
reportId,
report.getMonthStartDate(),
recentPalettes
);
report.assignImageColorPalette(selected);
return selected;
}
return report.getImageColorPalette();
}
}
Loading
Loading