Skip to content

Feature/receipt image vision v2#3

Open
jinho-yoo-jack wants to merge 2 commits into
masterfrom
feature/receipt-image-vision-v2
Open

Feature/receipt image vision v2#3
jinho-yoo-jack wants to merge 2 commits into
masterfrom
feature/receipt-image-vision-v2

Conversation

@jinho-yoo-jack

@jinho-yoo-jack jinho-yoo-jack commented Nov 20, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

๋ฆด๋ฆฌ์Šค ๋…ธํŠธ

  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ

    • ์˜์ˆ˜์ฆ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ฒฐ๊ณผ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ด๋ฏธ์ง€ ์œ ํ˜•, ํฌ๊ธฐ, ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰)๊ฐ€ ํ•จ๊ป˜ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
  • ๊ฐœ์„  ์‚ฌํ•ญ

    • Vision ์„œ๋น„์Šค์˜ ์ด๋ฏธ์ง€ ๋ถ„์„ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐœ์„ ๋˜์–ด ๋” ์ผ๊ด€๋œ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    • ๋ฌธ์„œ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ๊ฐ€ ์žฌ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown

์š”์•ฝ

Walkthrough

์ด ํ’€ ์š”์ฒญ์€ ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ธฐ๋Šฅ์„ ์ œ๋„ˆ๋ฆญ ์‘๋‹ต ๊ตฌ์กฐ๋กœ ๋ฆฌํŒฉํ† ๋งํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ImageAnalysis ๋ ˆ์ฝ”๋“œ์™€ ์ œ๋„ˆ๋ฆญ ImageAnalysisResponseV2<T> ์‘๋‹ต ํƒ€์ž…์„ ๋„์ž…ํ•˜๊ณ , ๋น„์ „ ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ฉํ•˜๋ฉฐ, ์˜์ˆ˜์ฆ ์ฒ˜๋ฆฌ ์ „์šฉ ReceiptAnalysisService๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ๋ฌธ์„œ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ƒˆ ์‘๋‹ต ๊ตฌ์กฐ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.

Changes

๋‚ด์šฉ ์„ค๋ช…
ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”
README.md
ํ”„๋กœ์ ํŠธ ์„ค๋ช…์ด ํฌํ•จ๋œ ์ƒˆ README ํŒŒ์ผ ์ถ”๊ฐ€
๊ณตํ†ต DTO ์ถ”๊ฐ€
spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysis.java
spring-ai-app/src/main/java/org/sprain/ai/dto/ImageAnalysisResponseV2.java
์ด๋ฏธ์ง€ ๋ถ„์„์„ ์œ„ํ•œ ์ƒˆ ๋ ˆ์ฝ”๋“œ ๋ฐ ์ œ๋„ˆ๋ฆญ ์‘๋‹ต ๋ž˜ํผ ๋„์ž…. ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ์œ ์—ฐํ•œ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์ง€์›
๋น„์ „ ์„œ๋น„์Šค ๋ฆฌํŒฉํ† ๋ง
spring-ai-app/src/main/java/org/sprain/ai/service/VisionService.java
spring-ai-app/src/main/java/org/sprain/ai/service/VisionServiceV2.java
๊ธฐ์กด analyzeImage ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๋„ˆ๋ฆญ ๋ฒ„์ „์œผ๋กœ ๋ณ€ํ™˜. ์ƒˆ๋กœ์šด VisionServiceV2 ๊ตฌํ˜„์œผ๋กœ ํ† ํฐ ์‚ฌ์šฉ ์ถ”์  ๋ฐ ํƒ€์ž… ๋ณ€ํ™˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
๋ฌธ์„œ ์ฒ˜๋ฆฌ ์„œ๋น„์Šค ์—…๋ฐ์ดํŠธ
spring-ai-app/src/main/java/org/sprain/ai/service/DocumentProcessingService.java
๋ฐ˜ํ™˜ ํƒ€์ž…์„ ImageAnalysisResponseV2<T>๋กœ ๋ณ€๊ฒฝ. ๋‚ด๋ถ€ ํŒŒ์‹ฑ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  ๋น„์ „ ์„œ๋น„์Šค๋กœ ์œ„์ž„
์‹ ๊ทœ ์˜์ˆ˜์ฆ ๋ถ„์„ ์„œ๋น„์Šค
spring-ai-app/src/main/java/org/sprain/ai/service/ReceiptAnalysisService.java
์˜์ˆ˜์ฆ ์ฒ˜๋ฆฌ ์ „์šฉ ์„œ๋น„์Šค ์ถ”๊ฐ€. JSON ์ •์ œ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง ํฌํ•จ
์ปจํŠธ๋กค๋Ÿฌ ํ†ตํ•ฉ
spring-ai-app/src/main/java/org/sprain/ai/controller/VisionController.java
ReceiptAnalysisService ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ /receipt ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€. ๊ธฐ์กด ์—”๋“œํฌ์ธํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์—…๋ฐ์ดํŠธ

