집계 기반으로 현황 코멘트(bullets)에 더해 지출 패턴 맞춤 절약 제안(tips) 2~3개 생성. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,5 +12,6 @@ import java.util.List;
|
|||||||
@Builder
|
@Builder
|
||||||
public class AiCommentResponse {
|
public class AiCommentResponse {
|
||||||
|
|
||||||
private List<String> bullets;
|
private List<String> bullets; // 현황 관찰/코멘트
|
||||||
|
private List<String> tips; // 절약 가이드(실행 가능한 제안)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,11 +326,10 @@ public class AccountService {
|
|||||||
public com.sb.web.account.dto.AiCommentResponse aiComment(com.sb.web.account.dto.AiCommentRequest req) {
|
public com.sb.web.account.dto.AiCommentResponse aiComment(com.sb.web.account.dto.AiCommentRequest req) {
|
||||||
try {
|
try {
|
||||||
String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(req);
|
String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(req);
|
||||||
return com.sb.web.account.dto.AiCommentResponse.builder()
|
return aiEntryParser.financeComment(json);
|
||||||
.bullets(aiEntryParser.financeComment(json))
|
|
||||||
.build();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return com.sb.web.account.dto.AiCommentResponse.builder().bullets(java.util.List.of()).build();
|
return com.sb.web.account.dto.AiCommentResponse.builder()
|
||||||
|
.bullets(java.util.List.of()).tips(java.util.List.of()).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.sb.web.account.service;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.sb.web.account.dto.AiCommentResponse;
|
||||||
import com.sb.web.account.dto.ParsedEntryResponse;
|
import com.sb.web.account.dto.ParsedEntryResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -108,21 +109,23 @@ public class AiEntryParser {
|
|||||||
return callClaude(system, content, 400, today, categoryHints, walletHints);
|
return callClaude(system, content, 400, today, categoryHints, walletHints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 이번 달 집계(JSON) → 2~3개의 한국어 재무 코멘트. 미설정/실패 시 빈 리스트. */
|
/** 이번 달 집계(JSON) → 현황 코멘트 + 절약 가이드. 미설정/실패 시 빈 응답. */
|
||||||
public List<String> financeComment(String summaryJson) {
|
public AiCommentResponse financeComment(String summaryJson) {
|
||||||
if (!enabled() || summaryJson == null || summaryJson.isBlank()) return List.of();
|
if (!enabled() || summaryJson == null || summaryJson.isBlank()) return emptyComment();
|
||||||
try {
|
try {
|
||||||
String system = """
|
String system = """
|
||||||
너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고
|
너는 한국어 개인 가계부 코치다. 아래 사용자의 이번 달 집계(JSON, 금액 단위 원)를 보고 두 가지를 만든다.
|
||||||
도움이 되는 코멘트를 2~3개 만든다. 각 코멘트는 한 문장, 가능하면 구체적 숫자를 인용하고,
|
1) bullets: 현황 관찰 2~3개. 각 한 문장, 구체적 숫자 인용, 잔소리·과장 없이 담백하게.
|
||||||
잔소리·과장 없이 담백하게. 반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지):
|
(예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율)
|
||||||
{"bullets":["문장1","문장2"]}
|
2) tips: 이 사용자의 지출 패턴에 딱 맞춘 '실행 가능한' 절약 제안 2~3개. 두루뭉술한 원칙 금지,
|
||||||
관점 예: 전월 대비 지출 증감, 예산 대비 소비 속도, 특정 분류 과다, 수입 대비 지출 비율.
|
구체적 분류·습관을 지목(예: "카페 지출이 8만원인데 주 2회로 줄이면 약 3만원 절약"). 각 한 문장, 제안형.
|
||||||
데이터가 빈약하거나 코멘트할 게 없으면 bullets 를 빈 배열로.
|
반드시 아래 JSON '하나만' 출력한다(마크다운/설명 금지):
|
||||||
|
{"bullets":["관찰1","관찰2"],"tips":["절약제안1","절약제안2"]}
|
||||||
|
데이터가 빈약하면 해당 배열을 빈 배열로.
|
||||||
""";
|
""";
|
||||||
Map<String, Object> reqBody = Map.of(
|
Map<String, Object> reqBody = Map.of(
|
||||||
"model", model,
|
"model", model,
|
||||||
"max_tokens", 400,
|
"max_tokens", 600,
|
||||||
"system", system,
|
"system", system,
|
||||||
"messages", List.of(Map.of("role", "user", "content", summaryJson)));
|
"messages", List.of(Map.of("role", "user", "content", summaryJson)));
|
||||||
JsonNode resp = rest.post()
|
JsonNode resp = rest.post()
|
||||||
@@ -136,23 +139,34 @@ public class AiEntryParser {
|
|||||||
String out = stripFences(resp.path("content").path(0).path("text").asText("")).trim();
|
String out = stripFences(resp.path("content").path(0).path("text").asText("")).trim();
|
||||||
log.info("[ai-comment] 원문='{}'", out);
|
log.info("[ai-comment] 원문='{}'", out);
|
||||||
String json = extractJson(out);
|
String json = extractJson(out);
|
||||||
if (json == null) return List.of();
|
if (json == null) return emptyComment();
|
||||||
JsonNode arr = om.readTree(json).path("bullets");
|
JsonNode root = om.readTree(json);
|
||||||
List<String> bullets = new ArrayList<>();
|
List<String> bullets = strArray(root.path("bullets"));
|
||||||
if (arr.isArray()) {
|
List<String> tips = strArray(root.path("tips"));
|
||||||
for (JsonNode n : arr) {
|
log.info("[ai-comment] 결과 — 코멘트 {}개, 절약팁 {}개", bullets.size(), tips.size());
|
||||||
String s = n.asText("").trim();
|
return AiCommentResponse.builder().bullets(bullets).tips(tips).build();
|
||||||
if (!s.isEmpty()) bullets.add(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("[ai-comment] 결과 — {}개", bullets.size());
|
|
||||||
return bullets;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[ai-comment] 실패: {}", e.toString());
|
log.warn("[ai-comment] 실패: {}", e.toString());
|
||||||
return List.of();
|
return emptyComment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AiCommentResponse emptyComment() {
|
||||||
|
return AiCommentResponse.builder().bullets(List.of()).tips(List.of()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JsonNode 배열 → 공백 제거한 문자열 리스트. */
|
||||||
|
private List<String> strArray(JsonNode arr) {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
if (arr != null && arr.isArray()) {
|
||||||
|
for (JsonNode n : arr) {
|
||||||
|
String s = n.asText("").trim();
|
||||||
|
if (!s.isEmpty()) out.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** 분류·계좌 후보와 오늘 날짜를 시스템 프롬프트 템플릿에 주입. */
|
/** 분류·계좌 후보와 오늘 날짜를 시스템 프롬프트 템플릿에 주입. */
|
||||||
private String withHints(String body, LocalDate today, List<String> categoryHints, List<String> walletHints) {
|
private String withHints(String body, LocalDate today, List<String> categoryHints, List<String> walletHints) {
|
||||||
String catList = (categoryHints == null || categoryHints.isEmpty()) ? "(없음)" : String.join(", ", categoryHints);
|
String catList = (categoryHints == null || categoryHints.isEmpty()) ? "(없음)" : String.join(", ", categoryHints);
|
||||||
|
|||||||
Reference in New Issue
Block a user