From b3b42f0db6aa0ad48997bf7cda5264b96b0a755b Mon Sep 17 00:00:00 2001 From: sb Date: Sun, 5 Jul 2026 12:56:14 +0900 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=EB=AA=A8=EB=8D=B8=EC=9D=B4=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EB=AC=B8=20=EC=84=9E=EC=96=B4=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=B4=EB=8F=84=20JSON=EB=A7=8C=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=E2=80=94=20=ED=8C=8C=EC=8B=B1=20=EA=B2=AC=EA=B3=A0?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 첫 '{'~마지막 '}' 구간만 추출해 파싱, 중괄호 없으면 조용히 규칙 폴백(WARN 대신 INFO) Co-Authored-By: Claude Opus 4.8 --- .../sb/web/account/service/AiEntryParser.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sb/web/account/service/AiEntryParser.java b/src/main/java/com/sb/web/account/service/AiEntryParser.java index 42fabe1..428cbd4 100644 --- a/src/main/java/com/sb/web/account/service/AiEntryParser.java +++ b/src/main/java/com/sb/web/account/service/AiEntryParser.java @@ -86,7 +86,13 @@ public class AiEntryParser { String out = stripFences(resp.path("content").path(0).path("text").asText("")).trim(); log.info("[ai-parse] 모델 원문='{}'", out); - JsonNode j = om.readTree(out); + // 모델이 설명문을 섞어 보내도 첫 '{'~마지막 '}' 만 추출해 JSON 파싱(견고성). + String json = extractJson(out); + if (json == null) { + log.info("[ai-parse] JSON 없음 — 거래 아님으로 간주, 규칙기반 폴백"); + return Optional.empty(); + } + JsonNode j = om.readTree(json); String type = "INCOME".equalsIgnoreCase(j.path("type").asText("EXPENSE")) ? "INCOME" : "EXPENSE"; LocalDate date; @@ -111,6 +117,14 @@ public class AiEntryParser { } } + /** 설명문이 앞뒤로 붙어도 첫 '{'~마지막 '}' 구간만 잘라 JSON 만 추출. 중괄호 없으면 null. */ + private String extractJson(String s) { + int a = s.indexOf('{'); + int b = s.lastIndexOf('}'); + if (a >= 0 && b > a) return s.substring(a, b + 1); + return null; + } + /** 모델이 ```json ... ``` 로 감쌌을 때 코드펜스 제거. */ private String stripFences(String s) { String t = s.trim();