Sequence Diagrams

sequenceDiagram
    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
Loading
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
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~60 minutes

  • ์ฃผ์˜ ์˜์—ญ:
    • VisionService.java: ๊ธฐ์กด analyzeImage ๋ฉ”์„œ๋“œ์˜ ์™„์ „ํ•œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ณ€๊ฒฝ(String, MultipartFile โ†’ ImageAnalysis, Class<T>). ์ œ๋„ˆ๋ฆญ ๋””์ŠคํŒจ์น˜ ๋กœ์ง(promptWithResponseFormat, promptByChatResponse)๊ณผ ํƒ€์ž… ์บ์ŠคํŒ… ์•ˆ์ „์„ฑ ๊ฒ€์ฆ ํ•„์š”
    • VisionServiceV2.java: ์ƒˆ๋กœ์šด ๊ตฌํ˜„์ฒด๋กœ JSON ์ •์ œ(cleanJsonResponse), ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(RuntimeException ๋ž˜ํ•‘) ๊ฒ€ํ†  ํ•„์š”
    • ReceiptAnalysisService.java: ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง, JSON ๊ฒ€์ฆ ๋กœ์ง ๋ฐ NoSuchFieldException ์Šค๋กœ์šฐ ์กฐ๊ฑด ๊ฒ€ํ† 
    • DocumentProcessingService.java: ๋ฐ˜ํ™˜ ํƒ€์ž… ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ๊ธฐ์กด ํ˜ธ์ถœ์ฒ˜ ํ˜ธํ™˜์„ฑ ์˜ํ–ฅ ๋ฒ”์œ„ ํŒŒ์•… ํ•„์š”
    • ์ œ๋„ˆ๋ฆญ ๊ตฌ์กฐ(ImageAnalysisResponseV2<T>) ๋„์ž…์— ๋”ฐ๋ฅธ ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ฐ ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์ •๋ณด ์†์‹ค ๊ฒ€ํ† 

Poem

๐Ÿฐ ์ƒˆ๋กœ์šด ๊ตฌ์กฐ๋กœ ์ด๋ฏธ์ง€ ๋ถ„์„,
์ œ๋„ˆ๋ฆญ์˜ ์šฐ์•„ํ•œ ๋งˆ๋ฒ•,
ํ† ํฐ์„ ์„ธ๋ฉฐ ์‘๋‹ต์„ ๊นŽ๊ณ ,
์˜์ˆ˜์ฆ๋ถ€ํ„ฐ ์ฐจํŠธ๊นŒ์ง€ ๋ชจ๋‘ ๋‹ด์•„,
๋ด„ AI์˜ ๋งˆ์Œ๋„ ํ•œ ๋ฐœ ๋” ๋‚˜์•„๊ฐ”๋„ค! โœจ

Pre-merge checks and finishing touches

โœ… Passed checks (3 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.
Title check โœ… Passed PR ์ œ๋ชฉ์ด ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ฐ˜์˜ํ•˜๋ฉฐ, ๊ตฌ์ฒด์ ์ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. Receipt ์ด๋ฏธ์ง€ ๋ถ„์„ ๋ฐ Vision V2 ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด๋ผ๋Š” ํ•ต์‹ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž˜ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค.
Docstring Coverage โœ… Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings
๐Ÿงช Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/receipt-image-vision-v2

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.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. ๐Ÿ“ Description โ€” Summarize the main change in 50โ€“60 words, explaining what was done.
  2. ๐Ÿ““ References โ€” List relevant issues, discussions, documentation, or related PRs.
  3. ๐Ÿ“ฆ Dependencies & Requirements โ€” Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. ๐Ÿ“Š Contributor Summary โ€” Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. โœ”๏ธ Additional Notes โ€” Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 4642821 and 2c71c0e.

๐Ÿ“’ 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)๋„ ํ…Œ์ŠคํŠธ๋‚˜ ๊ณ„์•ฝ ๋ฌธ์„œ๋กœ ๋งž์ถฐ ๋‘๋ฉด ์ดํ›„ ์žฅ์•  ๋ถ„์„์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

