선형 run-rate는 노트북 등 일회성 지출을 남은 날에 곱해 비현실적 값(2천만) 생성. AI가 분류를 보고 일회성을 일평균에서 제외해 추정 + 근거 문장 반환(경과일/총일수 전달). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ public class AiCommentRequest {
|
|||||||
private long prevExpense; // 직전 달 지출 합계(대비용)
|
private long prevExpense; // 직전 달 지출 합계(대비용)
|
||||||
private long budget; // 이번 달 예산 합계
|
private long budget; // 이번 달 예산 합계
|
||||||
private long spent; // 이번 달 예산 대비 소비액
|
private long spent; // 이번 달 예산 대비 소비액
|
||||||
|
private int elapsedDays; // 이번 달 경과일(현재 달일 때만 >0, 아니면 0)
|
||||||
|
private int totalDays; // 이번 달 총일수
|
||||||
private List<CategoryAmount> categories; // 분류별 지출 상위
|
private List<CategoryAmount> categories; // 분류별 지출 상위
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ public class AiCommentResponse {
|
|||||||
|
|
||||||
private List<String> bullets; // 현황 관찰/코멘트
|
private List<String> bullets; // 현황 관찰/코멘트
|
||||||
private List<String> tips; // 절약 가이드(실행 가능한 제안)
|
private List<String> tips; // 절약 가이드(실행 가능한 제안)
|
||||||
|
private Long forecast; // 월말 예상 지출(원) — 일회성 큰 지출 제외 추정. 없으면 null
|
||||||
|
private String forecastNote; // 예상 지출 근거 한 문장
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,14 +114,18 @@ public class AiEntryParser {
|
|||||||
if (!enabled() || summaryJson == null || summaryJson.isBlank()) return emptyComment();
|
if (!enabled() || summaryJson == null || summaryJson.isBlank()) return emptyComment();
|
||||||
try {
|
try {
|
||||||
String system = """
|
String system = """
|
||||||
너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고 두 가지를 만든다.
|
너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고 세 가지를 만든다.
|
||||||
1) bullets: 현황 관찰 2~3개. 각 한 문장, 구체적 숫자 인용, 잔소리·과장 없이 담백하게.
|
1) bullets: 현황 관찰 2~3개. 각 한 문장, 구체적 숫자 인용, 잔소리·과장 없이 담백하게.
|
||||||
(예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율)
|
(예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율)
|
||||||
2) tips: 이 사용자의 지출 패턴에 딱 맞춘 '실행 가능한' 절약 제안 2~3개. 두루뭉술한 원칙 금지,
|
2) tips: 이 사용자의 지출 패턴에 딱 맞춘 '실행 가능한' 절약 제안 2~3개. 두루뭉술한 원칙 금지,
|
||||||
구체적 분류·습관을 지목(예: "카페 지출이 8만원인데 주 2회로 줄이면 약 3만원 절약"). 각 한 문장, 제안형.
|
구체적 분류·습관을 지목(예: "카페 지출이 8만원인데 주 2회로 줄이면 약 3만원 절약"). 각 한 문장, 제안형.
|
||||||
|
3) forecast: 월말 예상 지출(원, 정수). 계산법 = '지금까지 실제 지출(expense)' + '남은 날 × 평소 하루 지출'.
|
||||||
|
단, 노트북·가전·여행 등 '일회성 큰 지출'로 보이는 분류는 남은 날에 반복되지 않으므로 하루 지출 산정에서 제외한다.
|
||||||
|
(평소 하루 지출 ≈ (expense − 일회성 큰 지출) ÷ elapsedDays). forecastNote 에 한 문장 근거(무엇을 일회성으로 봤는지).
|
||||||
|
elapsedDays 가 0 이면(과거/미래 달) forecast=0, forecastNote="".
|
||||||
반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지):
|
반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지):
|
||||||
{"bullets":["관찰1","관찰2"],"tips":["절약제안1","절약제안2"]}
|
{"bullets":["관찰1","관찰2"],"tips":["절약제안1","절약제안2"],"forecast":정수,"forecastNote":"근거 한 문장"}
|
||||||
데이터가 빈약하면 해당 배열을 빈 배열로.
|
데이터가 빈약하면 bullets/tips 는 빈 배열, forecast=0.
|
||||||
""";
|
""";
|
||||||
Map<String, Object> reqBody = Map.of(
|
Map<String, Object> reqBody = Map.of(
|
||||||
"model", model,
|
"model", model,
|
||||||
@@ -143,8 +147,14 @@ public class AiEntryParser {
|
|||||||
JsonNode root = om.readTree(json);
|
JsonNode root = om.readTree(json);
|
||||||
List<String> bullets = strArray(root.path("bullets"));
|
List<String> bullets = strArray(root.path("bullets"));
|
||||||
List<String> tips = strArray(root.path("tips"));
|
List<String> tips = strArray(root.path("tips"));
|
||||||
log.info("[ai-comment] 결과 — 코멘트 {}개, 절약팁 {}개", bullets.size(), tips.size());
|
long forecast = Math.max(0, root.path("forecast").asLong(0));
|
||||||
return AiCommentResponse.builder().bullets(bullets).tips(tips).build();
|
String forecastNote = root.path("forecastNote").asText("").trim();
|
||||||
|
log.info("[ai-comment] 결과 — 코멘트 {}개, 절약팁 {}개, 예상지출 {}", bullets.size(), tips.size(), forecast);
|
||||||
|
return AiCommentResponse.builder()
|
||||||
|
.bullets(bullets).tips(tips)
|
||||||
|
.forecast(forecast > 0 ? forecast : null)
|
||||||
|
.forecastNote(forecast > 0 ? forecastNote : null)
|
||||||
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[ai-comment] 실패: {}", e.toString());
|
log.warn("[ai-comment] 실패: {}", e.toString());
|
||||||
return emptyComment();
|
return emptyComment();
|
||||||
|
|||||||
Reference in New Issue
Block a user