Skip to content

Refactor: Json파싱 표준화로 MCP Servie 리팩토링#412

Merged
HeeMang-Lee merged 3 commits intodevfrom
refactor/411
Aug 13, 2025
Merged

Refactor: Json파싱 표준화로 MCP Servie 리팩토링#412
HeeMang-Lee merged 3 commits intodevfrom
refactor/411

Conversation

@HeeMang-Lee
Copy link
Member

@HeeMang-Lee HeeMang-Lee commented Aug 13, 2025

🔎 작업 내용

  • BraveSearchRagService : 먼저 구조화된 결과(url/title/description)를 우선 처리하고 없으면 기존 “Title:, URL:” 텍스트 포맷 파서로 fallback
  • BraveSearchMcpService : listTools() 호출 시 초기화 에러면 initialize()로 복구
    callTool(...).content()에서 type=text의 text 값을 꺼내 JSON으로 재파싱
    결과를 통일된 results: [ {url,title,description} ... ] 형태로 반환

Summary by CodeRabbit

  • New Features

    • 검색 결과를 항상 단일 "results" 배열로 표준화해 일관된 출력 제공
    • 각 결과에서 url/title/description을 우선 추출하고, 비어있으면 내부 JSON 또는 레거시 블록을 해석해 단일 문서로 생성
    • 결과별 단건 문서 생성 흐름으로 불필요한 누적 제거
  • Bug Fixes

    • Brave 도구 초기화 오류 감지 시 자동 초기화 후 재시도로 실패 감소
    • 중첩 JSON·비정형 레거시 형식에 대한 재귀적 정규화 강화로 누락·깨짐 개선
    • 빈 항목은 건너뛰어 무의미한 결과 방지

@HeeMang-Lee HeeMang-Lee linked an issue Aug 13, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Walkthrough

Brave MCP 응답을 항상 {"results":[...]} 형태로 정규화하는 JSON 파이프라인(재귀 깊이 제어, legacy 파싱 포함)을 추가하고, MCP 클라이언트 탐색/초기화 실패에 대해 재시도 및 오류 처리를 강화했습니다. RAG 변환은 각 result를 개별 Document로 변환하도록 단순화되었습니다.

Changes

Cohort / File(s) Summary of changes
Brave MCP 정규화 및 오류 처리
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java
- 모든 MCP 응답을 {"results":[...]} 형태로 정규화하도록 변경
- JSON 정규화 파이프라인 추가: MAX_JSON_RECURSION_DEPTH(=3), looksLikeJson, truncate, getTrimmed, normalizeContent/normalizeOne, parseJsonBlockIntoResults, addObjectToResults, parseLegacyBlock 등 구현
- listTools() 호출에 McpError 처리 추가: "initialized" 관련 메시지일 경우 initialize() 한 번 호출 후 재시도, 그 외 오류는 로깅 후 진행 또는 적합한 Brave 툴 미발견 시 IllegalStateException 발생
- 로깅을 raw pretty-print에서 JsonNode 직접 로깅으로 변경하고 정규화 완료 시 한국어로 결과 건수 로깅
- ArrayNode/ObjectNode, McpError, Locale 등 import/유틸 추가
RAG 변환(문서 생성) 단순화
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java
- toDocuments: root.path("results") 배열을 순회하며 각 result를 개별 처리하도록 변경
- 각 result에서 우선적으로 url, title, description 추출; description에서 HTML 제거; 모두 빈 경우 text 필드를 JSON으로 파싱하거나 legacy Title:/URL: 블록 파싱 시도
- url/title/description이 모두 비어있으면 해당 result는 건너뜀; Document id/body/metadata 결정 로직 단순화
- ObjectMapper 및 헬퍼 메서드(getText, isBlank, looksLikeJson, parseLegacyBlock, ParsedLegacy) 추가

Sequence Diagram(s)