Comment on lines +35 to +41
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ 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์ด ๋ฐœ์ƒํ•  ์†Œ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ •๋ฆฌ ๋ฐฉ์•ˆ ์ค‘ ํ•˜๋‚˜๋Š” ๋‹ค์Œ ๋‘˜ ์ค‘์—์„œ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  1. ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ง„์งœ 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๋ฅผ ๋ฐ›๋„๋ก ๋งž์ถœ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. ์ปจํŠธ๋กค๋Ÿฌ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ImageAnalysisResponseV2<String>์œผ๋กœ ๋ณ€๊ฒฝ

    • VisionService์—์„œ ChatResponse๋ฅผ ํ•ญ์ƒ String์œผ๋กœ ๋ณ€ํ™˜ํ•ด ์“ฐ๊ฒ ๋‹ค๋Š” ๋ฐฉํ–ฅ์ด๋ผ๋ฉด, ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์™€ ์ œ๋„ค๋ฆญ ํƒ€์ž… ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ <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/null

Length of output: 139


๐Ÿ Script executed:

#!/bin/bash
# Find ImageAnalysisResponseV2.java
fd -e java -e kt | xargs rg -l "class ImageAnalysisResponseV2" 2>/dev/null

Length 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 -100

Length 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 kt

Length 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.java

Length of output: 7099


๐Ÿ Script executed:

#!/bin/bash
# Search for ImageAnalysisResponseV2 more broadly
rg -n "ImageAnalysisResponseV2" --type java | head -50

Length of output: 3761


๐Ÿ Script executed:

#!/bin/bash
# Search for ImageAnalysisResponseV2 class definition
rg -n "class ImageAnalysisResponseV2" --type java

Length 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.java

Length 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.

Comment on lines +38 to 99
@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
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

๐Ÿงฉ 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/๋ฌธ์ž์—ด ๋‘ ๊ฐœ๋…์„ ๋’ค์„ž๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๋ณด๋‹ค ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด ๋‘ ๊ฐ€์ง€ ์ค‘ ํ•˜๋‚˜๋กœ ์ •๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค:

  1. 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 ๊ฐ€ ๋งž์•„ ๋–จ์–ด์ง‘๋‹ˆ๋‹ค.
  1. ํ˜น์€ 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 -5

Length 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 java

Length of output: 10296


๐Ÿ Script executed:

#!/bin/bash
# analyzeChart ๋ฉ”์„œ๋“œ ํ™•์ธ
rg -nA 20 'public.*analyzeChart\s*\(' --type java

Length of output: 6798


๐Ÿ Script executed:

#!/bin/bash
# promptWithResponseFormat ๋ฉ”์„œ๋“œ ํ™•์ธ
rg -nA 20 'private.*promptWithResponseFormat\s*\(' --type java

Length 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 -20

Length of output: 2754


๐Ÿ Script executed:

#!/bin/bash
# DEFAULT_CHAT_RESPONSE_CLASS ์ •์˜ ํ™•์ธ
rg -n 'DEFAULT_CHAT_RESPONSE_CLASS' --type java

Length of output: 709


๐Ÿ Script executed:

#!/bin/bash
# compareImages ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ํ™•์ธ
rg -nA 25 'public.*compareImages\s*\(' --type java

Length 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 java

Length of output: 153


๐Ÿ Script executed:

#!/bin/bash
# ChatResponse instanceof ์ฒดํฌ ํ›„ String ์บ์ŠคํŒ… ํŒจํ„ด
rg -nA 5 'instanceof ChatResponse' --type java

Length of output: 686


๐Ÿ Script executed:

#!/bin/bash
# SimpleClassName ๋ฌธ์ž์—ด ๋น„๊ต ํŒจํ„ด (promptWithResponseFormat์—์„œ)
rg -n 'getSimpleName|ChatResponse.*string' --type java

Length of output: 432


๐Ÿ Script executed:

#!/bin/bash
# VisionServiceV2์™€์˜ ๋น„๊ต๋ฅผ ์œ„ํ•ด compareImages ํ˜ธ์ถœ ํŒจํ„ด
rg -nB 5 -A 15 'compareImages' --type java | head -50

Length 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 -20

Length of output: 934


๐Ÿ Script executed:

#!/bin/bash
# analyzeImage ๋ฉ”์„œ๋“œ ์„œ๋ช… ํ™•์ธ
rg -n 'analyzeImage\s*\(' --type java | head -10

