Feat/sw 70 - AI 북마크 분석 로직 고도화 및 API 에러 처리 표준화#44
Conversation
## 변경사항 ### 1. 프롬프트 v4 (link-analysis-system-v4.md) - Step 1 기준을 "도메인(Domain) 레벨 일치"로 명확화 - 기존: "85% 이상 일치" (모호함) → 개선: "도메인이 완벽하게 일치" - 구체적 예제 추가 (Netflix vs 스포츠 폴더의 도메인 불일치) - Netflix 같은 사례에서 표면 특성(스트리밍)이 아닌 핵심 도메인(OTT)으로 판단하도록 유도 ### 2. 코드 개선 (LinkAnalysisServiceImpl.java) - 새 메서드 추가: inferCategoryFromTitles(List<String> titles) * 샘플 제목들에서 자동으로 폴더의 주요 카테고리 추론 * 키워드 기반 분류: OTT, AI도구, 개발, 디자인, 스포츠, 협업, 콘텐츠 * 점수 기반 우선순위: 50% 이상 단일 카테고리, 그 외 혼합 (예: "AI도구 & OTT") ### 3. 폴더 컨텍스트 블록 개선 - 기존: "- AI (3개, 최근 2026-04-27) 최근 저장: [...]" - 개선: "- AI (3개, 최근 2026-04-27, 주요 카테고리: AI도구 & OTT) 최근 저장: [...]" - AI가 폴더의 의미적 성격을 명확히 인지 가능 ## 효과 - Netflix → 스포츠(오류) 제거 - Netflix → OTT(정확) 추천 - 다른 서비스도 도메인 기반 정확 분류 기대 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Step 1을 더 엄격하게 ('애매하면 NO')
- Step 2 실행을 의무화 (Step 1 = NO일 때)
- suggestedTags에서 도메인 태그 추출 방법 구체화
- Netflix → 스포츠 오분류 제거 기대
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
LinkAnalysisServiceImpl: 추천 태그 매칭 로직 고도화 및 미분류 폴더 자동 보정 로직 구현. FolderContext / BookmarkDao: AI 문맥 정보 강화를 위한 폴더별 상위 태그 빈도수 데이터 연동. application.properties: 프롬프트 v4 설정 및 리소스 경로 다각화 반영. prompts/*.md: Identity-First 전략 및 최신 서비스 예시가 반영된 기능성 프롬프트 리소스 구축. LinkMetadataExtractor: 불필요한 콘텐츠 유형 추출 로직 제거 및 코드 최적화.
fetchClient.ts: ApiError 클래스 도입 및 표준 에러 메시지 추출 함수 구현을 통한 예외 처리 체계 구축. QueryProvider.tsx: 전역 Mutation 에러 핸들링 고도화를 통해 사용자에게 의미 있는 에러 메시지 제공. SaveLinkDialog.tsx: 재분석 시 AI 추천 태그 중복 축적 버그 해결 및 상태 초기화 로직 수정.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Walkthrough이 변경은 AI 기반 링크 분석 기능을 향상시킵니다. 프론트엔드에서는 API 오류 처리를 구조화하고, 백엔드에서는 사용자 폴더별 문맥을 조회하여 AI 프롬프트에 통합합니다. 새로운 Sequence DiagramsequenceDiagram
participant User as 사용자
participant Frontend as Frontend<br/>(SaveLinkDialog)
participant Service as LinkAnalysisServiceImpl
participant DAO as BookmarkDao
participant DB as Database
participant Extractor as LinkMetadataExtractor
participant AI as OpenAI API
User->>Frontend: 링크 저장 및 AI 분석 요청
Frontend->>Service: analyze(url, memberId, ...)
Service->>Extractor: extract(url)
Extractor-->>Service: PageContent<br/>(title, description,<br/>keywords, headings)
Service->>DAO: selectFolderContexts<br/>(memberId, sampleLimit, tagLimit)
DAO->>DB: SELECT folder contexts<br/>with aggregated stats
DB-->>DAO: List<Map<String, Object>>
DAO-->>Service: List<FolderContext>
Service->>Service: buildPrompt<br/>(pageContent, folders,<br/>tags, folderContexts)
Note over Service: folderContext 있으면<br/>enrichedUserPromptTemplate<br/>사용, 아니면<br/>basicUserPromptTemplate 사용
Service->>AI: POST /chat/completions<br/>(enriched prompt)
AI-->>Service: JSON response<br/>(title, description,<br/>suggestedTags, suggestedFolder)
Service->>Service: parseAndEnrich<br/>(response)
Note over Service: 태그 매칭, 폴더 재매핑,<br/>미분류 처리
Service-->>Frontend: AI 분석 결과
Frontend->>Frontend: aiSuggestedTags 업데이트<br/>(모든 AI 제안 태그 추적)
Frontend-->>User: 분석 완료, 태그/폴더 제안
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 833500fab1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Track ALL AI-suggested tags (both existing and new) for cleanup on re-analysis | ||
| aiTagNames.add(tag.tagName); |
There was a problem hiding this comment.
Preserve manually selected tags across re-analysis
aiSuggestedTags now records every AI-suggested tag, including tags the user had already selected manually before analysis. On the next analysis pass, baseTags removes all tags in prevAiTags, so any manual tag that happened to be suggested by AI gets dropped unexpectedly. This regression occurs when a user preselects a tag (e.g., an existing tag), runs AI analysis, then re-runs analysis; the preselected tag is treated as AI-owned and removed.
Useful? React with 👍 / 👎.
| CASE WHEN mf.folder_type = 'UNORGANIZED' THEN 1 ELSE 0 END ASC, | ||
| stats.last_created_at DESC NULLS LAST, | ||
| mf.folder_name ASC | ||
| LIMIT 50 |
There was a problem hiding this comment.
Avoid truncating folder candidates used for AI matching
This hard LIMIT 50 truncates the folder context set before prompt construction, and the enriched prompt path only sends {folderContext} (not a full folder-name fallback), so folders beyond the first 50 are invisible to the model. For users with more than 50 root folders, AI cannot recommend omitted existing folders and may instead suggest creating new duplicates.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/lib/api/fetchClient.ts (1)
144-150:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win2xx 성공 응답의
JSON.parse호출이 try-catch로 보호되지 않음비-2xx 경로(Lines 128–136)는
JSON.parse를 try-catch 내부에서 안전하게 처리하지만, 2xx 성공 경로의 Line 150JSON.parse(text)는 예외 처리가 없습니다. 서버가 2xx 상태코드와 함께 HTML(예: 게이트웨이 오류 페이지)이나 plain text를 반환하면SyntaxError가ApiError가 아닌 형태로 그대로 전파되어,QueryProvider의error instanceof ApiError분기를 우회합니다.🛡️ 제안 수정
- const json: ApiResponse<T> = JSON.parse(text); + let json: ApiResponse<T>; + try { + json = JSON.parse(text); + } catch { + throw new ApiError({ message: DEFAULT_API_ERROR_MESSAGE }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/lib/api/fetchClient.ts` around lines 144 - 150, The 2xx success path in fetch response handling calls JSON.parse(text) without try-catch (variable json: ApiResponse<T>), so wrap the parse in a try-catch around JSON.parse(text) and, on any SyntaxError or parse failure, throw an ApiError (or construct the same error shape used in the non-2xx branch) so callers (e.g., QueryProvider's error instanceof ApiError) handle it consistently; update the parsing block that produces `json` to catch parse errors and rethrow a mapped ApiError with useful context (status, body) rather than letting SyntaxError escape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/providers/QueryProvider.tsx`:
- Around line 30-32: The onError handler in QueryProvider.tsx calls
alert(getApiErrorMessage(error)) unconditionally, which blocks navigation when
fetchClient throws ApiError({ code: 'AUTH_EXPIRED' }); update the handler to
detect that error (check error.code === 'AUTH_EXPIRED' or error instanceof
ApiError with code) and skip showing the alert in that case so the existing
fetchClient redirect to '/login' is not blocked; for all other errors continue
to call getApiErrorMessage(error) and alert as before.
In `@src/main/java/com/web/SearchWeb/linkanalysis/domain/FolderContext.java`:
- Around line 113-126: In FolderContext where the Array-to-List conversion
happens (the block checking "if (value instanceof Array sqlArr)"), keep the
current behavior of swallowing SQLException but add a DEBUG-level log in the
catch to record the exception and context (include sqlArr and/or the column name
if available) so failures are visible during debugging; use the existing logger
(or add one if missing) and call logger.debug with a descriptive message and the
caught SQLException, then continue to return the empty/fallback result as
before.
---
Outside diff comments:
In `@frontend/src/lib/api/fetchClient.ts`:
- Around line 144-150: The 2xx success path in fetch response handling calls
JSON.parse(text) without try-catch (variable json: ApiResponse<T>), so wrap the
parse in a try-catch around JSON.parse(text) and, on any SyntaxError or parse
failure, throw an ApiError (or construct the same error shape used in the
non-2xx branch) so callers (e.g., QueryProvider's error instanceof ApiError)
handle it consistently; update the parsing block that produces `json` to catch
parse errors and rethrow a mapped ApiError with useful context (status, body)
rather than letting SyntaxError escape.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 53c55939-f420-448e-ac7b-13257445e364
📒 Files selected for processing (17)
frontend/src/components/dialogs/SaveLinkDialog.tsxfrontend/src/components/providers/QueryProvider.tsxfrontend/src/lib/api/fetchClient.tssrc/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.javasrc/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.javasrc/main/java/com/web/SearchWeb/linkanalysis/domain/FolderContext.javasrc/main/java/com/web/SearchWeb/linkanalysis/domain/PageContent.javasrc/main/java/com/web/SearchWeb/linkanalysis/service/LinkAnalysisServiceImpl.javasrc/main/java/com/web/SearchWeb/linkanalysis/service/LinkMetadataExtractor.javasrc/main/resources/application.propertiessrc/main/resources/mapper/bookmark-mapper.xmlsrc/main/resources/prompts/link-analysis-system-v1.mdsrc/main/resources/prompts/link-analysis-system-v2.mdsrc/main/resources/prompts/link-analysis-system-v3.mdsrc/main/resources/prompts/link-analysis-system-v4.mdsrc/main/resources/prompts/link-analysis-user-basic.mdsrc/main/resources/prompts/link-analysis-user-enriched.md
💤 Files with no reviewable changes (1)
- src/main/java/com/web/SearchWeb/linkanalysis/domain/PageContent.java
|
|
||
| // 2. 사용자에게는 어떤 상황에서도 기술 정보 없이 일반적인 안내 문구만 제공 | ||
| alert('요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'); | ||
| // 2. 백엔드가 내려준 사용자용 메시지가 있으면 그대로 안내한다. | ||
| alert(getApiErrorMessage(error)); |
There was a problem hiding this comment.
AUTH_EXPIRED 에러 시 alert()가 /login 리다이렉트를 차단
fetchClient는 토큰 갱신 실패 시 window.location.href = '/login'을 설정한 직후 ApiError({ code: 'AUTH_EXPIRED' })를 throw합니다(fetchClient.ts Lines 108–115). React Query가 이를 onError로 전달하면 alert()가 실행되어 브라우저의 다이얼로그가 네비게이션을 블로킹합니다. 사용자는 알림을 직접 닫아야 로그인 페이지로 이동할 수 있습니다.
이 동작은 해당 throw 구문이 이번 PR에서 신규 추가(~)된 것이므로 새로 도입된 회귀입니다.
🛡️ 제안 수정 — AUTH_EXPIRED alert 생략
- // 2. 백엔드가 내려준 사용자용 메시지가 있으면 그대로 안내한다.
- alert(getApiErrorMessage(error));
+ // 2. AUTH_EXPIRED는 /login 리다이렉트 중이므로 alert 를 생략한다.
+ if (error instanceof ApiError && error.code === 'AUTH_EXPIRED') return;
+ alert(getApiErrorMessage(error));📝 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.
| // 2. 사용자에게는 어떤 상황에서도 기술 정보 없이 일반적인 안내 문구만 제공 | |
| alert('요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'); | |
| // 2. 백엔드가 내려준 사용자용 메시지가 있으면 그대로 안내한다. | |
| alert(getApiErrorMessage(error)); | |
| // 2. AUTH_EXPIRED는 /login 리다이렉트 중이므로 alert 를 생략한다. | |
| if (error instanceof ApiError && error.code === 'AUTH_EXPIRED') return; | |
| alert(getApiErrorMessage(error)); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/providers/QueryProvider.tsx` around lines 30 - 32,
The onError handler in QueryProvider.tsx calls alert(getApiErrorMessage(error))
unconditionally, which blocks navigation when fetchClient throws ApiError({
code: 'AUTH_EXPIRED' }); update the handler to detect that error (check
error.code === 'AUTH_EXPIRED' or error instanceof ApiError with code) and skip
showing the alert in that case so the existing fetchClient redirect to '/login'
is not blocked; for all other errors continue to call getApiErrorMessage(error)
and alert as before.
| if (value instanceof Array sqlArr) { | ||
| try { | ||
| Object inner = sqlArr.getArray(); | ||
| if (inner instanceof Object[] objArr) { | ||
| List<String> out = new ArrayList<>(objArr.length); | ||
| for (Object o : objArr) { | ||
| if (o != null) out.add(o.toString()); | ||
| } | ||
| return Collections.unmodifiableList(out); | ||
| } | ||
| } catch (SQLException ignored) { | ||
| // fall through to empty | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
SQLException 무시 처리는 적절하나 로깅 추가 권장
java.sql.Array 변환 시 SQLException을 무시하고 있습니다. Graceful degradation 관점에서는 적절하지만, 디버깅을 위해 DEBUG 레벨 로깅을 추가하면 좋겠습니다.
🔧 선택적 로깅 추가
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
`@Getter`
`@Builder`
public class FolderContext {
// ...
} catch (SQLException ignored) {
- // fall through to empty
+ log.debug("Failed to convert SQL Array to List<String>", ignored);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/web/SearchWeb/linkanalysis/domain/FolderContext.java`
around lines 113 - 126, In FolderContext where the Array-to-List conversion
happens (the block checking "if (value instanceof Array sqlArr)"), keep the
current behavior of swallowing SQLException but add a DEBUG-level log in the
catch to record the exception and context (include sqlArr and/or the column name
if available) so failures are visible during debugging; use the existing logger
(or add one if missing) and call logger.debug with a descriptive message and the
caught SQLException, then continue to return the empty/fallback result as
before.
SaveLinkDialog: AI 추천 태그 상태 무결성 확보: 사용자가 기존에 수동 선택한 태그를 AI 추천 태그 목록에 포함하지 않도록 baseTags 비교 조건 추가 및 재분석 시 수동 선택 태그가 정리 대상에 섞이는 현상 방지. QueryProvider: 인증 만료 UX 개선: AUTH_EXPIRED 에러는 로그아웃 및 로그인 페이지 이동 흐름에서 이미 처리되므로 전역 alert 중복 노출을 차단하여 불필요한 경고 팝업 제거.
bookmark-mapper.xml: 폴더 추천 후보 확장: LLM 폴더 추천용 루트 폴더 컨텍스트 조회 제한을 50개에서 100개로 확대하여 폴더 수가 많은 사용자 환경에서 기존 폴더 인식 범위 개선. LinkAnalysisServiceImpl: 프롬프트 컨텍스트 최적화: 전체 폴더명과 설명은 최대 100개까지 제공하되 태그 및 샘플 제목 상세 정보는 상위 30개 폴더로 제한하여 추천 후보 누락 방지와 프롬프트 과대화 완화. LinkAnalysisServiceImpl: 폴더 설명 반영 강화: 폴더별 설명이 존재하는 경우 샘플 및 태그 상세 정보 여부와 무관하게 기본 컨텍스트에 포함하여 AI가 사용자의 폴더 의도를 더 안정적으로 판단하도록 개선. FolderContext: SQL Array 변환 진단성 개선: SQLException 발생 시 예외를 완전히 무시하지 않고 debug 로그로 원인을 남겨 폴더 컨텍스트 누락 문제 추적 가능성 확보.
CustomOAuth2MemberService: 신규 회원 초기화 안정성 확보: OAuth2 로그인 사용자 정보 저장과 신규 가입자 미분류 폴더 보장 로직이 하나의 트랜잭션 경계 안에서 실행되도록 loadUser에 @transactional 추가. CustomOAuth2MemberService: 가입 직후 폴더 조회 안정성 개선: 소셜 회원 생성 직후 기본 폴더 생성 과정에서 DB 동기화 타이밍이 어긋나는 상황을 줄여 첫 진입 시 폴더 목록 누락 가능성 완화.
💡 이슈
resolve {#43}
🤩 개요
🧑💻 작업 사항
ApiError클래스 도입으로 백엔드 비즈니스 에러 코드와 사용자 메시지 연동 표준화📖 참고 사항
application.properties에서 프롬프트 버전 및 리소스 경로 설정이 변경되었습니다.Summary by CodeRabbit
릴리스 노트
New Features
Bug Fixes