sequenceDiagram
  participant App as Caller
  participant Svc as BraveSearchMcpService
  participant MCP as Brave MCP Client

  App->>Svc: search(query)
  Svc->>MCP: listTools()
  alt McpError contains "initialized"
    Svc->>MCP: initialize()
    Svc->>MCP: listTools() 재시도
  end
  Svc->>MCP: call BRAVE_WEB_TOOL
  MCP-->>Svc: raw JsonNode / 텍스트
  Svc->>Svc: normalizeContent (looksLikeJson, 재귀 parseJsonBlockIntoResults, parseLegacyBlock)
  Svc-->>App: {"results":[{url,title,description}, ...]}
Loading
sequenceDiagram
  participant App as Caller
  participant Rag as BraveSearchRagService

  App->>Rag: toDocuments(Optional<JsonNode>)
  alt results not array
    Rag-->>App: []
  else results array
    loop for each result
      Rag->>Rag: extract url, title, description
      Rag->>Rag: description HTML 제거 / looksLikeJson 검사 / legacy 파싱
      alt url/title/description 존재
        Rag->>Rag: create Document (id=url||title, body=description||title||url, metadata title/url)
      else
        Rag-->>Rag: skip
      end
    end
    Rag-->>App: List<Document>
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • jong-0126
  • crocusia
  • Kimyoonbeom
  • Ksr-ccb
  • wannabeing

Poem

오늘도 토끼가 홉홉, JSON 냄새 맡고 🐇
블록을 쪼개고 합쳐서 결과를 모아요
title, url, description 한 그릇에 담아
RAG엔 문서로, 검색엔 배열로 즐겁게 hop!
로그엔 한국어로 숫자 세며 깡총깡총.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/411

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (5)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (3)

39-41: 로그 레벨을 DEBUG로 변경해야 합니다

운영 환경에서 모든 검색 응답의 전체 내용을 INFO 레벨로 로깅하면 로그 볼륨이 과도하게 증가할 수 있습니다. 디버깅 목적이라면 DEBUG 레벨이 더 적절합니다.

-        log.info("[Brave MCP Response Raw content]: {}", raw.toPrettyString());
+        log.debug("[Brave MCP Response Raw content]: {}", raw.toPrettyString());

114-117: JSON 검증 로직이 불완전합니다

현재 구현은 단순히 시작과 끝 문자만 확인하는데, "{invalid json}" 같은 잘못된 형식도 JSON으로 판단할 수 있습니다. 하지만 실제 파싱은 parseJsonBlockIntoResults에서 try-catch로 처리하므로 큰 문제는 없습니다.


200-208: 조건 검사가 불완전합니다