Length 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 token

Length 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 java

Length 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 -20

Length 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 -50

Length 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 java

Length of output: 191


๐Ÿ Script executed:

#!/bin/bash
# ChatResponse ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ํ™•์ธ
rg -nA 5 'getMetadata|getUsage' --type java | head -40

Length of output: 3801


๐Ÿ Script executed:

#!/bin/bash
# ํ˜ธ์ถœ๋ถ€์—์„œ ๋ถ„์„ ์‘๋‹ต ์‚ฌ์šฉ ๋ฐฉ์‹ ํ™•์ธ
rg -nB 3 -A 3 '\.analysis\(\)' --type java | head -50

Length of output: 4023


๐Ÿ Script executed:

#!/bin/bash
# ์ „์ฒด ๊ตฌ์กฐ๋ฅผ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด ์ฃผ์š” ํŒŒ์ผ๋“ค ๋‚˜์—ด
find . -type f -name "*.java" | grep -i vision | head -20

Length of output: 220


๐Ÿ Script executed:

#!/bin/bash
# Spring AI ๊ด€๋ จ ChatResponse import ํ™•์ธ
rg -n 'import.*ChatResponse' --type java

Length of output: 864


๐Ÿ Script executed:

#!/bin/bash
# UserMessage ๋นŒ๋” ํŒจํ„ด ํ™•์ธ
rg -nA 8 'UserMessage\.builder' --type java

Length of output: 3820


๐Ÿ Script executed:

#!/bin/bash
# ํƒ€์ž… ์•ˆ์ „ ๋ฌธ์ œ ์žˆ์„ ์ˆ˜ ์žˆ๋Š” ์บ์ŠคํŒ… ํŒจํ„ด
rg -n '\(T\).*analysis|\(String\).*response\.analysis' --type java

Length of output: 223


๐Ÿ Script executed:

#!/bin/bash
# compareImages์—์„œ "image/jpeg" ํ•˜๋“œ์ฝ”๋”ฉ ํ™•์ธ
rg -n 'image/jpeg|JPEG' --type java

Length 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 f

Length 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
fi

Length 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/null

Length of output: 49


๐Ÿ Script executed:

#!/bin/bash
# ๋ชจ๋“  java ํŒŒ์ผ์—์„œ analyzeImage ๊ฒ€์ƒ‰
rg 'def analyzeImage|public.*analyzeImage' --type java -A 25

Length 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"; done

Length of output: 7177


๐Ÿ Script executed:

#!/bin/bash
# ๋Œ€์•ˆ: ์ „์ฒด ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ํ™•์ธ
ls -la && echo "---" && find . -maxdepth 3 -type f -name "*Vision*" 2>/dev/null

Length of output: 1198


ChatResponse ๋ถ„๊ธฐ์—์„œ ์ œ๋„ค๋ฆญ ํƒ€์ž…๊ณผ ์‹ค์ œ payload ํƒ€์ž… ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ฌธ์ œ ํ™•์ธ

๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฆฌ๋ทฐ ์ฝ”๋ฉ˜ํŠธ์˜ ์ง€์ ์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ critical issue๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. analyzeImage ๋ฉ”์„œ๋“œ (๋ผ์ธ 84): referenceType == ChatResponse.class์ผ ๋•Œ String analysis๋ฅผ (T) analysis๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜ธ์ถœ๋ถ€(VisionController)์—์„œ ImageAnalysisResponseV2<ChatResponse>๋ฅผ ๊ธฐ๋Œ€ํ•˜์ง€๋งŒ, ์‹ค์ œ ๋‚ด๋ถ€ ํƒ€์ž…์€ String์ด ๋˜์–ด ๋Ÿฐํƒ€์ž„์— ClassCastException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  2. analyzeChart ๋ฉ”์„œ๋“œ (๋ผ์ธ 143-144): raw ํƒ€์ž… ImageAnalysisResponseV2 ์‚ฌ์šฉํ•˜๋ฉด์„œ DEFAULT_CHAT_RESPONSE_CLASS๋ฅผ ๋„˜๊ธฐ๊ณ , (String) response.analysis()๋กœ ๊ฐ•์ œ ์บ์ŠคํŒ…ํ•ฉ๋‹ˆ๋‹ค.

