revert(ai): 예상 지출을 AI에서 제거 — 비결정성으로 매 접속 값 변동
Deploy / deploy (push) Failing after 12m44s

AI 예상지출은 호출마다 40만~500만으로 흔들려 신뢰성 부족. 코멘트/팁만 AI 유지.
예상 지출은 프론트에서 결정적 계산으로 대체.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-05 15:43:44 +09:00
parent 2802ec9d5a
commit 7d15f06390
3 changed files with 5 additions and 19 deletions
@@ -17,8 +17,6 @@ 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,6 +14,4 @@ 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,18 +114,14 @@ 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"],"forecast":정수,"forecastNote":"근거 한 문장"} {"bullets":["관찰1","관찰2"],"tips":["절약제안1","절약제안2"]}
데이터가 빈약하면 bullets/tips 는 빈 배열, forecast=0. 데이터가 빈약하면 해당 배열을 빈 배열로.
"""; """;
Map<String, Object> reqBody = Map.of( Map<String, Object> reqBody = Map.of(
"model", model, "model", model,
@@ -147,14 +143,8 @@ 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"));
long forecast = Math.max(0, root.path("forecast").asLong(0)); log.info("[ai-comment] 결과 — 코멘트 {}개, 절약팁 {}개", bullets.size(), tips.size());
String forecastNote = root.path("forecastNote").asText("").trim(); return AiCommentResponse.builder().bullets(bullets).tips(tips).build();
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();