flushOne 메서드는 title과 url이 모두 존재하고 body가 있을 때만 결과를 추가합니다. 하지만 body가 없어도 title과 url만으로 유효한 검색 결과가 될 수 있습니다.

     private void flushOne(ArrayNode out, String title, String url, StringBuilder body) {
-        if (title != null && url != null && body.length() > 0) {
+        if (title != null && url != null) {
             ObjectNode one = objectMapper.createObjectNode();
             one.put("title", title);
             one.put("url", url);
-            one.put("description", body.toString().trim());
+            String description = body.toString().trim();
+            if (!description.isEmpty()) {
+                one.put("description", description);
+            }
             out.add(one);
         }
     }
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (2)

34-36: null 처리 로직 개선 필요

title이 null이거나 빈 값일 때 url을 사용하고, url도 없으면 "Web result"를 사용하는 로직이 좋습니다. 하지만 url이 빈 문자열인 경우에도 docTitle로 사용될 수 있습니다.

                     String docTitle = (title != null && !title.isBlank())
-                        ? title : (url != null ? url : "Web result");
+                        ? title : (url != null && !url.isBlank() ? url : "Web result");

40-40: 메타데이터의 일관성 확인

url이 null일 때 빈 문자열로 설정하는 것은 좋은 방어 코딩입니다. 하지만 docTitle과 metadata의 title이 다를 수 있습니다 (docTitle은 url이나 "Web result"가 될 수 있지만 metadata의 title은 항상 docTitle).

                     documents.add(new Document(
                         docTitle,
                         body,
-                        Map.of("title", docTitle, "url", url == null ? "" : url)
+                        Map.of("title", title != null ? title : "", "url", url == null ? "" : url)
                     ));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36d8b00 and e32a8a6.

📒 Files selected for processing (2)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (3 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (1 hunks)
🔇 Additional comments (1)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (1)

56-59: JSON 파싱 시도 전 빈 문자열 검사가 중복됩니다

looksLikeJson 메서드는 이미 내부에서 trim()을 수행하므로, 빈 문자열은 JSON 형태가 아닌 것으로 판단됩니다. 위의 52-53 라인에서 이미 빈 문자열을 검사했으므로 이 부분은 항상 false가 되지 않습니다.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (2)

198-220: URL 스킴 검증으로 잠재적 스니펫/자바스크립트 URL 필터링 권장

javascript:, data: 등 비의도적 스킴을 간단히 배제하면 후속 RAG 파이프라인의 안전성이 좋아집니다.

예시:

         String url = getTrimmed(obj, "url");
         String title = getTrimmed(obj, "title");
         String desc = getTrimmed(obj, "description");
 
         // 세 필드 중 하나라도 있으면 결과로 채택
-        if ((url != null && !url.isBlank()) ||
+        boolean validUrl = url != null && !url.isBlank()
+            && (url.startsWith("http://") || url.startsWith("https://"));
+        if (validUrl ||
             (title != null && !title.isBlank()) ||
             (desc != null && !desc.isBlank())) {
 
             ObjectNode one = objectMapper.createObjectNode();
-            if (url != null) {
+            if (validUrl) {
                 one.put("url", url);
             }

57-78: 에러/엣지 케이스용 단위 테스트 추가 제안

중첩 JSON, {results: [...]}, text 래핑, 레거시 Title/URL, 빈/노이즈 입력 등 케이스별 유닛 테스트가 있으면 회귀 방지에 유리합니다. 필요하시면 테스트 스캐폴딩을 제공하겠습니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (5)

29-31: text 폴백은 현재 경로에선 도달 불가에 가깝습니다

MCP에서 text를 제거하고 description으로 표준화하므로, 이 분기는 방어적 코드 이상으로 쓰이지 않을 가능성이 큽니다. 유지 자체는 해가 없지만, 복잡도 축소 차원에서 제거하거나 주석으로 “디버그용 폴백”임을 명시해도 좋습니다.


34-36: HTML 제거 정규식은 간단/안전하지만 엔티티는 남습니다 — 선택적으로 디코딩 고려

필요 시 &amp;, &lt; 등 엔티티 디코딩까지 수행하면 가독성이 좋아집니다. 의존성 추가가 부담되면 소수 엔티티만 수동 치환해도 충분합니다.

예시:

description = description
    .replaceAll("<[^>]+>", "")
    .replace("&amp;", "&")
    .replace("&lt;", "<")
    .replace("&gt;", ">");

21-22: results가 배열이 아닐 때의 진단 로그 추가 제안

입력 구조가 예상과 다를 때 조용히 무시되면 디버깅이 어렵습니다. warn 레벨로 한 줄 남기면 운용성이 좋아집니다.

예시:

-            if (results != null && results.isArray()) {
+            if (results != null && results.isArray()) {
                 results.forEach(r -> {
                     // ...
                 });
-            }
+            } else {
+                // 예상 외 구조 진단용
+                if (results != null) {
+                    // 길이 제한하여 로그
+                    String preview = results.toString();
+                    if (preview.length() > 1000) preview = preview.substring(0, 1000) + "…";
+                    // 필요 시 debug로 낮추셔도 됩니다.
+                    org.slf4j.LoggerFactory.getLogger(BraveSearchRagService.class)
+                        .warn("BraveSearchRagService: results 배열이 아닙니다: {}", preview);
+                }
+            }

24-41: 사소한 정제: 트리밍으로 공백 노이즈 제거

title/url/description에 앞뒤 공백이 포함될 수 있습니다. body 조합 전 간단히 trim하면 결과 품질이 개선됩니다.

예시:

-                    String title = r.path("title").asText("");
-                    String url = r.path("url").asText("");
-                    String description = r.path("description").asText("");
+                    String title = r.path("title").asText("").trim();
+                    String url = r.path("url").asText("").trim();
+                    String description = r.path("description").asText("").trim();

16-50: 유닛 테스트 보강 제안

다음 케이스를 포함한 toDocuments 테스트를 제안합니다.

  • 정상 케이스: url/title/description 조합 다양성
  • description 공백 시 title 폴백
  • HTML 포함/제거
  • results가 배열이 아닌 경우 처리
  • url만 존재하는 항목의 스킵/포함 정책 확인

필요하시면 테스트 템플릿을 제공하겠습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e32a8a6 and 97e4dae.

📒 Files selected for processing (2)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (3 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (1 hunks)
🔇 Additional comments (3)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (2)

165-169: 재귀 깊이 제한 추가 훌륭합니다

중첩 문자열화 JSON 방어를 위해 최대 깊이 제한을 둔 점이 안정성 측면에서 적절합니다.


119-134: 정규화 파이프라인의 형태 다양성 처리 좋습니다

배열/객체/문자열(JSON 포함)의 다양한 입력 형태를 흡수하여 단일 results로 수렴시키는 접근이 명확합니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (1)

24-27: 정규화된 필드(title/url/description) 우선 사용으로 일관성 확보

BraveSearchMcpService의 표준화 결과와 정합적입니다. 과거 text 필드 의존 문제를 해소했습니다.

Comment on lines +67 to 69
JsonNode raw = objectMapper.valueToTree(result.content());
log.info("[Brave MCP Response Raw content]: {}", raw);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

과도한 원본 응답 로그는 크기/민감도 리스크가 있습니다 — 로그 레벨 하향 및 트렁케이션 제안

대용량/다중 중첩 응답이 그대로 info 로그에 남습니다. 디버깅 구간에서만 필요하므로 debug 레벨과 길이 제한을 권장합니다.

아래와 같이 수정을 제안합니다.

-        JsonNode raw = objectMapper.valueToTree(result.content());
-        log.info("[Brave MCP Response Raw content]: {}", raw);
+        JsonNode raw = objectMapper.valueToTree(result.content());
+        log.debug("[Brave MCP Response Raw content]: {}",
+            truncate(raw != null ? raw.toString() : "null", 2000));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonNode raw = objectMapper.valueToTree(result.content());
log.info("[Brave MCP Response Raw content]: {}", raw);
JsonNode raw = objectMapper.valueToTree(result.content());
log.debug("[Brave MCP Response Raw content]: {}",
truncate(raw != null ? raw.toString() : "null", 2000));
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java
around lines 67-69, the code currently logs the full raw JsonNode at info level
which risks large and sensitive output; change this to emit at debug level and
truncate the serialized JSON to a safe max length (e.g. 1000 chars) to avoid
huge logs and leakage. Implement by checking logger.isDebugEnabled() before
serializing, convert the JsonNode to a string, truncate it to the configured max
length (append "…(truncated)" when shortened), and then call log.debug with the
truncated string instead of log.info; ensure no heavy work runs when debug
logging is disabled.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (1)

67-68: 원본 MCP 응답을 info로 전체 로그 → debug + 트렁케이션 권장

대용량 또는 민감 정보가 포함될 수 있는 원본 응답을 info 레벨로 그대로 출력하면 로그 폭증/민감도 리스크가 큽니다. debug로 내리고 길이 제한을 두세요.

아래처럼 변경을 제안합니다.

-        JsonNode raw = objectMapper.valueToTree(result.content());
-        log.info("[Brave MCP Response Raw content]: {}", raw);
+        JsonNode raw = objectMapper.valueToTree(result.content());
+        if (log.isDebugEnabled()) {
+            String rawStr = raw != null ? raw.toString() : "null";
+            log.debug("[Brave MCP Response Raw content]: {}", truncate(rawStr, 2000));
+        }
🧹 Nitpick comments (2)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (1)

65-78: callTool 예외 전파 시 컨텍스트를 보강해 주세요 (선택사항)

외부 호출 실패 시 상위로 예외를 그대로 전파하면 호출자 측 트러블슈팅이 어렵습니다. 로그에 query/count/offset 등 최소 컨텍스트를 포함하고 IllegalStateException으로 래핑해 재던지길 권장합니다. 위의 로그 레벨/트렁케이션 변경과 충돌을 피하려면 한 PR 안에서 순차 반영하세요.

cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (1)

29-39: text 필드의 JSON-fallback 처리 합리적이나 results 객체도 고려 가능

현재는 text가 JSON 객체일 때 url/title/description만 추출합니다. 드물게 text가 {"results":[...]} 형태일 수 있으므로, 이 경우 첫 요소나 전체를 병합하는 전략을 추가하면 복원력이 더 좋아집니다. 필요 시 알려주시면 패치 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97e4dae and 2b18ded.

📒 Files selected for processing (2)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (3 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (2 hunks)
🔇 Additional comments (5)
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java (2)

90-112: 미초기화 감지 및 1회 재시도 로직은 타당합니다

에러 메시지 기반 미초기화 판별, initialize 1회 재시도, 실패 시 다음 클라이언트로 진행하는 흐름이 합리적입니다. 로그 레벨과 메시지도 적절합니다.


122-137: 정규화 파이프라인 진입점 구조가 명확합니다

any-shape JSON을 배열/단일 케이스로 분기해 normalizeOne으로 위임하는 방식이 간결하고 확장 가능한 형태입니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java (3)

22-27: 표준 필드 우선 추출 흐름이 목적(결과 표준화)에 부합합니다

url/title/description을 1차로 읽고 이후 단계에서만 보강하는 전략이 깔끔합니다.


55-64: Document id 우선순위(URL > title)와 body 구성 로직이 적절합니다

중복/변동 가능성이 큰 title을 지양하고 URL을 우선 id로 사용하는 선택이 실무적으로 안정적입니다. description이 없을 때 title/url로 대체하는 폴백도 합리적입니다.

Also applies to: 65-72


80-83: 보조 헬퍼들의 방어적 구현이 적절합니다 (텍스트형만 취급, trim, JSON 힌트 검사 등)

getText/isBlank/looksLikeJson의 보수적인 동작이 다운스트림 오류를 줄입니다. looksLikeJson은 과잉매칭 가능성이 있으나 현재 용도(폴백 트리거)에서는 수용 가능합니다.

Also applies to: 89-95

Comment on lines +139 to +161
private void normalizeOne(JsonNode node, ArrayNode out) {
if (node == null || node.isNull()) {
return;
}

// 이미 {url,title,description} 형태
if (node.isObject() && (node.has("url") || node.has("title") || node.has("description"))) {
addObjectToResults((ObjectNode) node, out);
return;
}

// MCP가 주는 content item: { "type":"text", "text":"{...json...}" } 같은 형태를 방어
if (node.isObject() && node.has("text")) {
String text = node.path("text").asText("");
if (looksLikeJson(text)) {
parseJsonBlockIntoResults(text, out, 0);
} else {
// "Title: ..." 라인 포맷 등 레거시 텍스트 포맷 처리(옵션)
parseLegacyBlock(text, out);
}
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

normalizeOne가 객체 루트의 "results" 배열을 직접 처리하지 않습니다

현재는 문자열 JSON으로 들어온 경우(parseJsonBlockIntoResults)만 루트에 results 배열을 순회합니다. 실제로 raw가 이미 객체(JSON)인 경우에도 results 배열을 지원하도록 분기를 추가해야 누락이 없습니다.

아래와 같이 분기를 추가해 주세요.

     private void normalizeOne(JsonNode node, ArrayNode out) {
         if (node == null || node.isNull()) {
             return;
         }

         // 이미 {url,title,description} 형태
         if (node.isObject() && (node.has("url") || node.has("title") || node.has("description"))) {
             addObjectToResults((ObjectNode) node, out);
             return;
         }
 
+        // 루트에 results 배열이 있는 객체 형태 처리
+        if (node.isObject() && node.has("results") && node.get("results").isArray()) {
+            for (JsonNode n : node.get("results")) {
+                normalizeOne(n, out);
+            }
+            return;
+        }
+
         // MCP가 주는 content item: { "type":"text", "text":"{...json...}" } 같은 형태를 방어
         if (node.isObject() && node.has("text")) {
             String text = node.path("text").asText("");
             if (looksLikeJson(text)) {
                 parseJsonBlockIntoResults(text, out, 0);
             } else {
                 // "Title: ..." 라인 포맷 등 레거시 텍스트 포맷 처리(옵션)
                 parseLegacyBlock(text, out);
             }
             return;
         }

Comment on lines +246 to +259
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
if (title != null && url != null && body.length() > 0) {
ObjectNode one = objectMapper.createObjectNode();
one.put("title", title);
one.put("url", url);
one.put("description", body.toString().trim());
out.add(one);
body.setLength(0);
}
title = trimmed.substring(6).trim();
} else if (trimmed.regionMatches(true, 0, "URL:", 0, 4)) {
url = trimmed.substring(4).trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

레거시 파서의 중간 flush 조건에서 본문(body) 유무를 요구하면 유효 결과가 누락됩니다

Title/URL만 있는 케이스도 결과로 인정해야 합니다. 중간 flush 시 body.length() > 0 조건을 제거하세요.

-            if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
-                if (title != null && url != null && body.length() > 0) {
+            if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
+                if (title != null && url != null) {
                     ObjectNode one = objectMapper.createObjectNode();
                     one.put("title", title);
                     one.put("url", url);
                     one.put("description", body.toString().trim());
                     out.add(one);
                     body.setLength(0);
                 }
                 title = trimmed.substring(6).trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
if (title != null && url != null && body.length() > 0) {
ObjectNode one = objectMapper.createObjectNode();
one.put("title", title);
one.put("url", url);
one.put("description", body.toString().trim());
out.add(one);
body.setLength(0);
}
title = trimmed.substring(6).trim();
} else if (trimmed.regionMatches(true, 0, "URL:", 0, 4)) {
url = trimmed.substring(4).trim();
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
if (title != null && url != null) {
ObjectNode one = objectMapper.createObjectNode();
one.put("title", title);
one.put("url", url);
one.put("description", body.toString().trim());
out.add(one);
body.setLength(0);
}
title = trimmed.substring(6).trim();
} else if (trimmed.regionMatches(true, 0, "URL:", 0, 4)) {
url = trimmed.substring(4).trim();
}
// ...
}
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java
around lines 246 to 259, the intermediate "flush" condition currently requires
body.length() > 0 which causes entries that have only Title and URL to be
dropped; remove the body.length() > 0 check so the block flushes whenever title
and url are present, and keep the existing behavior of creating the ObjectNode,
putting title/url/description (use body.toString().trim() which may be empty),
adding it to out, and resetting body with body.setLength(0).

Comment on lines +17 to +18
private final ObjectMapper objectMapper = new ObjectMapper();

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ObjectMapper는 직접 생성 대신 주입받으세요

테스트 용이성/설정 일관성을 위해 Spring 컨테이너에서 주입받는 편이 바람직합니다. 현재 @requiredargsconstructor가 있으므로 필드 초기화만 제거하면 됩니다.

-    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final ObjectMapper objectMapper;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final ObjectMapper objectMapper = new ObjectMapper();
- private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectMapper objectMapper;
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchRagService.java
around lines 17 to 18, the ObjectMapper is being instantiated directly which
hinders testability and central configuration; remove the new ObjectMapper()
initialization and rely on constructor injection (the class already uses
@RequiredArgsConstructor) by making the field final without an initializer so
Spring will inject the application-wide ObjectMapper bean.

@HeeMang-Lee HeeMang-Lee merged commit a34fbb6 into dev Aug 13, 2025
2 checks passed
@HeeMang-Lee HeeMang-Lee deleted the refactor/411 branch August 13, 2025 04:48
This was referenced Aug 13, 2025
@coderabbitai coderabbitai bot mentioned this pull request Aug 20, 2025
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.

Brave Search 검색은 성공하나 결과가 비어있는 버그

2 participants