์ œ์•ˆ๋œ ์ˆ˜์ • ๋ฐฉ์‹์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ํƒ€์ž…๊ณผ ์ œ๋„ค๋ฆญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ผ์น˜์‹œํ‚ค๋ ค๋ฉด:

  • analyzeImage์—์„œ ChatResponse ๋ถ„๊ธฐ ์‹œ String์„ ์ถ”์ถœํ•œ ํ›„ ์‹ค์ œ๋กœ๋Š” String ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๊ณ 
  • analyzeChart๋Š” String.class๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

์ถ”๊ฐ€๋กœ promptWithResponseFormat์˜ getSimpleName() ๋ฌธ์ž์—ด ๋น„๊ต(๋ผ์ธ 208)๋„ Class.equals() ๋น„๊ต๋กœ ๊ฐœ์„ ํ•˜๋ฉด ๋”์šฑ ๊ฒฌ๊ณ ํ•ด์ง‘๋‹ˆ๋‹ค.

Comment on lines +65 to +67
String rawText = chatResponse.getResult().getOutput().getText();
log.debug("Vision API ์›๋ณธ ์‘๋‹ต: {}", rawText);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ 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 java

Length 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 java

Length 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 yaml

Length 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.

@jinho-yoo-jack

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

์ด PR์€ Vision ์„œ๋น„์Šค๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜์—ฌ ์ผ๊ด€๋œ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ๋„์ž…ํ•˜๊ณ  ์ƒˆ๋กœ์šด ์˜์ˆ˜์ฆ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์ค‘์š”ํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ImageAnalysisResponseV2 DTO๋ฅผ ๋„์ž…ํ•˜์—ฌ API ์‘๋‹ต์„ ํ‘œ์ค€ํ™”ํ•œ ์ ์€ ๋งค์šฐ ์ข‹์€ ๊ฐœ์„ ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ฆฌํŒฉํ† ๋ง๋œ VisionService์˜ analyzeImage ๋ฉ”์„œ๋“œ์— ์‹ฌ๊ฐํ•œ ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠน์ • ์กฐ๊ฑด์—์„œ ClassCastException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ตฌ์กฐํ™”๋œ ์‘๋‹ต์„ ๋ฐ›์„ ๋•Œ ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์†์‹ค๋˜๋Š” ์„ค๊ณ„ ๊ฒฐํ•จ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

PR์— ํ•จ๊ป˜ ์ถ”๊ฐ€๋œ VisionServiceV2.java ํŒŒ์ผ์€ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•œ ๋” ๋‚˜์€ ๊ตฌํ˜„์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ์ด ์„œ๋น„์Šค๋Š” ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์ง€ ์•Š์ง€๋งŒ, ๊ธฐ์กด VisionService์˜ ๋ถˆ์•ˆ์ •ํ•œ ๋กœ์ง์„ VisionServiceV2์˜ ๊ฒฌ๊ณ ํ•œ ๋กœ์ง์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๊ทธ ์™ธ์—๋„ ์ปจํŠธ๋กค๋Ÿฌ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹๊ณผ ์ผ๋ถ€ ์„œ๋น„์Šค์˜ ๋ฏธ์‚ฌ์šฉ ์ฝ”๋“œ ๋“ฑ ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์˜ ๊ฐœ๋ณ„ ์ฝ”๋ฉ˜ํŠธ์—์„œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

