Feature/receipt image vision v2#3
Conversation
์์ฝWalkthrough์ด ํ ์์ฒญ์ ์ด๋ฏธ์ง ๋ถ์ ๊ธฐ๋ฅ์ ์ ๋๋ฆญ ์๋ต ๊ตฌ์กฐ๋ก ๋ฆฌํฉํ ๋งํฉ๋๋ค. ์๋ก์ด Changes
Sequence DiagramssequenceDiagram
participant Client
participant Controller
participant VisionService
participant ChatAPI
participant DocumentService
Client->>Controller: POST /receipt (image)
Controller->>VisionService: analyzeImage(ImageAnalysis, ReceiptData.class)
VisionService->>VisionService: validateImageAnalysis()
VisionService->>VisionService: createUserMessage(imageBytes, contentType)
VisionService->>ChatAPI: call chat API with image + prompt
ChatAPI-->>VisionService: response text + metadata
VisionService->>VisionService: cleanJsonResponse(text)
VisionService->>VisionService: convertToTargetType(text, ReceiptData.class)
VisionService->>VisionService: extractTokenUsage(metadata)
VisionService-->>Controller: ImageAnalysisResponseV2<ReceiptData>
Controller-->>Client: HTTP 200 + response
sequenceDiagram
participant DocumentService
participant VisionService
participant ImageAnalysis
Note over DocumentService,ImageAnalysis: ์ด์ : ์ง์ ํ์ฑ ๋ก์ง
DocumentService->>DocumentService: extractReceipt(MultipartFile)
DocumentService->>DocumentService: manual JSON parsing
DocumentService-->>DocumentService: ReceiptData
rect rgb(200, 220, 255)
Note over DocumentService,ImageAnalysis: ์ดํ: ์์ ํจํด
DocumentService->>ImageAnalysis: ImageAnalysis.of(prompt, file)
DocumentService->>VisionService: analyzeImage(imageAnalysis, ReceiptData.class)
VisionService-->>DocumentService: ImageAnalysisResponseV2<ReceiptData>
end
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes
Poem
Pre-merge checks and finishing touchesโ Passed checks (3 passed)
โจ Finishing touches
๐งช Generate unit tests (beta)
Tip ๐ Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests โ including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
๐งน Nitpick comments (7)
README.md (1)
1-1: README ๋ด์ฉ์ ์กฐ๊ธ๋ง ๋ณด์ํ๋ฉด ์จ๋ณด๋ฉ์ ๋์์ด ๋ ๊ฒ ๊ฐ์ต๋๋ค.ํ์ฌ ์ ์ฅ์ ๋ชฉ์ ๊ณผ ์ฌ์ฉ ๋ฐฉ๋ฒ ์ค๋ช ์ด ์์ด๋ ๋์์๋ ๋ฌธ์ ์์ง๋ง, ๋์ค์ ์คํ ๋ฐฉ๋ฒ(๋น๋/์คํ ์ปค๋งจ๋)๊ณผ ์ฃผ์ API ์๋ํฌ์ธํธ ์ ๋๋ฅผ ๊ฐ๋จํ ์ ๋ฆฌํด ๋๋ฉด ํ์ ยท๋ณต์ต ์ ํจ์ฌ ํธํ ๊ฒ ๊ฐ์ต๋๋ค.
spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java (1)
93-97: /receipt ์๋ํฌ์ธํธ ์์ด์ด๋ง์ ์์ฐ์ค๋ฝ์ง๋ง ์์ธ ์ ๋ต์ ์ ๋ฆฌํ๋ฉด ์ข๊ฒ ์ต๋๋ค.
/api/vision/receipt๊ฐReceiptAnalysisService.processReceipt(image)๋ฅผ ๊ทธ๋๋ก ์์ํดImageAnalysisResponseV2<ReceiptData>๋ฅผ ๋ฐํํ๋ ๊ตฌ์กฐ๋ ๋ช ํํฉ๋๋ค. ๋ค๋ง ํ์ฌthrows IOException, NoSuchFieldException๋ฅผ ๊ทธ๋๋ก ๋ ธ์ถํ๋๋ฐ, ์ค์ ๊ตฌํ์์๋NoSuchFieldException์ ๋์ง์ง ์๊ณ ์์ต๋๋ค(ReceiptAnalysisService ๋ด๋ถ JSON ์ ์ ๋ฉ์๋๋ ์์ง ์ฌ์ฉ๋์ง ์์).
- ๋จ๊ธฐ์ ์ผ๋ก๋ ์ค์ ๋ก ๋์ง์ง ์๋ checked ์์ธ๋ฅผ ์๊ทธ๋์ฒ์์ ์ ๊ฑฐํ๊ฑฐ๋,
- ์กฐ๋ง๊ฐ
cleanJsonResponse๋ฑ๊ณผ ์ฐ๋ํดNoSuchFieldException์ ๋์ง ๊ณํ์ด๋ผ๋ฉด, ํด๋น ๋ก์ง์ ์๋น์ค์ ์ฐ๊ฒฐํ๊ณ ์ปจํธ๋กค๋ฌ/๊ธ๋ก๋ฒ ์์ธ ์ฒ๋ฆฌ๊ธฐ์์ 4xx/5xx ์๋ต ์ ๋ต์ ๋ช ํํ ํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค.๋์ ์์ฒด์๋ ๋ฌธ์ ๊ฐ ์์ผ๋, ์์ธ ์๊ทธ๋์ฒ์ ์ค์ ๊ตฌํ์ ์กฐ๊ธฐ์ ๋ง์ถฐ ๋๋ฉด ์ถํ ์ ์ง๋ณด์ ์ ํผ์ ์ ์ค์ผ ์ ์์ต๋๋ค.
spring-ai-app/src/main/java/org/sprain/ai/service/ReceiptAnalysisService.java (1)
49-88:cleanJsonResponse๋ ํ์ฌ ์ฌ์ฉ๋์ง ์์ผ๋ฏ๋ก, ์ ๋ต์ ์ ๋ฆฌํ๋ ํธ์ด ์ข๊ฒ ์ต๋๋ค.
cleanJsonResponse๋ ๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก/๋ฐฑํฑ ์ ๊ฑฐ, JSON ๋ฒ์ ์ถ์ถ ๋ฑ ๊ฝค ๊ณต๋ค์ธ ์ ์ ๋ก์ง์ธ๋ฐ, ํ์ฌ ์ด๋ ๊ณณ์์๋ ํธ์ถ๋์ง ์๊ณ ์์ต๋๋ค. ์ค๊ณ ์๋์ ๋ฐ๋ผ ๋ค์ ์ค ํ๋๋ฅผ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.
- Spring AI ๊ตฌ์กฐํ ์๋ต๋ง ์ฌ์ฉํ ๊ณํ์ด๋ผ๋ฉด:
cleanJsonResponse์ ๊ด๋ จ ์์ธ(NoSuchFieldException)๋ ์ ๊ฑฐํ์ฌ ์ฝ๋ ๋ฒ ์ด์ค๋ฅผ ๋จ์ํ๊ฒ ์ ์งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.- LLM์ด ๋ฐํํ "ํ ์คํธ JSON"์ ์ง์ ํ์ฑํ ๊ณํ์ด๋ผ๋ฉด:
processReceipt์์ ๋จผ์ ImageAnalysisResponseV2<String>ํํ๋ก ์๋ฌธ ํ ์คํธ๋ฅผ ๋ฐ์cleanJsonResponseโobjectMapper.readValue(..., ReceiptData.class)์์ผ๋ก ์ค์ ํ์ฑ์ ์ํํ๋๋ก ์ฐ๊ฒฐํ๋ ์ชฝ์ด ๋ช ํํฉ๋๋ค.- ์ด ๊ฒฝ์ฐ
NoSuchFieldException์ ๋์ง๋ ์์น์ ์ปจํธ๋กค๋ฌ/๊ธ๋ก๋ฒ ์์ธ ์ฒ๋ฆฌ์์์ HTTP ์๋ต ๋งคํ๋ ํจ๊ป ์ ์ํด ๋๋ฉด ์ข์ต๋๋ค.์ด๋ ์ชฝ์ด๋ ์ง๊ธ์ฒ๋ผ ๋ฏธ์ฌ์ฉ ์ํ๋ก ๋๊ธฐ๋ณด๋ค๋, ์ค์ ์ ๋ต์ ๋ง์ถฐ ์ ๊ฑฐํ๊ฑฐ๋ ์ฐ๊ฒฐํ๋ ๋ฐฉํฅ์ผ๋ก ์ ๋ฆฌํ๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
spring-ai-app/src/main/java/org/sprain/ai/service/VisionServiceV2.java (2)
41-90: analyzeImage ํ์ดํ๋ผ์ธ์ ๋ช ํํ์ง๋ง, ์์ธ ๋ํ ๋ฒ์๋ ์ขํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณผ ๋งํฉ๋๋ค์ ๋ ฅ ๊ฒ์ฆ โ ์ด๋ฏธ์ง/์ปจํ ์ธ ํ์ ์ถ์ถ โ UserMessage ์์ฑ โ Chat ํธ์ถ โ ํ ํฐ ์ฌ์ฉ๋/ํ๊ฒ ํ์ ๋ณํ โ ์๋ต ์์ฑ๊น์ง ๋จ๊ณ๊ฐ ์ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค.
๋ค๋ง
analyzeImage๊ฐcatch (Exception e)๋ก ๋ชจ๋ ์์ธ๋ฅผRuntimeException("์ด๋ฏธ์ง ๋ถ์ ์ค ์ค๋ฅ ๋ฐ์", e)ํ๋๋ก ๋ํํ๊ณ ์์ด,
- ์ ๋ ฅ ๊ฒ์ฆ ์คํจ(IllegalArgumentException),
- JSON ํ์ฑ ์คํจ,
- ChatClient/๋คํธ์ํฌ ์ค๋ฅ
๋ฅผ ์์ ๋ ์ด์ด์์ ๊ตฌ๋ถํ๊ธฐ ์ด๋ ต์ต๋๋ค.๊ฐ๋ฅํ๋ค๋ฉด,
- ๊ฒ์ฆ ์คํจ/ํธ์ถ์ ์๋ชป ๋ฑ์ ๊ทธ๋๋ก IllegalArgumentException ์ ์ ํํ๊ณ ,
- ์ธ๋ถ ์์คํ /๋ชจ๋ธ ์ค๋ฅ๋ง ๋ณ๋ ๋๋ฉ์ธ ์์ธ(์:
ImageAnalysisException)๋ก ๊ฐ์ธ๋ ์์ผ๋ก ์๋ฌ ํ์ ์ ๋ถ๋ฆฌํ๋ฉด ๋๋ฒ๊น ๊ณผ ์๋ฌ ์๋ต ์ค๊ณ๊ฐ ๋ ์์ํด์ง ๊ฒ ๊ฐ์ต๋๋ค.
275-307: cleanJsonResponse ๋{...}์ค๋ธ์ ํธ ๋ฃจํธ๋ง ๊ฐ์ ํ๊ณ ์์ด ํฌ๋งท ๋ณํ์ ์ทจ์ฝํ ์ ์์ต๋๋คํ์ฌ ๊ตฌํ์,
- ์ฝ๋๋ธ๋ก/๋ฐฑํฑ ์ ๊ฑฐ ํ,
- ์ฒซ
{์ ๋ง์ง๋ง}์ฌ์ด๋ง ์๋ผ JSON ์ผ๋ก ๊ฐ์ฃผํ๋ ๋ฐฉ์์ ๋๋ค.์ด ๋ฐฉ์์ ๋ชจ๋ธ์ด ํญ์ ๋จ์ผ ๊ฐ์ฒด(JSON object)๋ง ๋ฐํํ๋ค๋ ์ ์ ๋ฅผ ๊น๊ณ ์์ด์,
- ๋ฃจํธ๊ฐ ๋ฐฐ์ด(
[...])์ด๊ฑฐ๋,- ๊ฐ์ฒด ์ฌ๋ฌ ๊ฐ๊ฐ ๋ถ์ด ๋์ค๋ ๊ฒฝ์ฐ,
- ์๋ต ์๋ค์ ์ค๋ช ํ ์คํธ๊ฐ ์๋ JSON์ด ๋ ๋ถ๋ ๊ฒฝ์ฐ
์ ์ฝ๊ฒ ์คํจํ ์ ์์ต๋๋ค.๋ชจ๋ธ ํ๋กฌํํธ๋ก ํญ์ ๊ฐ์ฒด ํ ๊ฐ๋ง ๋ฐํํ๋๋ก ๊ฐํ๊ฒ ์ ํํ๊ณ ์๋ค๋ฉด ๊ทธ๋๋ก๋ ์ถฉ๋ถํ์ง๋ง, ํฌ๋งท ๋ณ๊ฒฝ ๊ฐ๋ฅ์ฑ์ ๋๋นํ๋ค๋ฉด:
- ๋ฃจํธ๊ฐ ๋ฐฐ์ด์ธ ๊ฒฝ์ฐ๋ ํ์ฉํ๋๋ก
[/]์ฒ๋ฆฌ ๋ถ๊ธฐ ์ถ๊ฐ,- ๋๋ Jackson ์ ์คํธ๋ฆฌ๋ฐ ํ์๋ก ์ฒซ ์ ํจ JSON ํ ํฐ์ ํ์ฑํ๋ ๋ฐฉ์ ๋ฑ์ ๊ณ ๋ คํด ๋ณผ ์ ์์ต๋๋ค.
spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java (2)
198-214: promptWithResponseFormat ์ Class ๋น๊ต๋ simpleName ๋ฌธ์์ด ๋์ Class ๋น๊ต๋ฅผ ์ฌ์ฉํ๋ ํธ์ด ๋ ์์ ํฉ๋๋คํ์ฌ ๋ถ๊ธฐ ๋ก์ง์ด:
String responseTypeClassName = responseType.getSimpleName(); if ("ChatResponse".equals(responseTypeClassName)) ...์ ๊ฐ์ด simpleName ๋ฌธ์์ด์ ์์กดํ๊ณ ์์ด,
๋์ผ simpleName ์ ๊ฐ์ง ๋ค๋ฅธ ํ์ ์ด ์๊ธฐ๊ฑฐ๋ ํด๋์ค๋ช ์ด ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ์ค๋์ํ ์ฌ์ง๊ฐ ์์ต๋๋ค.๋ณด๋ค ๋ช ํํ๊ฒ:
- @SuppressWarnings("unchecked") - private <T> T promptWithResponseFormat(Message userMessage, Class<T> responseType) { - String responseTypeClassName = responseType.getSimpleName(); - if ("ChatResponse".equals(responseTypeClassName)) return (T) promptByChatResponse(userMessage); - else return chatClient.prompt() - .messages(userMessage) - .call() - .entity(responseType); - } + @SuppressWarnings("unchecked") + private <T> T promptWithResponseFormat(Message userMessage, Class<T> responseType) { + if (ChatResponse.class.equals(responseType)) { + return (T) promptByChatResponse(userMessage); + } + return chatClient.prompt() + .messages(userMessage) + .call() + .entity(responseType); + }์ฒ๋ผ
ChatResponse.class.equals(responseType)๋ก ๋น๊ตํ๋ฉด ํ์ ๋ฆฌํฉํฐ๋ง์๋ ๋ ๊ฒฌ๊ณ ํฉ๋๋ค.
150-168: compareImages ์์ DEFAULT_CONTENT_TYPE ์์๋ฅผ ์ฌ์ฌ์ฉํ๋ฉด ์ค๋ณต์ ์ค์ผ ์ ์์ต๋๋ค
compareImages์์ MIME ํ์ ๊ธฐ๋ณธ๊ฐ์"image/jpeg"๋ฌธ์์ด๋ก ๋ ๋ฒ ์ง์ ์ฌ์ฉํ๊ณ ์๋๋ฐ, ํด๋์ค ์๋จ์ ์ด๋ฏธDEFAULT_CONTENT_TYPE = "image/jpeg"์์๊ฐ ์ ์๋์ด ์์ต๋๋ค.๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ์ํด ๋ค์์ฒ๋ผ ์์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค:
- Media media1 = new Media( - MimeTypeUtils.parseMimeType( - image1.getContentType() != null ? image1.getContentType() : "image/jpeg" - ), - new ByteArrayResource(bytes1) - ); + Media media1 = new Media( + MimeTypeUtils.parseMimeType( + image1.getContentType() != null ? image1.getContentType() : DEFAULT_CONTENT_TYPE + ), + new ByteArrayResource(bytes1) + ); ... - Media media2 = new Media( - MimeTypeUtils.parseMimeType( - image2.getContentType() != null ? image2.getContentType() : "image/jpeg" - ), - new ByteArrayResource(bytes2) - ); + Media media2 = new Media( + MimeTypeUtils.parseMimeType( + image2.getContentType() != null ? image2.getContentType() : DEFAULT_CONTENT_TYPE + ), + new ByteArrayResource(bytes2) + );๋์ค์ ๊ธฐ๋ณธ MIME ํ์ ์ ๋ณ๊ฒฝํด์ผ ํ ๋ ์์ ํ ๊ณณ๋ง ์์ ํ๋ฉด ๋์ด ๊ด๋ฆฌ๊ฐ ๋ ์ฌ์์ง๋๋ค.
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (8)
README.md(1 hunks)spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java(5 hunks)spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysis.java(1 hunks)spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysisResponseV2.java(1 hunks)spring-ai-app/src/main/java/org/sprain/ai/service/DocumentProcessingService.java(5 hunks)spring-ai-app/src/main/java/org/sprain/ai/service/ReceiptAnalysisService.java(1 hunks)spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java(4 hunks)spring-ai-app/src/main/java/org/sprain/ai/service/VisionServiceV2.java(1 hunks)
๐งฐ Additional context used
๐งฌ Code graph analysis (2)
spring-ai-app/src/main/java/org/sprain/ai/service/ReceiptAnalysisService.java (2)
spring-ai-app/src/main/java/org/sprain/ai/service/DocumentProcessingService.java (1)
Service(13-69)spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java (1)
Service(23-215)
spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java (1)
spring-ai-app/src/main/java/org/sprain/ai/controller/ChatController.java (1)
RestController(15-101)
๐ Additional comments (4)
spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysisResponseV2.java (1)
1-24: ์ ๋ค๋ฆญ ์๋ต DTO ์ค๊ณ๊ฐ ๋จ์ํ๊ณ ์ฌ์ฌ์ฉํ๊ธฐ ์ข์ ๋ณด์ ๋๋ค.ํ๋ ๊ตฌ์ฑ๊ณผ
of(...)์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๋ก ํธ์ถ๋ถ ๊ฐ๋ ์ฑ์ด ์ข์์ก๊ณ , ํ ํฐ ์ฌ์ฉ๋๊น์ง ํ ๋ฒ์ ๋ฌถ์ด ๋ฐํํ๋ ํจํด๋ ์ผ๊ด์ฑ ์๊ฒ ๋ณด์ ๋๋ค. ํ์ฌ ํํ ๊ทธ๋๋ก ์ฌ์ฉํด๋ ์ถฉ๋ถํ ๊ด์ฐฎ์ ๋ณด์ ๋๋ค.spring-ai-app/src/main/java/org/sprain/ai/service/ReceiptAnalysisService.java (1)
22-47: ์์์ฆ ์ฒ๋ฆฌ ํ๋ก์ฐ ์์ฒด๋ ๋จ์ยท๋ช ํํฉ๋๋ค.
processReceipt์์ ํ๋กฌํํธ๋ฅผ ๊ตฌ์ฑํ ๋คVisionService.analyzeImage(ImageAnalysis.of(prompt, imageFile), ReceiptData.class)๋ก ๋ฐ๋ก ์์ํ๋ ๊ตฌ์กฐ๋ ์ผ๊ด๋๊ณ ์ดํดํ๊ธฐ ์ฝ์ต๋๋ค. Spring AI์entity(ReceiptData.class)๊ตฌ์กฐํ ์๋ต์ ๊ทธ๋๋ก ํ์ฉํ๊ฒ ๋ค๋ ๋ฐฉํฅ์ด๋ฉด ํ์ฌ ๊ตฌํ๋ง์ผ๋ก๋ ์ถฉ๋ถํ ๋์ ๊ฐ๋ฅํด ๋ณด์ ๋๋ค.spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysis.java (1)
1-29: ์ด๋ฏธ์ง ์ ๋ ฅ์ DTO๋ก ์บก์ํํ ์ค๊ณ๊ฐ ๊น๋ํฉ๋๋ค.
MultipartFileโ(prompt, imageBytes, contentType)๋ก ์ ๊ทํํ๋ ์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๋ค์ ํตํด VisionServiceยท๊ฐ์ข ๋ถ์ ์๋น์ค๊ฐ ๋์ผํ DTO๋ฅผ ๋ค๋ฃจ๋๋ก ํ ์ ์ด ์ข์ต๋๋ค.getBytes()์ฌ์ฉ ํน์ฑ์ ์์ ๋ ์ด์ด(์ปจํธ๋กค๋ฌ๋ ํํฐ)์์ ์ ๋ก๋ ํ์ผ ํฌ๊ธฐ ์ ํ๋ง ๊ฑธ์ด๋๋ฉด, ํ์ฌ ๊ตฌํ์ผ๋ก๋ ์ถฉ๋ถํ ์ค์ฉ์ ์ธ ๊ตฌ์กฐ๋ผ๊ณ ํ๋จ๋ฉ๋๋ค.spring-ai-app/src/main/java/org/sprain/ai/service/DocumentProcessingService.java (1)
22-68: ์ ๋ค๋ฆญ ์๋ต ๊ตฌ์กฐ๋ก์ ์ ํ์ ์ ์ ํ๋ฉฐ, ์์ธ/DTO ๊ณ์ฝ๋ง ํ์ธํด ๋๋ฉด ์ข๊ฒ ์ต๋๋ค
extractReceipt/extractBusinessCard/inspectProduct๋ชจ๋ImageAnalysis.of(prompt, MultipartFile)+visionService.analyzeImage(..., DTO.class)ํจํด์ผ๋ก ์ ํต์ผ๋์ด ์์ด ์ฌ์ฌ์ฉ์ฑ๊ณผ ๊ฐ๋ ์ฑ์ด ์ข์ ๋ณด์ ๋๋ค.๋ค๋ง,
- ์ด ์๋น์ค ๋ฉ์๋๋ค์ด
throws IOException์ ๋ ธ์ถํ๋ฏ๋ก, ์์ ์ปจํธ๋กค๋ฌ/ํธ์ถ๋ถ์์ ์ด๋ฅผ ๊ทธ๋๋ก ์ ํํ ์ง(์: 500 ๋งคํ) ์๋๋ฉด ๋๋ฉ์ธ ์์ธ๋ก ๊ฐ์์ง ์ ์ฑ ์ ํ ๋ฒ ์ ๋ฆฌํด ๋๋ฉด ์ข๊ฒ ์ต๋๋ค.ReceiptData/BusinessCard/DefectReport๊ตฌ์กฐ๊ฐ ์ค์ LLM JSON ์๋ต ์คํค๋ง์ ์์ ํ ์ผ์นํ๋์ง, ํ์ฑ ์คํจ ์ ์ด๋ค HTTP ์๋ต์ ๊ธฐ๋ํ๋์ง(์: 4xx vs 5xx)๋ ํ ์คํธ๋ ๊ณ์ฝ ๋ฌธ์๋ก ๋ง์ถฐ ๋๋ฉด ์ดํ ์ฅ์ ๋ถ์์ ๋์์ด ๋ฉ๋๋ค.
| public ResponseEntity<ImageAnalysisResponseV2<ChatResponse>> analyzeImage( | ||
| @RequestParam String prompt, | ||
| @RequestParam MultipartFile image) throws IOException { | ||
|
|
||
|
|
||
| ImageAnalysisResponse response = visionService.analyzeImage(prompt, image); | ||
| return ResponseEntity.ok(response); | ||
| return ResponseEntity.status(HttpStatus.OK) | ||
| .body(visionService.analyzeImage(ImageAnalysis.of(prompt, image), DEFAULT_CHAT_RESPONSE_CLASS)); |
There was a problem hiding this comment.
๐งฉ Analysis chain
analyzeImage ์ ๋ค๋ฆญ ํ์
๊ณผ ์ค์ analysis ๊ฐ ํ์
์ด ์ด๊ธ๋ ์ ์์ต๋๋ค.
ํ์ฌ VisionService.analyzeImage(ImageAnalysis, Class<T>)์์ referenceType์ด ChatResponse.class์ธ ๊ฒฝ์ฐ, ๋ด๋ถ ๊ตฌํ์ ChatResponse๋ฅผ ๋ฐ์ ํ
์คํธ(String)๋ก ๋ณํํ ๋ค:
return ImageAnalysisResponseV2.of(
(T) analysis, // ์ค์ ๋ก๋ String
contentType,
imageBytes.length,
tokenUsage
);์ฒ๋ผ analysis์ String์ ๋ฃ๊ณ ์์ต๋๋ค. ๊ทธ๋ฐ๋ฐ ์ปจํธ๋กค๋ฌ ์๊ทธ๋์ฒ๋:
ResponseEntity<ImageAnalysisResponseV2<ChatResponse>>๋ก ์ ์ธ๋์ด ์์ด, ํธ์ถ ์ธก์์ response.analysis()๋ฅผ ChatResponse๋ก ๊ฐ์ ํ๊ณ ์ ๊ทผํ๋ฉด ๋ฐํ์ ClassCastException์ด ๋ฐ์ํ ์์ง๊ฐ ์์ต๋๋ค.
์ ๋ฆฌ ๋ฐฉ์ ์ค ํ๋๋ ๋ค์ ๋ ์ค์์ ์ ํํ๋ ๊ฒ์ ๋๋ค.
-
๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ง์ง
ChatResponse๋ก ์ ์งVisionService.analyzeImage์ ChatResponse ๋ถ๊ธฐ์์analysisํ๋์ChatResponse์์ฒด๋ฅผ ๋ฃ๊ณ , ํ ํฐ ์ฌ์ฉ๋๋ง ๋ณ๋ ์ถ์ถ:if (response instanceof ChatResponse chatResponse) { // tokenUsage ๊ณ์ฐ return ImageAnalysisResponseV2.of( (T) response, // ChatResponse ๊ทธ๋๋ก contentType, imageBytes.length, tokenUsage ); }
- ์ด๋ ์ปจํธ๋กค๋ฌ ์๊ทธ๋์ฒ(
ImageAnalysisResponseV2<ChatResponse>)๋ ๊ทธ๋๋ก ๋๊ณ , ์ค์ ๋ก๋ChatResponse๋ฅผ ๋ฐ๋๋ก ๋ง์ถ ์ ์์ต๋๋ค.
-
์ปจํธ๋กค๋ฌ ๋ฐํ ํ์ ์
ImageAnalysisResponseV2<String>์ผ๋ก ๋ณ๊ฒฝ- VisionService์์ ChatResponse๋ฅผ ํญ์ String์ผ๋ก ๋ณํํด ์ฐ๊ฒ ๋ค๋ ๋ฐฉํฅ์ด๋ผ๋ฉด, ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ์๊ทธ๋์ฒ์ ์ ๋ค๋ฆญ ํ์
ํ๋ผ๋ฏธํฐ๋ฅผ
<String>์ผ๋ก ๋ฐ๊พธ๊ณ , ํธ์ถ๋ถ์์๋ ๊ทธ ์ ์ ๋ฅผ ๋ช ํํ ํ๋ ์ชฝ์ด ํ์ ์์ ์ฑ ์ธก๋ฉด์์ ๋ ์์ ํฉ๋๋ค.
- VisionService์์ ChatResponse๋ฅผ ํญ์ String์ผ๋ก ๋ณํํด ์ฐ๊ฒ ๋ค๋ ๋ฐฉํฅ์ด๋ผ๋ฉด, ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ์๊ทธ๋์ฒ์ ์ ๋ค๋ฆญ ํ์
ํ๋ผ๋ฏธํฐ๋ฅผ
ํ์ฌ ์ค๊ณ ์๋(ํ๋ก ํธ์์ ChatResponse ๊ตฌ์กฐ๊น์ง ํ์ฉํ ์ง, ํ ์คํธ๋ง ์ธ์ง)์ ๋ง์ถฐ 1 ๋๋ 2 ์ค ํ๋๋ก ํ์ ์ ํต์ผํด ์ฃผ์๋ ๊ฑธ ๊ถ์ฅํฉ๋๋ค.
---
๐ Script executed:
```shell
#!/bin/bash
# Find and examine VisionController.java
fd -e java -e kt | xargs rg -l "class VisionController" 2>/dev/null
Length of output: 74
๐ Script executed:
#!/bin/bash
# Find VisionService.java
fd -e java -e kt | xargs rg -l "class VisionService" 2>/dev/nullLength of output: 139
๐ Script executed:
#!/bin/bash
# Find ImageAnalysisResponseV2.java
fd -e java -e kt | xargs rg -l "class ImageAnalysisResponseV2" 2>/dev/nullLength of output: 0
๐ Script executed:
#!/bin/bash
# Search for analyzeImage method in VisionService with context
rg -n -A 20 "def analyzeImage|analyzeImage\s*\(" --type java --type kt | head -100Length of output: 26
๐ Script executed:
#!/bin/bash
# Search for ImageAnalysisResponseV2.of calls with context
rg -n -B 3 -A 3 "ImageAnalysisResponseV2\.of" --type java --type ktLength of output: 26
---
๐ Script executed:
```shell
#!/bin/bash
# Examine VisionController.java - read the entire file to find analyzeImage
cat spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java
Length of output: 3582
๐ Script executed:
#!/bin/bash
# Examine VisionService.java - read the entire file
cat spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.javaLength of output: 7099
๐ Script executed:
#!/bin/bash
# Search for ImageAnalysisResponseV2 more broadly
rg -n "ImageAnalysisResponseV2" --type java | head -50Length of output: 3761
๐ Script executed:
#!/bin/bash
# Search for ImageAnalysisResponseV2 class definition
rg -n "class ImageAnalysisResponseV2" --type javaLength of output: 0
๐ Script executed:
#!/bin/bash
# Find all files containing ImageAnalysisResponseV2
find . -name "*.java" -exec grep -l "ImageAnalysisResponseV2" {} \;Length of output: 460
๐ Script executed:
#!/bin/bash
# Examine ImageAnalysisResponseV2 to confirm generic type parameter usage
cat spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysisResponseV2.javaLength of output: 754
analyzeImage ์ ๋ค๋ฆญ ํ์
๊ณผ ์ค์ analysis ๊ฐ ํ์
๋ถ์ผ์น ๋ฌธ์ ๊ฐ ํ์ธ๋์์ต๋๋ค.
๊ฒ์ฆ ๊ฒฐ๊ณผ, ๋ฆฌ๋ทฐ ์ฝ๋ฉํธ์ ์ง์ ์ด ์ ํํฉ๋๋ค:
VisionController.analyzeImage๋ImageAnalysisResponseV2<ChatResponse>๋ฐํ์ ์ ์ธํฉ๋๋ค.VisionService.analyzeImage์์ ChatResponse ์ฒ๋ฆฌ ์, ์ค์ ๋ก๋String๊ฐ์analysisํ๋์ ์ ์ฅํฉ๋๋ค:String analysis = chatResponse.getResult().getOutput().getText(); return ImageAnalysisResponseV2.of( (T) analysis, // String์ ChatResponse๋ก ์บ์คํ contentType, imageBytes.length, tokenUsage );
- ๊ฐ์ ์๋น์ค์
analyzeChart๋ฉ์๋์์(String) response.analysis()๋ก ๋ช ์์ ์บ์คํ ํ๋ ๊ฒ์ด ์ด ๋ฌธ์ ๋ฅผ ์ฆ๋ช ํฉ๋๋ค.
์ด๋ Java ์ ๋ค๋ฆญ ๊ณ์ฝ ์๋ฐ์ด๋ฉฐ ํด๋ผ์ด์ธํธ๊ฐ API๋ฅผ ์ฌ์ฉํ ๋ ๋ฐํ์ ClassCastException ์ํ์ด ์์ต๋๋ค. ์ ์๋ ๋ ๊ฐ์ง ํด๊ฒฐ ๋ฐฉ์(โ ChatResponse ๊ทธ๋๋ก ์ ์ง, โก ๋ฐํ ํ์
์ <String>์ผ๋ก ๋ณ๊ฒฝ) ์ค ํ๋๋ก ํ์
์ ํต์ผํ์๊ธฐ ๋ฐ๋๋๋ค.
๐ค Prompt for AI Agents
spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java
around lines 35-41: the controller declares
ResponseEntity<ImageAnalysisResponseV2<ChatResponse>> but the service actually
places a String into the analysis field, causing unsafe casts and possible
ClassCastException; fix by unifying the generic types โ either (A) change the
controller method signature and any default class constant to return
ImageAnalysisResponseV2<String> (and pass String.class to
visionService.analyzeImage), or (B) change VisionService.analyzeImage to return
a real ChatResponse object (wrap the String into a ChatResponse) so the declared
ChatResponse generic is accurate; pick one approach and update method signature,
constants, and any callers accordingly to keep compile-time type safety.
| @SuppressWarnings("unchecked") | ||
| public <T> ImageAnalysisResponseV2<T> analyzeImage(ImageAnalysis imageAnalysis, Class<T> referenceType) { | ||
| String prompt = imageAnalysis.prompt(); | ||
|
|
||
| // 2. MIME ํ์ ๊ฒฐ์ | ||
| String contentType = imageFile.getContentType(); | ||
| if (contentType == null) { | ||
| contentType = "image/jpeg"; // ๊ธฐ๋ณธ๊ฐ | ||
| } | ||
| // 1. ์ด๋ฏธ์ง ๋ฐ์ดํธ ๋ฐฐ์ด ๊ฐ์ ธ์ค๊ธฐ | ||
| byte[] imageBytes = imageAnalysis.imageBytes(); | ||
|
|
||
| // 3. Media ๊ฐ์ฒด ์์ฑ | ||
| Media media = new Media( | ||
| // 2. MIME ํ์ ๊ฒฐ์ | ||
| String contentType = imageAnalysis.contentType(); | ||
| if (contentType == null) { | ||
| contentType = DEFAULT_CONTENT_TYPE; | ||
| } | ||
|
|
||
| // 3. Media ๊ฐ์ฒด ์์ฑ | ||
| Media media = new Media( | ||
| MimeTypeUtils.parseMimeType(contentType), | ||
| new ByteArrayResource(imageBytes) | ||
| ); | ||
| ); | ||
|
|
||
| // 4. UserMessage ์์ฑ (์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋ ์ฌ์ฉ) | ||
| UserMessage userMessage = UserMessage.builder() | ||
| // 4. UserMessage ์์ฑ (์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋ ์ฌ์ฉ) | ||
| UserMessage userMessage = UserMessage.builder() | ||
| .media(List.of(media)) | ||
| .text(prompt) | ||
| .build(); | ||
|
|
||
| // 5. Claude Vision API ํธ์ถ | ||
| ChatResponse response = chatClient.prompt() | ||
| .messages(userMessage) | ||
| .call() | ||
| .chatResponse(); | ||
| // 5. Claude Vision API ํธ์ถ | ||
| T response = promptWithResponseFormat(userMessage, referenceType); | ||
| TokenUsage tokenUsage = null; | ||
|
|
||
| // 6. ์๋ต ์ถ์ถ | ||
| String analysis = response.getResult().getOutput().getText(); | ||
| // 6. ์๋ต ํ์ ์ ๋ฐ๋ผ ์ฒ๋ฆฌ | ||
| if (response instanceof ChatResponse chatResponse) { | ||
| // ChatResponse ํ์ ์ธ ๊ฒฝ์ฐ | ||
| String analysis = chatResponse.getResult().getOutput().getText(); | ||
|
|
||
| // 7. ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ | ||
| TokenUsage tokenUsage = null; | ||
| var metadata = response.getMetadata(); | ||
| // ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ | ||
| var metadata = chatResponse.getMetadata(); | ||
| if (metadata != null && metadata.getUsage() != null) { | ||
| var usage = metadata.getUsage(); | ||
| tokenUsage = new TokenUsage( | ||
| usage.getPromptTokens(), | ||
| usage.getCompletionTokens(), | ||
| usage.getTotalTokens() | ||
| usage.getPromptTokens(), | ||
| usage.getCompletionTokens(), | ||
| usage.getTotalTokens() | ||
| ); | ||
| } | ||
|
|
||
| return ImageAnalysisResponse.of( | ||
| analysis, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| return ImageAnalysisResponseV2.of( | ||
| (T) analysis, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| ); | ||
|
|
||
| } catch (IOException e) { | ||
| log.error("์ด๋ฏธ์ง ์ฒ๋ฆฌ ์คํจ: {}", e.getMessage()); | ||
| throw new RuntimeException("์ด๋ฏธ์ง ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์", e); | ||
| } else { | ||
| // String ํ์ ์ธ ๊ฒฝ์ฐ | ||
| return ImageAnalysisResponseV2.of( | ||
| response, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
๐งฉ Analysis chain
ChatResponse ๋ถ๊ธฐ์์ ์ ๋ค๋ฆญ ํ์ ๊ณผ ์ค์ payload ํ์ ์ด ์ด๊ธ๋ ํ์ ์์ ์ฑ์ด ๊นจ์ง๋๋ค
ํ์ฌ analyzeImage ๋ referenceType == ChatResponse.class ์ธ ๊ฒฝ์ฐ์๋:
promptWithResponseFormat์์ChatResponse๋ฅผ ๋ฐ์์จ ๋ค,String analysis = chatResponse.getResult().getOutput().getText();๋ก ํ ์คํธ๋ฅผ ๋ฝ๊ณ ,- ์ด
analysis(String)๋ฅผ(T) analysis๋ก ์บ์คํ ํดImageAnalysisResponseV2<T>์ ๋ฃ์ต๋๋ค.
์ฆ ํธ์ถ๋ถ์์ ImageAnalysisResponseV2<ChatResponse> response = analyzeImage(..., DEFAULT_CHAT_RESPONSE_CLASS); ์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด,
- ์ ๋ค๋ฆญ ํ์
์ธ์๋
ChatResponse์ด์ง๋ง, - ๋ด๋ถ
analysis()์ค์ ๋ฐํ์ ํ์ ์String์ด๋ผ, - ์ดํ
ChatResponse๋ก ์บ์คํ ํ๊ฑฐ๋ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ ์์ ์ClassCastException์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
analyzeChart ๋ DEFAULT_CHAT_RESPONSE_CLASS ๋ฅผ ๋๊ฒจ ๋๊ณ , ImageAnalysisResponseV2 raw ํ์
+ (String) response.analysis() ๋ก ์บ์คํ
ํ๋ ๋ฑ ํ์ฌ ์ค๊ณ๊ฐ ChatResponse/๋ฌธ์์ด ๋ ๊ฐ๋
์ ๋ค์๊ณ ์์ต๋๋ค.
๋ณด๋ค ํ์ ์์ ํ๊ฒ ํ๋ ค๋ฉด ๋ ๊ฐ์ง ์ค ํ๋๋ก ์ ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค:
- referenceType == ChatResponse.class ์ธ ๊ฒฝ์ฐ ์ค์ ๋ก ChatResponse ๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๋๋ก ๋ณ๊ฒฝ
- T response = promptWithResponseFormat(userMessage, referenceType);
- TokenUsage tokenUsage = null;
-
- // 6. ์๋ต ํ์
์ ๋ฐ๋ผ ์ฒ๋ฆฌ
- if (response instanceof ChatResponse chatResponse) {
- // ChatResponse ํ์
์ธ ๊ฒฝ์ฐ
- String analysis = chatResponse.getResult().getOutput().getText();
-
- // ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ
- var metadata = chatResponse.getMetadata();
- if (metadata != null && metadata.getUsage() != null) {
- var usage = metadata.getUsage();
- tokenUsage = new TokenUsage(
- usage.getPromptTokens(),
- usage.getCompletionTokens(),
- usage.getTotalTokens()
- );
- }
-
- return ImageAnalysisResponseV2.of(
- (T) analysis,
- contentType,
- imageBytes.length,
- tokenUsage
- );
-
- } else {
- // String ํ์
์ธ ๊ฒฝ์ฐ
- return ImageAnalysisResponseV2.of(
- response,
- contentType,
- imageBytes.length,
- tokenUsage
- );
- }
+ T response = promptWithResponseFormat(userMessage, referenceType);
+ TokenUsage tokenUsage = null;
+
+ // 6. ChatResponse ์ธ ๊ฒฝ์ฐ์๋ง ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ
+ if (response instanceof ChatResponse chatResponse) {
+ var metadata = chatResponse.getMetadata();
+ if (metadata != null && metadata.getUsage() != null) {
+ var usage = metadata.getUsage();
+ tokenUsage = new TokenUsage(
+ usage.getPromptTokens(),
+ usage.getCompletionTokens(),
+ usage.getTotalTokens()
+ );
+ }
+ }
+
+ // referenceType ๊ณผ ์ผ์นํ๋ ์ค์ ํ์
๊ทธ๋๋ก ๋ํ
+ return ImageAnalysisResponseV2.of(
+ response,
+ contentType,
+ imageBytes.length,
+ tokenUsage
+ );๊ทธ๋ฆฌ๊ณ analyzeChart ๋ ํ
์คํธ๋ง ํ์ํ๋ฏ๋ก String.class ๋ฅผ ์ฌ์ฉํ๋๋ก ์ ๋ฆฌํ ์ ์์ต๋๋ค:
- ImageAnalysisResponseV2 response = analyzeImage(ImageAnalysis.of(prompt, imageFile), DEFAULT_CHAT_RESPONSE_CLASS);
- return (String) response.analysis();
+ ImageAnalysisResponseV2<String> response =
+ analyzeImage(ImageAnalysis.of(prompt, imageFile), String.class);
+ return response.analysis();์ด๋ ๊ฒ ํ๋ฉด:
ImageAnalysisResponseV2<ChatResponse>์ธ ๊ฒฝ์ฐanalysis()๊ฐ ์ง์งChatResponse๊ฐ ๋๊ณ ,- ๋ฌธ์์ด ์๋ต์ด ํ์ํ ๊ณณ์ ์ผ๊ด๋๊ฒ
String.class๋ฅผ ์ฌ์ฉํด ์ ๋ค๋ฆญ ํ์ ๊ณผ payload ๊ฐ ๋ง์ ๋จ์ด์ง๋๋ค.
- ํน์ ChatResponse ๋ฅผ ์ ํ ๋ ธ์ถํ์ง ์๊ณ โํญ์ Stringโ ์ ๋ฐํํ๋ API ๋ก ๋จ์ํ
์ด๋ฏธ VisionServiceV2 ๊ฐ ChatResponse ๊ธฐ๋ฐ ํ์ดํ๋ผ์ธ์ ๋ช
ํํ๊ฒ ์ ๊ณตํ๊ณ ์์ผ๋ฏ๋ก, ์ด ์๋น์ค๋ ๋จ์ํ String/DTO ๋ฅผ ๋ฐํํ๋ ์ชฝ์ผ๋ก ํ์ ํ๋ ๊ฒ๋ ๊ณ ๋ ค ์ฌ์ง๊ฐ ์์ต๋๋ค. ๋ค๋ง ์ด ๊ฒฝ์ฐ ๊ธฐ์กด ํธ์ถ๋ถ ์ํฅ ๋ฒ์๋ฅผ ๋ฐ๋ก ๊ฒํ ํด์ผ ํฉ๋๋ค.
ํ์ฌ PR ์์ VisionController/๋ค๋ฅธ ์๋น์ค๋ค์ด ImageAnalysisResponseV2<ChatResponse> ๋ก ์ฌ์ฉํ๊ณ ์๋ค๋ฉด, ์ 1๋ฒ ๋ฐฉ์๋๋ก ์ ๋ฆฌํ๊ณ ํธ์ถ๋ถ ์ ๋ค๋ฆญ ํ์
/์บ์คํ
์ ํจ๊ป ๋ง์ถ๋ ๊ฒ์ด ์์ ํด ๋ณด์
๋๋ค.
Also applies to: 133-145
ํ์ํ ์ ๋ณด๋ฅผ ์์งํ๊ธฐ ์ํด ์ฝ๋๋ฒ ์ด์ค๋ฅผ ๊ฒ์ฌํ๊ฒ ์ต๋๋ค.
๐ Script executed:
#!/bin/bash
# VisionService.java ์ ์ฒด ํ์ผ ๋ด์ฉ ํ์ธ
find . -type f -name "VisionService.java" | head -5Length of output: 70
๐ Script executed:
#!/bin/bash
# VisionService ํด๋์ค ๊ตฌ์กฐ์ ๋ฉ์๋ ๋ชฉ๋ก
ast-grep --pattern 'class VisionService {
$$$
}'Length of output: 0
๐ Script executed:
#!/bin/bash
# analyzeImage, analyzeChart, promptWithResponseFormat ๋ฉ์๋ ๊ตฌํ ํ์ธ
rg -nA 30 'public.*analyzeImage\s*\(' --type javaLength of output: 10296
๐ Script executed:
#!/bin/bash
# analyzeChart ๋ฉ์๋ ํ์ธ
rg -nA 20 'public.*analyzeChart\s*\(' --type javaLength of output: 6798
๐ Script executed:
#!/bin/bash
# promptWithResponseFormat ๋ฉ์๋ ํ์ธ
rg -nA 20 'private.*promptWithResponseFormat\s*\(' --type javaLength of output: 1068
๐ Script executed:
#!/bin/bash
# ImageAnalysisResponseV2 ํด๋์ค ๊ตฌ์กฐ
find . -type f -name "ImageAnalysisResponseV2.java" -exec cat {} \;Length of output: 754
๐ Script executed:
#!/bin/bash
# VisionController์์ VisionService ์ฌ์ฉ ํจํด ํ์ธ
rg -nA 5 -B 5 'analyzeImage|analyzeChart' --type java | grep -E '(VisionController|ImageAnalysisResponseV2|ChatResponse)' | head -20Length of output: 2754
๐ Script executed:
#!/bin/bash
# DEFAULT_CHAT_RESPONSE_CLASS ์ ์ ํ์ธ
rg -n 'DEFAULT_CHAT_RESPONSE_CLASS' --type javaLength of output: 709
๐ Script executed:
#!/bin/bash
# compareImages ๋ฉ์๋ ๊ตฌํ ํ์ธ
rg -nA 25 'public.*compareImages\s*\(' --type javaLength of output: 7632
๐ Script executed:
#!/bin/bash
# ์ ์ฒด VisionService ํ์ผ ๋ด์ฉ ํ์ธ
fd VisionService.java -x cat {}Length of output: 7099
๐ Script executed:
#!/bin/bash
# ํธ์ถ๋ถ์์ ImageAnalysisResponseV2<ChatResponse> ์ฌ์ฉ ์ฌ๋ถ ํ์ธ
rg 'ImageAnalysisResponseV2<ChatResponse>' --type javaLength of output: 153
๐ Script executed:
#!/bin/bash
# ChatResponse instanceof ์ฒดํฌ ํ String ์บ์คํ
ํจํด
rg -nA 5 'instanceof ChatResponse' --type javaLength of output: 686
๐ Script executed:
#!/bin/bash
# SimpleClassName ๋ฌธ์์ด ๋น๊ต ํจํด (promptWithResponseFormat์์)
rg -n 'getSimpleName|ChatResponse.*string' --type javaLength of output: 432
๐ Script executed:
#!/bin/bash
# VisionServiceV2์์ ๋น๊ต๋ฅผ ์ํด compareImages ํธ์ถ ํจํด
rg -nB 5 -A 15 'compareImages' --type java | head -50Length of output: 5219
๐ Script executed:
#!/bin/bash
# String.class์ ChatResponse.class ์ฌ์ฉ ํจํด ๋น๊ต
rg -n '\.(class|String\.class|ChatResponse\.class)' --type java | grep -E '(analyzeImage|analyzeChart|promptWithResponseFormat)' | head -20Length of output: 934
๐ Script executed:
#!/bin/bash
# analyzeImage ๋ฉ์๋ ์๋ช
ํ์ธ
rg -n 'analyzeImage\s*\(' --type java | head -10Length of output: 1670
๐ Script executed:
#!/bin/bash
# ์ ์ฒด VisionService ๋ฉ์๋ ๋ชฉ๋ก
ast-grep --pattern 'public $_ $_($$$) { $$$ }'Length of output: 66822
๐ Script executed:
#!/bin/bash
# TokenUsage ํด๋์ค ์ ๋ณด ํ์ธ
find . -type f -name "*TokenUsage*" -o -name "*Usage*" | grep -i tokenLength of output: 63
๐ Script executed:
#!/bin/bash
# ImageAnalysis ํด๋์ค ํ์ธ
fd ImageAnalysis.java -x cat {}Length of output: 860
๐ Script executed:
#!/bin/bash
# DEFAULT_CONTENT_TYPE ์์ ํ์ธ
rg -n 'DEFAULT_CONTENT_TYPE|DEFAULT_CHAT_RESPONSE_CLASS' --type javaLength of output: 1520
๐ Script executed:
#!/bin/bash
# ์บ์คํ
ํจํด ์์ธ ๊ฒ์ฌ
rg -nA 3 -B 3 '\(T\)\s*' --type java | grep -A 5 -B 5 'analysis\|response'Length of output: 1716
๐ Script executed:
#!/bin/bash
# ๋ชจ๋ VisionService ๊ด๋ จ ํ์ผ ๋ชฉ๋ก
find . -type f -name "*Vision*" | grep -E '\.(java|kt)$'Length of output: 220
๐ Script executed:
#!/bin/bash
# ์ ์ฒด ํ์ผ ๊ฒฝ๋ก ํ์ธ
pwd && ls -la | head -20Length of output: 1213
๐ Script executed:
#!/bin/bash
# VisionService.java ์์น ํ์
find . -path '*/spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java'Length of output: 70
๐ Script executed:
#!/bin/bash
# ํด๋น ๊ฒฝ๋ก์์ VisionService ๋ด์ฉ ํ์ธ
cat spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java 2>/dev/null || cat ./src/main/java/org/sprain/ai/service/VisionService.java 2>/dev/null || find . -name VisionService.java -type f -exec cat {} \;Length of output: 7099
๐ Script executed:
#!/bin/bash
# ํ์
์์ ์ฑ ๊ด๋ จ ์ฃผ์ ํด๋์ค๋ค ํ์ธ
find . -type f \( -name "ImageAnalysisResponseV2.java" -o -name "ImageAnalysis.java" -o -name "VisionService.java" \) -exec echo "=== {} ===" \; -exec cat {} \;Length of output: 8954
๐ Script executed:
#!/bin/bash
# ImageAnalysisResponseV2์ of ๋ฉ์๋์ analysis() ๋ฉ์๋ ํ์ธ
rg -nA 10 'public.*analysis\(\)|public static.*of\(' spring-ai-app/src --type java 2>/dev/null | head -50Length of output: 5320
๐ Script executed:
#!/bin/bash
# analyzeChart ๋ผ์ธ 133-145 ํ์ธ
find . -name VisionService.java -exec sed -n '133,145p' {} \;Length of output: 475
๐ Script executed:
#!/bin/bash
# ์ฌ๊ท ๊ฒ์: VisionService์์ raw ํ์
ImageAnalysisResponseV2 ์ฌ์ฉ
rg 'ImageAnalysisResponseV2\s*[a-zA-Z_]' --type javaLength of output: 191
๐ Script executed:
#!/bin/bash
# ChatResponse ๋ฉํ๋ฐ์ดํฐ ๊ตฌ์กฐ ํ์ธ
rg -nA 5 'getMetadata|getUsage' --type java | head -40Length of output: 3801
๐ Script executed:
#!/bin/bash
# ํธ์ถ๋ถ์์ ๋ถ์ ์๋ต ์ฌ์ฉ ๋ฐฉ์ ํ์ธ
rg -nB 3 -A 3 '\.analysis\(\)' --type java | head -50Length of output: 4023
๐ Script executed:
#!/bin/bash
# ์ ์ฒด ๊ตฌ์กฐ๋ฅผ ํ์
ํ๊ธฐ ์ํด ์ฃผ์ ํ์ผ๋ค ๋์ด
find . -type f -name "*.java" | grep -i vision | head -20Length of output: 220
๐ Script executed:
#!/bin/bash
# Spring AI ๊ด๋ จ ChatResponse import ํ์ธ
rg -n 'import.*ChatResponse' --type javaLength of output: 864
๐ Script executed:
#!/bin/bash
# UserMessage ๋น๋ ํจํด ํ์ธ
rg -nA 8 'UserMessage\.builder' --type javaLength of output: 3820
๐ Script executed:
#!/bin/bash
# ํ์
์์ ๋ฌธ์ ์์ ์ ์๋ ์บ์คํ
ํจํด
rg -n '\(T\).*analysis|\(String\).*response\.analysis' --type javaLength of output: 223
๐ Script executed:
#!/bin/bash
# compareImages์์ "image/jpeg" ํ๋์ฝ๋ฉ ํ์ธ
rg -n 'image/jpeg|JPEG' --type javaLength of output: 631
๐ Script executed:
#!/bin/bash
# ์ ์ฒด ์ค ์ ํ์ธ ๋ฐ ๋ผ์ธ ๋ฒ์ ๊ฒ์ฆ
find . -name VisionService.java -exec wc -l {} \; -exec sed -n '38,99p' {} \; -exec echo "---" \; -exec sed -n '133,145p' {} \;Length of output: 2620
๐ Script executed:
#!/bin/bash
# ๋ง์ง๋ง์ผ๋ก VisionService.java ๊ฒฝ๋ก ์ฌํ์ธ
find . -name VisionService.java -type fLength of output: 70
๐ Script executed:
#!/bin/bash
# ๊ฒฝ๋ก๊ฐ ์์ผ๋ฉด ์ง์ ๋ด์ฉ ํ์ธ
paths=$(find . -name VisionService.java -type f)
if [ -n "$paths" ]; then
for path in $paths; do
echo "=== File: $path ==="
wc -l "$path"
echo "--- Lines 38-99 ---"
sed -n '38,99p' "$path"
echo "--- Lines 133-145 ---"
sed -n '133,145p' "$path"
done
else
echo "VisionService.java not found"
find . -name "*.java" -path "*/service/*" -type f | head -20
fiLength of output: 2743
๐ Script executed:
#!/bin/bash
# ๊ตฌ์กฐ ํ์
: spring-ai-app ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
find spring-ai-app -type d -name "service" 2>/dev/null || find . -type d -name "service" 2>/dev/nullLength of output: 49
๐ Script executed:
#!/bin/bash
# ๋ชจ๋ java ํ์ผ์์ analyzeImage ๊ฒ์
rg 'def analyzeImage|public.*analyzeImage' --type java -A 25Length of output: 8363
๐ Script executed:
#!/bin/bash
# ์ต์ข
: VisionService ๋ฐ๊ฒฌ ๋ฐ ์ ์ฒด ๋ด์ฉ ์ถ๋ ฅ
find . -type f -name "VisionService.java" 2>/dev/null | while read f; do echo "Found: $f"; cat "$f"; doneLength of output: 7177
๐ Script executed:
#!/bin/bash
# ๋์: ์ ์ฒด ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ํ์ธ
ls -la && echo "---" && find . -maxdepth 3 -type f -name "*Vision*" 2>/dev/nullLength of output: 1198
ChatResponse ๋ถ๊ธฐ์์ ์ ๋ค๋ฆญ ํ์ ๊ณผ ์ค์ payload ํ์ ๋ถ์ผ์น๋ก ์ธํ ํ์ ์์ ์ฑ ๋ฌธ์ ํ์ธ
๊ฒ์ฆ ๊ฒฐ๊ณผ ๋ฆฌ๋ทฐ ์ฝ๋ฉํธ์ ์ง์ ์ด ์ ํํฉ๋๋ค. ์ฝ๋๋ฒ ์ด์ค์์ ๋ค์ ๋ ๊ฐ์ง critical issue๋ฅผ ํ์ธํ์ต๋๋ค:
-
analyzeImage ๋ฉ์๋ (๋ผ์ธ 84):
referenceType == ChatResponse.class์ผ ๋ Stringanalysis๋ฅผ(T) analysis๋ก ์บ์คํ ํ์ฌ ๋ฐํํฉ๋๋ค. ํธ์ถ๋ถ(VisionController)์์ImageAnalysisResponseV2<ChatResponse>๋ฅผ ๊ธฐ๋ํ์ง๋ง, ์ค์ ๋ด๋ถ ํ์ ์ String์ด ๋์ด ๋ฐํ์์ClassCastException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. -
analyzeChart ๋ฉ์๋ (๋ผ์ธ 143-144): raw ํ์
ImageAnalysisResponseV2์ฌ์ฉํ๋ฉด์DEFAULT_CHAT_RESPONSE_CLASS๋ฅผ ๋๊ธฐ๊ณ ,(String) response.analysis()๋ก ๊ฐ์ ์บ์คํ ํฉ๋๋ค.
์ ์๋ ์์ ๋ฐฉ์์ด ์ ์ ํฉ๋๋ค. ์ค์ ํ์ ๊ณผ ์ ๋ค๋ฆญ ํ๋ผ๋ฏธํฐ๋ฅผ ์ผ์น์ํค๋ ค๋ฉด:
- analyzeImage์์ ChatResponse ๋ถ๊ธฐ ์ String์ ์ถ์ถํ ํ ์ค์ ๋ก๋ String ํ์ ์ผ๋ก ๋ฐํํ๊ณ
- analyzeChart๋
String.class๋ฅผ ๋ช ์์ ์ผ๋ก ์ฌ์ฉํ์ฌ ํ์ ์์ ์ฑ์ ๋ณด์ฅํด์ผ ํฉ๋๋ค
์ถ๊ฐ๋ก promptWithResponseFormat์ getSimpleName() ๋ฌธ์์ด ๋น๊ต(๋ผ์ธ 208)๋ Class.equals() ๋น๊ต๋ก ๊ฐ์ ํ๋ฉด ๋์ฑ ๊ฒฌ๊ณ ํด์ง๋๋ค.
| String rawText = chatResponse.getResult().getOutput().getText(); | ||
| log.debug("Vision API ์๋ณธ ์๋ต: {}", rawText); | ||
|
|
There was a problem hiding this comment.
๐งฉ Analysis chain
LLM ์๋ต ์ ๋ฌธ ๋ก๊น ์ ์์์ฆยท๋ช ํจ ๋ฑ PII ์ ์ถ ๋ฆฌ์คํฌ๊ฐ ์์ด ๋ณด์ ๋๋ค
rawText, ์ ์ ์ /ํ ์๋ต, JSON ํ์ฑ ์คํจ ์ ์ ์ฒด ํ
์คํธ๋ฅผ DEBUG ๋ก ๋จ๊ธฐ๊ณ ์๋๋ฐ, ์ด ์๋น์ค๊ฐ ์์์ฆ/๋ช
ํจ/์ ํ ์ด๋ฏธ์ง ๋ฑ์ ๋ค๋ฃจ๋ ๋งํผ,
- ๊ณ ๊ฐ ์ด๋ฆ, ์ฐ๋ฝ์ฒ, ์ฃผ์, ๊ฒฐ์ ๊ธ์ก ๋ฑ ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ๊ทธ๋๋ก ๋ก๊ทธ์ ๋จ์ ์ ์๊ณ ,
- ์ด์ ํ๊ฒฝ์์ DEBUG ๋ ๋ฒจ์ด ํ์ฑํ๋๋ฉด ์ฅ๊ธฐ ๋ณด๊ด/์ ์ก๋๋ฉด์ ๊ฐ์ธ์ ๋ณด/๋ณด์ ๋ฆฌ์คํฌ๊ฐ ๋ ์ ์์ต๋๋ค.
์ด์ ํ๊ฒฝ ๊ธฐ์ค์ผ๋ก๋,
- ์ ๋ฌธ์ด ์๋ ์ผ๋ถ๋ง ๋ง์คํน/์ ๋จํด ๋ก๊น ํ๊ฑฐ๋(์: ์ N์ + ๊ธธ์ด),
- ์๋ฌ ์ํฉ์์๋ ์๋ณ ๋ถ๊ฐ๋ฅํ ์๋ณ์๋ ์์ฝ ์ ๋ณด๋ง ๋จ๊ธฐ๊ณ ์๋ฌธ์ ์ ์ฅํ์ง ์๋ ์ชฝ์ ๊ถ์ฅํฉ๋๋ค.
Also applies to: 267-268, 280-305
---
๐ Script executed:
```shell
#!/bin/bash
# Retrieve the VisionServiceV2.java file to examine logging patterns
fd -t f "VisionServiceV2.java" --exec cat {} \;
Length of output: 9807
๐ Script executed:
#!/bin/bash
# Search for logging patterns and DEBUG statements in the codebase
rg -n "log\.(debug|info|warn|error)" spring-ai-app/src/main/java/org/sprain/ai/service/ -C 3 --type javaLength of output: 10931
๐ Script executed:
#!/bin/bash
# Search for any PII masking/redaction utilities
rg -n "(mask|redact|hide|sanitize|PII)" spring-ai-app/src --type javaLength of output: 0
๐ Script executed:
#!/bin/bash
# Check for logging configuration files
fd -t f "(logback|log4j|logging)" --extension xml --extension properties --extension yml --extension yamlLength of output: 0
๋ฏผ๊ฐ ์ ๋ณด ๋ก๊น ์ ๊ฑฐ ๋ฐ ๋ง์คํน ๊ตฌํ ํ์
LLM ์๋ต ์ ๋ฌธ์ DEBUG ๋ฐ ERROR ๋ ๋ฒจ์์ ๋ก๊น ํ๊ณ ์์ด PII ์ ์ถ ๋ฆฌ์คํฌ๊ฐ ์ค์ ๋ก ์กด์ฌํฉ๋๋ค. ์ด ์๋น์ค๊ฐ ์์์ฆ/๋ช ํจ ๋ฑ ๋ฏผ๊ฐ ์ ๋ณด๋ฅผ ํฌํจํ ์ด๋ฏธ์ง๋ฅผ ์ฒ๋ฆฌํ๋ฏ๋ก, ๋ค์ ์์น์ ๋ก๊ทธ์์ ๊ณ ๊ฐ ์ด๋ฆ, ์ฐ๋ฝ์ฒ, ์ฃผ์, ๊ฒฐ์ ๊ธ์ก ๋ฑ์ด ๋ ธ์ถ๋ ์ ์์ต๋๋ค:
- Line 66: ์๋ณธ ์๋ต ์ ๋ฌธ ๋ก๊น
- Line 267: JSON ํ์ฑ ์คํจ ์ ์ ์ฒด ํ ์คํธ
- Line 280, 304: ์ ์ ์ /ํ ์๋ต ์ ๋ฌธ
- Line 297: ERROR ๋ก๊ทธ์ ์คํจํ ์๋ต ์ ๋ฌธ
์ด์ ํ๊ฒฝ ๊ธฐ์ค์ผ๋ก ๋ค์ ์ค ํ๋๋ฅผ ์ ์ฉํ์๊ธฐ ๋ฐ๋๋๋ค:
- ์๋ต ๊ธธ์ด๋ง ๋ก๊น
(์:
์๋ต ๊ธธ์ด: {}bytes) - ์๋ณ ๋ถ๊ฐ๋ฅํ ์์ฝ ์ ๋ณด๋ง ๊ธฐ๋ก (์: JSON ํค ๋ชฉ๋ก, ์ถ์ถ ์ฑ๊ณต ์ฌ๋ถ)
- ์๋ฌ ์ํฉ์์๋ ์๋ฌธ ์ ์ธ ํ ์๋ฌ ํ์ ๊ณผ ์๋ณ์๋ง ๊ธฐ๋ก
๐ค Prompt for AI Agents
spring-ai-app/src/main/java/org/sprain/ai/service/VisionServiceV2.java lines
~65-67, 267, 280, 297, 304: The code currently logs full LLM responses (raw and
pre/post-clean) which may contain PII; replace these full-text logs with
redacted/summary logs. Specifically, remove or change debug/error calls that
print rawText or full responses to instead log only safe metadata (e.g.,
response length in bytes, a list of top-level JSON keys, and boolean flags like
"parseSuccess" or "extractionSuccess"), and on errors log only the error
type/message and a non-sensitive identifier; implement a reusable redact utility
that strips/obfuscates common PII patterns (names, phone numbers, addresses,
payment amounts) and call it before any remaining debug logs, and ensure
full-text logging is gated behind an explicit non-production flag so production
never emits unredacted responses.
|
/gemini review |
There was a problem hiding this comment.
Code Review
์ด PR์ Vision ์๋น์ค๋ฅผ ๋ฆฌํฉํ ๋งํ์ฌ ์ผ๊ด๋ ์๋ต ๊ตฌ์กฐ๋ฅผ ๋์
ํ๊ณ ์๋ก์ด ์์์ฆ ์ฒ๋ฆฌ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ์ค์ํ ๋ณ๊ฒฝ ์ฌํญ์ ํฌํจํ๊ณ ์์ต๋๋ค. ImageAnalysisResponseV2 DTO๋ฅผ ๋์
ํ์ฌ API ์๋ต์ ํ์คํํ ์ ์ ๋งค์ฐ ์ข์ ๊ฐ์ ์
๋๋ค.
ํ์ง๋ง ๋ฆฌํฉํ ๋ง๋ VisionService์ analyzeImage ๋ฉ์๋์ ์ฌ๊ฐํ ๋ฒ๊ทธ๊ฐ ์์ต๋๋ค. ํน์ ์กฐ๊ฑด์์ ClassCastException์ด ๋ฐ์ํ ์ ์์ผ๋ฉฐ, ๊ตฌ์กฐํ๋ ์๋ต์ ๋ฐ์ ๋ ํ ํฐ ์ฌ์ฉ๋ ๋ฉํ๋ฐ์ดํฐ๊ฐ ์์ค๋๋ ์ค๊ณ ๊ฒฐํจ๋ ์กด์ฌํฉ๋๋ค.
PR์ ํจ๊ป ์ถ๊ฐ๋ VisionServiceV2.java ํ์ผ์ ์ด๋ฌํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ ๋ ๋์ ๊ตฌํ์ผ๋ก ๋ณด์
๋๋ค. ํ์ฌ ์ด ์๋น์ค๋ ์ฌ์ฉ๋๊ณ ์์ง ์์ง๋ง, ๊ธฐ์กด VisionService์ ๋ถ์์ ํ ๋ก์ง์ VisionServiceV2์ ๊ฒฌ๊ณ ํ ๋ก์ง์ผ๋ก ๋์ฒดํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ๊ฐ๋ ฅํ ๊ถ์ฅํฉ๋๋ค.
๊ทธ ์ธ์๋ ์ปจํธ๋กค๋ฌ์ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ์๊ณผ ์ผ๋ถ ์๋น์ค์ ๋ฏธ์ฌ์ฉ ์ฝ๋ ๋ฑ ๊ฐ์ ์ด ํ์ํ ๋ถ๋ถ์ด ์์ต๋๋ค. ์๋์ ๊ฐ๋ณ ์ฝ๋ฉํธ์์ ์์ธํ ๋ด์ฉ์ ํ์ธํด์ฃผ์ธ์.
| public static ImageAnalysis of(ImageAnalysisRequest request) throws IOException { | ||
| return ImageAnalysis.of(request.prompt(), request.image()); | ||
| } |
| if (response instanceof ChatResponse chatResponse) { | ||
| // ChatResponse ํ์ ์ธ ๊ฒฝ์ฐ | ||
| String analysis = chatResponse.getResult().getOutput().getText(); | ||
|
|
||
| // 7. ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ | ||
| TokenUsage tokenUsage = null; | ||
| var metadata = response.getMetadata(); | ||
| // ํ ํฐ ์ฌ์ฉ๋ ์ถ์ถ | ||
| var metadata = chatResponse.getMetadata(); | ||
| if (metadata != null && metadata.getUsage() != null) { | ||
| var usage = metadata.getUsage(); | ||
| tokenUsage = new TokenUsage( | ||
| usage.getPromptTokens(), | ||
| usage.getCompletionTokens(), | ||
| usage.getTotalTokens() | ||
| usage.getPromptTokens(), | ||
| usage.getCompletionTokens(), | ||
| usage.getTotalTokens() | ||
| ); | ||
| } | ||
|
|
||
| return ImageAnalysisResponse.of( | ||
| analysis, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| return ImageAnalysisResponseV2.of( | ||
| (T) analysis, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| ); |
There was a problem hiding this comment.
์ฌ๊ธฐ์ ์ฌ๊ฐํ ๋ฒ๊ทธ๊ฐ ์์ต๋๋ค. ์ด ๋ฉ์๋๊ฐ ChatResponse.class๋ฅผ referenceType์ผ๋ก ํ์ฌ ํธ์ถ๋๋ฉด, ์ ๋ค๋ฆญ ํ์
T๋ ChatResponse๊ฐ ๋ฉ๋๋ค. ํ์ง๋ง 84๋ฒ์งธ ์ค์์ String ํ์
์ธ analysis ๋ณ์๋ฅผ T๋ก ์บ์คํ
ํ๊ณ ์์ด, ๋ฐํ์์ String์ ChatResponse๋ก ๋ณํํ ์ ์๋ค๋ ClassCastException์ด ๋ฐ์ํฉ๋๋ค.
| } | ||
|
|
||
| @PostMapping(value = "/receipt", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||
| public ResponseEntity<ImageAnalysisResponseV2<ReceiptData>> processReceipt(@RequestParam("image") MultipartFile image) throws IOException, NoSuchFieldException { |
There was a problem hiding this comment.
์ปจํธ๋กค๋ฌ ๋ฉ์๋ ์๊ทธ๋์ฒ์ NoSuchFieldException๊ณผ ๊ฐ์ ํน์ ๊ตฌํ ์์ธ๋ฅผ ๋
ธ์ถํ๋ ๊ฒ์ ์ข์ ๋ฐฉ๋ฒ์ด ์๋๋๋ค. ์ด ์์ธ๋ ๋ด๋ถ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ณ , @ControllerAdvice ๋ฑ์ ์ฌ์ฉํ์ฌ 400 Bad Request ๋๋ 500 Internal Server Error์ ๊ฐ์ ์ ์ ํ HTTP ์ํ ์ฝ๋๋ก ๋งคํํ๋ ๊ฒ์ด ์ข์ต๋๋ค. API ๊ณ์ฝ์ด ๋ด๋ถ ์๋น์ค ๊ตฌํ์ ์ข
์๋์ง ์๋๋ก ํด์ผ ํฉ๋๋ค.
| } else { | ||
| // String ํ์ ์ธ ๊ฒฝ์ฐ | ||
| return ImageAnalysisResponseV2.of( | ||
| response, | ||
| contentType, | ||
| imageBytes.length, | ||
| tokenUsage | ||
| ); | ||
| } |
There was a problem hiding this comment.
์ด else ๋ธ๋ก์์๋ ์๋ต์ด entity๋ก ์ง์ ๋ณํ๋ ๋(ReceiptData ๋ฑ) tokenUsage๊ฐ ํญ์ null์ด ๋ฉ๋๋ค. ChatClient์์ .entity(responseType)๋ฅผ ํธ์ถํ๋ฉด ํ ํฐ ์ฌ์ฉ๋ ์ ๋ณด๊ฐ ํฌํจ๋ ChatResponse ๋ฉํ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์
๋๋ค. ์ด๋ ์ค์ํ ์ ๋ณด ์์ค๋ก ์ด์ด์ง๋๋ค. ํจ๊ป ์ถ๊ฐ๋ VisionServiceV2.java ํ์ผ์ ๊ตฌํ์ ChatResponse๋ฅผ ๋จผ์ ๋ฐ์ ํ ๋ด์ฉ์ ํ์ฑํจ์ผ๋ก์จ ์ด ๋ฌธ์ ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํด๊ฒฐํ๊ณ ์์ต๋๋ค.
| public ResponseEntity<Map<String, String>> compareImages( | ||
| @RequestParam("image1") MultipartFile image1, | ||
| @RequestParam("image2") MultipartFile image2) { | ||
| @RequestParam("image1") MultipartFile image1, | ||
| @RequestParam("image2") MultipartFile image2) { |
There was a problem hiding this comment.
์ด compareImages ๋ฉ์๋๋ ์ปจํธ๋กค๋ฌ ๋ด ๋ค๋ฅธ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ฉ์๋(analyzeImage, extractText ๋ฑ)์ ๋ฌ๋ฆฌ throws IOException์ ์ ์ธํ์ง ์์ต๋๋ค. ์ด๋ ์ผ๊ด์ฑ ์๋ ์ค๋ฅ ์ฒ๋ฆฌ ๋ฐฉ์์
๋๋ค. visionService.compareImages๊ฐ IOException์ RuntimeException์ผ๋ก ๋ํํ๊ณ ์๋๋ฐ, ์ผ๊ด์ฑ์ ์ํด visionService.compareImages๊ฐ IOException์ ์ง์ ๋์ง๋๋ก ์์ ํ๊ณ ์ฌ๊ธฐ์๋ ์ ์ธํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์.
| @Slf4j | ||
| public class ReceiptAnalysisService { | ||
| private final VisionService visionService; | ||
| private final ObjectMapper objectMapper; |
| private final ObjectMapper objectMapper; | ||
|
|
||
|
|
||
| public ImageAnalysisResponseV2<ReceiptData> processReceipt(MultipartFile imageFile) throws IOException, NoSuchFieldException { |
There was a problem hiding this comment.
์ด ๋ฉ์๋๋ ์๊ทธ๋์ฒ์ throws NoSuchFieldException์ ์ ์ธํ๊ณ ์์ง๋ง, ์ค์ ๋ฉ์๋ ๋ณธ๋ฌธ์์๋ ์ด ์์ธ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค. (์ด ์์ธ๋ฅผ ๋์ง๋ cleanJsonResponse๊ฐ ํธ์ถ๋์ง ์์). ์ด๋ ์คํด์ ์์ง๊ฐ ์์ผ๋ฏ๋ก throws ์ ์์ NoSuchFieldException์ ์ ๊ฑฐํด์ผ ํฉ๋๋ค.
| private String cleanJsonResponse(String response) throws NoSuchFieldException { | ||
| if (response == null || response.isEmpty()) { | ||
| throw new NoSuchFieldException("Vision API ์๋ต์ด ๋น์ด์์ต๋๋ค"); | ||
| } | ||
|
|
||
| log.debug("์ ์ ์ ์๋ต: {}", response); | ||
|
|
||
| // 1. ๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ | ||
| response = response.replaceAll("```json\\s*", ""); | ||
| response = response.replaceAll("```\\s*", ""); | ||
|
|
||
| // 2. ๋ชจ๋ ๋ฐฑํฑ ์ ๊ฑฐ | ||
| response = response.replace("`", ""); | ||
|
|
||
| // 3. ์๋ค ๊ณต๋ฐฑ ์ ๊ฑฐ | ||
| response = response.trim(); | ||
|
|
||
| // 4. JSON ์์ ์์น ์ฐพ๊ธฐ | ||
| int jsonStart = response.indexOf("{"); | ||
| int jsonEnd = response.lastIndexOf("}"); | ||
|
|
||
| if (jsonStart == -1 || jsonEnd == -1 || jsonStart >= jsonEnd) { | ||
| log.error("์ ํจํ JSON์ ์ฐพ์ ์ ์์ต๋๋ค. ์๋ต: {}", response); | ||
| throw new NoSuchFieldException("Vision API ์๋ต์์ JSON์ ์ฐพ์ ์ ์์ต๋๋ค"); | ||
| } | ||
|
|
||
| // 5. JSON ๋ถ๋ถ๋ง ์ถ์ถ | ||
| response = response.substring(jsonStart, jsonEnd + 1); | ||
|
|
||
| log.debug("์ ์ ํ ์๋ต: {}", response); | ||
|
|
||
| return response; | ||
| } |
There was a problem hiding this comment.
cleanJsonResponse ๋ฉ์๋๊ฐ ์ด ์๋น์ค ๋ด์์ ํธ์ถ๋์ง ์๋ ๋ฏธ์ฌ์ฉ ์ฝ๋(dead code)์
๋๋ค. ํ์ํ ๋ก์ง์ด๋ผ๋ฉด ์ฌ์ฉํด์ผ ํ๊ณ , ๊ทธ๋ ์ง ์๋ค๋ฉด ์ฝ๋๋ฒ ์ด์ค๋ฅผ ๊น๋ํ๊ฒ ์ ์งํ๊ธฐ ์ํด ์ ๊ฑฐํด์ผ ํฉ๋๋ค. ์ ์ฌํ๊ณ ์ค์ ๋ก ์ฌ์ฉ๋๋ ๊ตฌํ์ด VisionServiceV2์ ์กด์ฌํฉ๋๋ค.
| @SuppressWarnings("unchecked") | ||
| private <T> T promptWithResponseFormat(Message userMessage, Class<T> responseType) { | ||
| String responseTypeClassName = responseType.getSimpleName(); | ||
| if ("ChatResponse".equals(responseTypeClassName)) return (T) promptByChatResponse(userMessage); |
There was a problem hiding this comment.
ํด๋์ค ํ์
์ ๋จ์ ์ด๋ฆ ๋ฌธ์์ด("ChatResponse")๋ก ๋น๊ตํ๋ ๊ฒ์ ๋ถ์์ ํฉ๋๋ค. ํด๋์ค ์ด๋ฆ์ด ๋ณ๊ฒฝ๋๊ฑฐ๋ ๋ค๋ฅธ ํจํค์ง์ ๋์ผํ ์ด๋ฆ์ ํด๋์ค๊ฐ ์๊ธฐ๋ฉด ์ฝ๋๊ฐ ๊นจ์ง ์ ์์ต๋๋ค. ์ง์ ํด๋์ค๋ฅผ ๋น๊ตํ๋ ๊ฒ์ด ๋ ์์ ํ๊ณ ๋ช
ํํฉ๋๋ค.
| if ("ChatResponse".equals(responseTypeClassName)) return (T) promptByChatResponse(userMessage); | |
| if (responseType == ChatResponse.class) return (T) promptByChatResponse(userMessage); |
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์๋ก์ด ๊ธฐ๋ฅ
๊ฐ์ ์ฌํญ
โ๏ธ Tip: You can customize this high-level summary in your review settings.