Comment on lines +22 to +24
public static ImageAnalysis of(ImageAnalysisRequest request) throws IOException {
return ImageAnalysis.of(request.prompt(), request.image());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

ImageAnalysisRequest ํƒ€์ž…์ด ์ด ํŒŒ์ผ์— ์ •์˜๋˜์–ด ์žˆ๊ฑฐ๋‚˜ import๋˜์ง€ ์•Š์•„ ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด ํด๋ž˜์Šค์˜ ์ •์˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ผ๋ฉด ์‚ญ์ œํ•ด์ฃผ์„ธ์š”.

Comment on lines +68 to 88
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
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

์—ฌ๊ธฐ์— ์‹ฌ๊ฐํ•œ ๋ฒ„๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๊ฐ€ 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— NoSuchFieldException๊ณผ ๊ฐ™์€ ํŠน์ • ๊ตฌํ˜„ ์˜ˆ์™ธ๋ฅผ ๋…ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹™๋‹ˆ๋‹ค. ์ด ์˜ˆ์™ธ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , @ControllerAdvice ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ 400 Bad Request ๋˜๋Š” 500 Internal Server Error์™€ ๊ฐ™์€ ์ ์ ˆํ•œ HTTP ์ƒํƒœ ์ฝ”๋“œ๋กœ ๋งคํ•‘ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. API ๊ณ„์•ฝ์ด ๋‚ด๋ถ€ ์„œ๋น„์Šค ๊ตฌํ˜„์— ์ข…์†๋˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Comment on lines +90 to 98
} else {
// String ํƒ€์ž…์ธ ๊ฒฝ์šฐ
return ImageAnalysisResponseV2.of(
response,
contentType,
imageBytes.length,
tokenUsage
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

์ด else ๋ธ”๋ก์—์„œ๋Š” ์‘๋‹ต์ด entity๋กœ ์ง์ ‘ ๋ณ€ํ™˜๋  ๋•Œ(ReceiptData ๋“ฑ) tokenUsage๊ฐ€ ํ•ญ์ƒ null์ด ๋ฉ๋‹ˆ๋‹ค. ChatClient์—์„œ .entity(responseType)๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ChatResponse ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์ค‘์š”ํ•œ ์ •๋ณด ์†์‹ค๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์ถ”๊ฐ€๋œ VisionServiceV2.java ํŒŒ์ผ์˜ ๊ตฌํ˜„์€ ChatResponse๋ฅผ ๋จผ์ € ๋ฐ›์€ ํ›„ ๋‚ด์šฉ์„ ํŒŒ์‹ฑํ•จ์œผ๋กœ์จ ์ด ๋ฌธ์ œ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•ด๊ฒฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Comment on lines 85 to +87
public ResponseEntity<Map<String, String>> compareImages(
@RequestParam("image1") MultipartFile image1,
@RequestParam("image2") MultipartFile image2) {
@RequestParam("image1") MultipartFile image1,
@RequestParam("image2") MultipartFile image2) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

์ด compareImages ๋ฉ”์„œ๋“œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด ๋‹ค๋ฅธ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ(analyzeImage, extractText ๋“ฑ)์™€ ๋‹ฌ๋ฆฌ throws IOException์„ ์„ ์–ธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ผ๊ด€์„ฑ ์—†๋Š” ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. visionService.compareImages๊ฐ€ IOException์„ RuntimeException์œผ๋กœ ๋ž˜ํ•‘ํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด visionService.compareImages๊ฐ€ IOException์„ ์ง์ ‘ ๋˜์ง€๋„๋ก ์ˆ˜์ •ํ•˜๊ณ  ์—ฌ๊ธฐ์„œ๋„ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

@Slf4j
public class ReceiptAnalysisService {
private final VisionService visionService;
private final ObjectMapper objectMapper;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

์ฃผ์ž…๋œ ObjectMapper ํ•„๋“œ๊ฐ€ ํด๋ž˜์Šค ๋‚ด์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜ผ๋™์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ์ด ๋ฏธ์‚ฌ์šฉ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

private final ObjectMapper objectMapper;


public ImageAnalysisResponseV2<ReceiptData> processReceipt(MultipartFile imageFile) throws IOException, NoSuchFieldException {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

์ด ๋ฉ”์„œ๋“œ๋Š” ์‹œ๊ทธ๋‹ˆ์ฒ˜์— throws NoSuchFieldException์„ ์„ ์–ธํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์‹ค์ œ ๋ฉ”์„œ๋“œ ๋ณธ๋ฌธ์—์„œ๋Š” ์ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (์ด ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” cleanJsonResponse๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Œ). ์ด๋Š” ์˜คํ•ด์˜ ์†Œ์ง€๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ throws ์ ˆ์—์„œ NoSuchFieldException์„ ์ œ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Comment on lines +56 to +88
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ํด๋ž˜์Šค ํƒ€์ž…์„ ๋‹จ์ˆœ ์ด๋ฆ„ ๋ฌธ์ž์—ด("ChatResponse")๋กœ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ์•ˆ์ •ํ•ฉ๋‹ˆ๋‹ค. ํด๋ž˜์Šค ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ํŒจํ‚ค์ง€์— ๋™์ผํ•œ ์ด๋ฆ„์˜ ํด๋ž˜์Šค๊ฐ€ ์ƒ๊ธฐ๋ฉด ์ฝ”๋“œ๊ฐ€ ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ํด๋ž˜์Šค๋ฅผ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•˜๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

Suggested change
if ("ChatResponse".equals(responseTypeClassName)) return (T) promptByChatResponse(userMessage);
if (responseType == ChatResponse.class) return (T) promptByChatResponse(userMessage);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant