[FEAT] 기술 분석 흐름 및 탐색 상태 관리 개선 - #51
Conversation
# Conflicts: # src/app/(main)/search/loading/page.tsx # src/store/searchFormStore.ts
📝 WalkthroughWalkthrough분석 존재 여부와 인증 상태를 조회 훅에 반영하고, 상태 배지와 리포트 메뉴 표시를 통합했습니다. 검색 결과는 영속 스토어에 저장되며, 검색 화면과 로딩 폴링이 이를 복원·정리하도록 변경되었습니다. Changes분석 상태와 데이터 조회
상태 및 리포트 UI
활성 검색 흐름
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/components/myhistory/AnalysisMenu.tsx (2)
68-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value동작은 정확해요. 다만 중첩 try/catch가 한눈에 들어오진 않네요.
"기존 결과 조회 → 없으면(
N001) 새로 실행" 폴백 로직 자체는 PR 목표("기존 신규성 분석 결과가 있으면 재분석하지 않고 조회")에 맞게 잘 구현됐어요. 다만 try 안에 try가 있는 구조라 에러 흐름을 따라가기가 약간 번거로워요. 이 부분을 별도 헬퍼로 뽑으면handleNoveltyAnalysis가 한결 읽기 쉬워질 것 같아요.♻️ 제안: 헬퍼 함수로 분리
+ const ensureNoveltyAnalysis = async (caseId: string) => { + try { + await getNoveltyAnalysis(caseId); + } catch (error) { + if (!(error instanceof ApiError) || error.errorCode !== "N001") { + throw error; + } + await runNoveltyAnalysis(caseId); + } + }; + const handleNoveltyAnalysis = async () => { if (isNoveltyAnalyzing) return; setIsNoveltyAnalyzing(true); setNoveltyAnalysisError(null); try { - try { - await getNoveltyAnalysis(caseId); - } catch (error) { - if (!(error instanceof ApiError) || error.errorCode !== "N001") { - throw error; - } - - await runNoveltyAnalysis(caseId); - } - + await ensureNoveltyAnalysis(caseId); router.push(`/myhistory/${encodeURIComponent(caseId)}/novelty${titleQuery}`); } catch (error) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/myhistory/AnalysisMenu.tsx` around lines 68 - 77, Extract the existing “fetch novelty analysis, and run it only when ApiError.errorCode is N001” fallback into a dedicated helper near handleNoveltyAnalysis, then have handleNoveltyAnalysis call that helper. Preserve propagation of all non-N001 errors and the current getNoveltyAnalysis-before-runNoveltyAnalysis behavior.
96-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win메뉴 항목이 늘어날수록 onClick 삼항연산자가 계속 깊어지고 있어요.
report항목이 추가되면서isResearchItem → isNoveltyItem → isInventiveStepItem → isReportItem → undefined로 4단 중첩 삼항연산자가 됐어요. 지금은 읽을 수 있지만, 다음 메뉴가 하나 더 추가되면 가독성이 급격히 나빠질 구조예요.item.key를 키로 하는 핸들러 맵으로 바꾸면 새 항목 추가 시에도 분기 깊이가 늘지 않고, 타입 안정성도 함께 챙길 수 있어요.♻️ 제안: key 기반 핸들러 맵
+ const menuHandlers: Record<(typeof ANALYSIS_MENU_ITEMS)[number]["key"], () => void> = { + research: () => router.push(`/search?caseId=${encodeURIComponent(caseId)}`), + novelty: handleNoveltyAnalysis, + "inventive-step": () => router.push(`/analysis/${encodeURIComponent(caseId)}${titleQuery}`), + report: () => router.push(`/myhistory/${encodeURIComponent(caseId)}/report${titleQuery}`), + }; + {ANALYSIS_MENU_ITEMS.filter((item) => item.key !== "report" || showReport).map((item) => { - const isResearchItem = item.key === "research"; - const isNoveltyItem = item.key === "novelty"; - const isInventiveStepItem = item.key === "inventive-step"; - const isReportItem = item.key === "report"; - const isLoading = isNoveltyItem && isNoveltyAnalyzing; + const isLoading = item.key === "novelty" && isNoveltyAnalyzing; return ( <button ... - onClick={ - isResearchItem - ? () => router.push(`/search?caseId=${encodeURIComponent(caseId)}`) - : isNoveltyItem - ? handleNoveltyAnalysis - : isInventiveStepItem - ? () => router.push(`/analysis/${encodeURIComponent(caseId)}${titleQuery}`) - : isReportItem - ? () => - router.push( - `/myhistory/${encodeURIComponent(caseId)}/report${titleQuery}` - ) - : undefined - } + onClick={menuHandlers[item.key]}Also applies to: 109-122
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/myhistory/AnalysisMenu.tsx` around lines 96 - 100, Refactor the menu click handling in AnalysisMenu’s mapped ANALYSIS_MENU_ITEMS flow to use a handler map keyed by item.key instead of the nested isResearchItem/isNoveltyItem/isInventiveStepItem/isReportItem ternary chain. Define the map with type-safe keys and preserve each existing item’s handler, including the undefined fallback for unsupported keys, so adding future menu items does not increase branching depth.src/hooks/useMyHistory.ts (1)
38-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win좋은 패턴이에요 — status 기반으로 필요한 호출만 하고 있네요. 👍
useCaseDetail.ts처럼 무조건 두 API를 다 부르는 대신, 여기선status를 보고 정말 필요한 경우에만 추가 조회를 하고 있어서 훨씬 효율적이에요.다만 한 가지,
.then(() => true, () => false)가 모든 실패를 "분석 없음"으로 취급하고 있어요. 만약getInventiveStepAnalysis/getNoveltyAnalysis가 네트워크 오류나 500 에러로 실패한 경우에도false가 되어, 실제로는 분석이 존재하는데 목록에서는 "미완료"로 잘못 표시될 수 있습니다.ApiError의errorCode(예: "N001" — 존재하지 않음)만false로 처리하고, 그 외 오류는 별도로 무시하거나 이전 값을 유지하는 편이 더 안전해요.🛡️ 제안: "없음"과 "조회 실패"를 구분
- if (project.status === "NOVELTY_COMPLETED") { - const inventiveAnalysisExists = await getInventiveStepAnalysis(project.caseId).then( - () => true, - () => false - ); - return { ...project, noveltyAnalysisExists: true, inventiveAnalysisExists }; - } + if (project.status === "NOVELTY_COMPLETED") { + const inventiveAnalysisExists = await getInventiveStepAnalysis(project.caseId).then( + () => true, + (err) => !(err instanceof ApiError && err.errorCode === "N001") ? project.inventiveAnalysisExists ?? false : false + ); + return { ...project, noveltyAnalysisExists: true, inventiveAnalysisExists }; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useMyHistory.ts` around lines 38 - 56, Update resolveAnalysisState so the getInventiveStepAnalysis and getNoveltyAnalysis checks return false only for the ApiError errorCode that specifically indicates the analysis does not exist (such as “N001”); do not convert network, server, or other errors into false, and preserve the existing project value or otherwise propagate those failures according to the surrounding error-handling pattern.src/hooks/useCaseDetail.ts (1)
102-140: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win분석 존재 여부 조회가 상세 페이지 첫 렌더를 블로킹하고 있어요.
getCaseDetail이 끝난 뒤getNoveltyAnalysis/getInventiveStepAnalysis두 요청이 모두 settle 되어야setDetail이 호출됩니다(Line 108-119). 즉REPORT_COMPLETED나NOT_STARTED처럼 두 분석 결과가 굳이 필요 없는 케이스에서도, 사용자는 제목·출원인 같은 기본 정보조차 두 추가 API 응답을 기다려야 화면에서 볼 수 있어요.바로 아래
useMyHistory.ts의resolveAnalysisState는 이미status를 보고 필요한 경우에만 추가 호출을 하도록 잘 최적화되어 있는데, 같은 PR 안에서 이 훅만 그 패턴을 따르지 않고 있는 점도 아쉬워요.개선 방향 두 가지를 제안드려요:
status가 이미 완료/미시작을 확정하는 경우 추가 API 호출 자체를 건너뛰기 (resolveAnalysisState와 동일 패턴)getCaseDetail결과를 먼저setDetail로 반영해 기본 정보를 즉시 그리고, 분석 존재 여부는 별도 state로 비동기 병합React 공식 문서에서도
useEffect/데이터 패칭 시 "필요한 만큼만 순차적으로, 불필요한 대기(waterfall)를 만들지 말라"는 가이드가 있으니 참고해보세요.💡 제안: status 기반 단축 + 점진적 렌더링
getCaseDetail(caseId) .then(async (result) => { - const [noveltyResult, inventiveResult] = await Promise.allSettled([ - getNoveltyAnalysis(caseId), - getInventiveStepAnalysis(caseId), - ]); - - if (cancelled) return; - setDetail({ - ...result, - noveltyAnalysisExists: noveltyResult.status === "fulfilled", - inventiveAnalysisExists: inventiveResult.status === "fulfilled", - }); + if (cancelled) return; + // 먼저 기본 정보로 즉시 렌더링 + setDetail(result); setError(null); setLoadedId(caseId); + + // 필요한 경우에만 분석 존재 여부를 비동기로 보강 + if (result.status === "REPORT_COMPLETED" || result.status === "NOT_STARTED") return; + + const [noveltyResult, inventiveResult] = await Promise.allSettled([ + getNoveltyAnalysis(caseId), + getInventiveStepAnalysis(caseId), + ]); + if (cancelled) return; + setDetail((prev) => + prev + ? { + ...prev, + noveltyAnalysisExists: noveltyResult.status === "fulfilled", + inventiveAnalysisExists: inventiveResult.status === "fulfilled", + } + : prev + ); })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useCaseDetail.ts` around lines 102 - 140, Update the useEffect data-fetching flow around getCaseDetail so the case detail is applied with setDetail as soon as that request succeeds, without waiting for both analysis requests to settle. Reuse the status-based optimization from resolveAnalysisState to skip analysis API calls when the case status already determines their absence or presence, and asynchronously merge analysis existence flags when checks are needed while preserving cancellation and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`(main)/search/loading/page.tsx:
- Around line 146-152: Update the retry-exhaustion handling around pollError in
the loading page to call clearActiveSearch(), treating exceeded retries as a
terminal search state. Preserve the existing cleanup for completed and stopped
searches, and ensure persisted activeSearch is cleared before the user can
re-enter the loading route and repeat the failure loop.
- Around line 100-109: Normalize the URL caseId in the search loading page with
the existing parseCaseId utility before using it. Replace the raw searchParams
caseId fallback in the caseId value, then use the validated result consistently
for cancelSearch, getSearchStatus, and activeSearch restoration so invalid or
non-positive values are not converted to NaN or propagated.
In `@src/hooks/useCaseDetail.ts`:
- Around line 12-61: The status badge mapping is duplicated across both files
and should be centralized. In src/lib/caseStatus.ts lines 3-27, export a shared
mapping function for the REPORT_COMPLETED, technical, novelty, inventive, and
fallback label/variant rules, and update deriveCaseSummaryStatusBadge to reuse
it. In src/hooks/useCaseDetail.ts lines 12-61, preserve
deriveAnalysisCompletion’s completion calculations but replace
deriveStatusBadge’s local mapping with the shared function, passing the detail
status, statusLabel, and computed completion flags.
---
Nitpick comments:
In `@src/components/myhistory/AnalysisMenu.tsx`:
- Around line 68-77: Extract the existing “fetch novelty analysis, and run it
only when ApiError.errorCode is N001” fallback into a dedicated helper near
handleNoveltyAnalysis, then have handleNoveltyAnalysis call that helper.
Preserve propagation of all non-N001 errors and the current
getNoveltyAnalysis-before-runNoveltyAnalysis behavior.
- Around line 96-100: Refactor the menu click handling in AnalysisMenu’s mapped
ANALYSIS_MENU_ITEMS flow to use a handler map keyed by item.key instead of the
nested isResearchItem/isNoveltyItem/isInventiveStepItem/isReportItem ternary
chain. Define the map with type-safe keys and preserve each existing item’s
handler, including the undefined fallback for unsupported keys, so adding future
menu items does not increase branching depth.
In `@src/hooks/useCaseDetail.ts`:
- Around line 102-140: Update the useEffect data-fetching flow around
getCaseDetail so the case detail is applied with setDetail as soon as that
request succeeds, without waiting for both analysis requests to settle. Reuse
the status-based optimization from resolveAnalysisState to skip analysis API
calls when the case status already determines their absence or presence, and
asynchronously merge analysis existence flags when checks are needed while
preserving cancellation and error handling.
In `@src/hooks/useMyHistory.ts`:
- Around line 38-56: Update resolveAnalysisState so the getInventiveStepAnalysis
and getNoveltyAnalysis checks return false only for the ApiError errorCode that
specifically indicates the analysis does not exist (such as “N001”); do not
convert network, server, or other errors into false, and preserve the existing
project value or otherwise propagate those failures according to the surrounding
error-handling pattern.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 178b50b4-b5d0-4fde-a3fd-117196a2b856
📒 Files selected for processing (13)
src/app/(main)/myhistory/[id]/page.tsxsrc/app/(main)/myhistory/page.tsxsrc/app/(main)/search/loading/page.tsxsrc/app/(main)/search/page.tsxsrc/components/myhistory/AnalysisMenu.tsxsrc/hooks/useCaseDetail.tssrc/hooks/useMyHistory.tssrc/hooks/usePriorArtsList.tssrc/hooks/useRecentCases.tssrc/hooks/useSearchForm.tssrc/lib/caseStatus.tssrc/store/activeSearchStore.tssrc/types/case.type.ts
| const activeSearch = useActiveSearchStore((state) => state.activeSearch); | ||
| const setActiveSearch = useActiveSearchStore((state) => state.setActiveSearch); | ||
| const clearActiveSearch = useActiveSearchStore((state) => state.clearActiveSearch); | ||
| const resetSearchForm = useSearchFormStore((state) => state.resetForm); | ||
| const isAuthInitialized = useAuthStore((state) => state.isInitialized); | ||
| const accessToken = useAuthStore((state) => state.accessToken); | ||
| const caseId = searchParams.get("caseId") ?? activeSearch?.caseId.toString() ?? null; | ||
| const title = searchParams.get("title") ?? activeSearch?.title ?? null; | ||
| const resultCount = | ||
| Number(searchParams.get("count") ?? activeSearch?.resultCount) || DEFAULT_RESULT_COUNT; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- locate relevant files ---\n'
git ls-files | rg 'src/app/\(main\)/search/loading/page\.tsx|src/app/\(main\)/search/page\.tsx|caseId|getSearchStatus|cancelSearch|searchFormStore|activeSearchStore|authStore' | head -200
printf '\n--- target file outline ---\n'
ast-grep outline 'src/app/(main)/search/loading/page.tsx' --view expanded || true
printf '\n--- target file sections ---\n'
wc -l 'src/app/(main)/search/loading/page.tsx'
sed -n '1,180p' 'src/app/(main)/search/loading/page.tsx'
printf '\n--- search for parse/caseId handling and calls ---\n'
rg -n 'caseId|parseCaseId|searchParams\.get\("caseId"|Number\(caseId\)|getSearchStatus|cancelSearch|setActiveSearch|clearActiveSearch' 'src' -SRepository: CEOS-IPX/IPX-FE
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- locate relevant files ---'
git ls-files | grep -E 'src/app/\(main\)/search/(loading/page\.tsx|page\.tsx)|caseId|getSearchStatus|cancelSearch|searchFormStore|activeSearchStore|authStore' | head -200 || true
printf '%s\n' ''
printf '%s\n' '--- target file outline ---'
ast-grep outline 'src/app/(main)/search/loading/page.tsx' --view expanded || true
printf '%s\n' ''
printf '%s\n' '--- target file sections ---'
wc -l 'src/app/(main)/search/loading/page.tsx'
sed -n '1,180p' 'src/app/(main)/search/loading/page.tsx'
printf '%s\n' ''
printf '%s\n' '--- search for parse/caseId handling and calls ---'
rg -n 'caseId|parseCaseId|searchParams\.get\("caseId"|Number\(caseId\)|getSearchStatus|cancelSearch|setActiveSearch|clearActiveSearch' 'src' -S || trueRepository: CEOS-IPX/IPX-FE
Length of output: 22197
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- src/lib/parseCaseId.ts ---'
cat -n src/lib/parseCaseId.ts
printf '%s\n' ''
printf '%s\n' '--- relevant search hook/form sections ---'
sed -n '45,70p' src/hooks/useSearchForm.ts
sed -n '285,340p' src/hooks/useSearchForm.ts
printf '%s\n' ''
printf '%s\n' '--- search API section ---'
cat -n src/lib/api/search.ts
printf '%s\n' ''
printf '%s\n' '--- loading page relevant remainder ---'
sed -n '170,230p' 'src/app/(main)/search/loading/page.tsx'
printf '%s\n' ''
printf '%s\n' '--- Next docs availability ---'
if [ -d node_modules/next/dist/docs ]; then
find node_modules/next/dist/docs -maxdepth 2 -type f | head -50 | sed 's#node_modules/next/dist/docs/##'
else
printf '%s\n' 'node_modules/next/dist/docs not present'
fiRepository: CEOS-IPX/IPX-FE
Length of output: 8244
URL caseId를 parseCaseId로 먼저 정규화하세요.
src/app/(main)/search/loading/page.tsx가 searchParams.get("caseId")를 문자열로 그대로 사용하다 Number(caseId) 변환으로 NaN이 될 수 있습니다. 잘못된 값이 cancelSearch(Number(caseId)), getSearchStatus(Number(caseId)), activeSearch 저장에 전달되면 잘못된 API 경로와 복원 상태를 만드는 원인이 됩니다.
이미 src/lib/parseCaseId.ts가 Number.isInteger(parsed) && parsed > 0를 검증하고 있으므로, 동일 패턴을 적용해 valid caseId로 처리한 뒤 API 요청과 store 상태를 연결하는 편이 더 안전합니다.
제안
- const caseId = searchParams.get("caseId") ?? activeSearch?.caseId.toString() ?? null;
+ const parsed = parseCaseId(searchParams.get("caseId") ?? undefined);
+ const caseId = parsed ?? activeSearch?.caseId ?? null;Next.js App Router의 URL 기반 컴포넌트에서는 useSearchParams의 입력을 App domain 타입으로 검증하는 편이 TypeScript 타입 안정성과 런타임 보호에 도움이 됩니다.
📝 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.
| const activeSearch = useActiveSearchStore((state) => state.activeSearch); | |
| const setActiveSearch = useActiveSearchStore((state) => state.setActiveSearch); | |
| const clearActiveSearch = useActiveSearchStore((state) => state.clearActiveSearch); | |
| const resetSearchForm = useSearchFormStore((state) => state.resetForm); | |
| const isAuthInitialized = useAuthStore((state) => state.isInitialized); | |
| const accessToken = useAuthStore((state) => state.accessToken); | |
| const caseId = searchParams.get("caseId") ?? activeSearch?.caseId.toString() ?? null; | |
| const title = searchParams.get("title") ?? activeSearch?.title ?? null; | |
| const resultCount = | |
| Number(searchParams.get("count") ?? activeSearch?.resultCount) || DEFAULT_RESULT_COUNT; | |
| const activeSearch = useActiveSearchStore((state) => state.activeSearch); | |
| const setActiveSearch = useActiveSearchStore((state) => state.setActiveSearch); | |
| const clearActiveSearch = useActiveSearchStore((state) => state.clearActiveSearch); | |
| const resetSearchForm = useSearchFormStore((state) => state.resetForm); | |
| const isAuthInitialized = useAuthStore((state) => state.isInitialized); | |
| const accessToken = useAuthStore((state) => state.accessToken); | |
| const parsed = parseCaseId(searchParams.get("caseId") ?? undefined); | |
| const caseId = parsed ?? activeSearch?.caseId ?? null; | |
| const title = searchParams.get("title") ?? activeSearch?.title ?? null; | |
| const resultCount = | |
| Number(searchParams.get("count") ?? activeSearch?.resultCount) || DEFAULT_RESULT_COUNT; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(main)/search/loading/page.tsx around lines 100 - 109, Normalize the
URL caseId in the search loading page with the existing parseCaseId utility
before using it. Replace the raw searchParams caseId fallback in the caseId
value, then use the validated result consistently for cancelSearch,
getSearchStatus, and activeSearch restoration so invalid or non-positive values
are not converted to NaN or propagated.
Source: Path instructions
| if (!caseId || isStopping || !isAuthInitialized || !accessToken) return; | ||
|
|
||
| setActiveSearch({ | ||
| caseId: Number(caseId), | ||
| resultCount, | ||
| title: title ?? "", | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching search loading/page.tsx and search/page.tsx:"
fd -a 'page\.tsx$' . | sed 's#^\./##' | grep -E 'search/(loading/)?page\.tsx$|search/page\.tsx$|search' | head -100
echo
echo "Outline loading page:"
if [ -f 'src/app/(main)/search/loading/page.tsx' ]; then
ast-grep outline 'src/app/(main)/search/loading/page.tsx' --view compact || true
wc -l 'src/app/(main)/search/loading/page.tsx'
echo
sed -n '1,260p' 'src/app/(main)/search/loading/page.tsx' | cat -n
else
echo "loading page not found"
fi
echo
echo "Outline target page:"
if [ -f 'src/app/(main)/search/page.tsx' ]; then
ast-grep outline 'src/app/(main)/search/page.tsx' --view compact || true
wc -l 'src/app/(main)/search/page.tsx'
echo
sed -n '1,220p' 'src/app/(main)/search/page.tsx' | cat -n
else
echo "target page not found"
fi
echo
echo "Search for clearActiveSearch and setActiveSearch:"
rg -n "clearActiveSearch|setActiveSearch|activeSearch|resultCount|isStopping|isAuthInitialized|accessToken" src app -g '*.{ts,tsx,js,jsx}'Repository: CEOS-IPX/IPX-FE
Length of output: 27493
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "activeSearchStore.ts:"
sed -n '1,220p' 'src/store/activeSearchStore.ts' | cat -n
echo
echo "Next docs availability:"
if [ -d 'node_modules/next/dist/docs' ]; then
find node_modules/next/dist/docs -maxdepth 3 -type f | head -100
else
echo "node_modules/next/dist/docs not present"
fi
echo
echo "Search for local storage / persist references:"
rg -n 'localStorage|persist|activeSearchStore|activeSearch' src/store src/app src/components src/hooks 'types/search.type.ts' -g '*.{ts,tsx,js,jsx}'Repository: CEOS-IPX/IPX-FE
Length of output: 9496
재시도 한도 초과 시 activeSearch도 종료 처리하세요.
zustand/persist로 저장된 activeSearch는 loading 조회가 실패한 뒤에도 남아 있겠죠. src/app/(main)/search/page.tsx가 activeSearch일 때 /search/loading으로 복원하므로, Line 188-194에서 pollError만 남으면 사용자가 다시 loading 진입 → 실패를 반복하게 됩니다.
React 공식 문서에서 Effect는 clearTimeout(timer)로 타이머를 정리하지만, 지속된 탐색 상태를 저장한persist 상태는 terminal 처리도 필요합니다. 완료·중단처럼 재시도 초과도 terminal 상태로 여겨 clearActiveSearch()를 호출해 복구 루프를 끊는 것이 안전합니다.
제안
} else {
setPollError(message);
+ clearActiveSearch();
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(main)/search/loading/page.tsx around lines 146 - 152, Update the
retry-exhaustion handling around pollError in the loading page to call
clearActiveSearch(), treating exceeded retries as a terminal search state.
Preserve the existing cleanup for completed and stopped searches, and ensure
persisted activeSearch is cleared before the user can re-enter the loading route
and repeat the failure loop.
Source: Path instructions
| type CaseDetailWithAnalysisState = CaseDetail & { | ||
| noveltyAnalysisExists?: boolean; | ||
| inventiveAnalysisExists?: boolean; | ||
| }; | ||
|
|
||
| export function deriveAnalysisCompletion(detail: CaseDetailWithAnalysisState) { | ||
| const noveltyCompleted = | ||
| detail.noveltyAnalysisExists === true || | ||
| Boolean(detail.noveltyCompletedAt) || | ||
| detail.status === "NOVELTY_COMPLETED" || | ||
| detail.status === "REPORT_COMPLETED"; | ||
| const inventiveCompleted = | ||
| detail.inventiveAnalysisExists === true || | ||
| Boolean(detail.inventiveCompletedAt) || | ||
| detail.status === "INVENTIVE_COMPLETED" || | ||
| detail.status === "REPORT_COMPLETED"; | ||
|
|
||
| return { | ||
| noveltyCompleted, | ||
| inventiveCompleted, | ||
| technicalAnalysisCompleted: | ||
| (noveltyCompleted && inventiveCompleted) || | ||
| detail.status === "REPORT_COMPLETED" || | ||
| detail.reportAvailable, | ||
| }; | ||
| } | ||
|
|
||
| export function deriveStatusBadge(detail: CaseDetailWithAnalysisState): { | ||
| label: string; | ||
| variant: "primary" | "secondary"; | ||
| } { | ||
| if (detail.status === "REPORT_COMPLETED") { | ||
| return { label: "완료", variant: "secondary" }; | ||
| } | ||
|
|
||
| const { noveltyCompleted, inventiveCompleted, technicalAnalysisCompleted } = | ||
| deriveAnalysisCompletion(detail); | ||
|
|
||
| if (technicalAnalysisCompleted) { | ||
| return { label: "기술 분석 완료", variant: "primary" }; | ||
| } | ||
| if (noveltyCompleted) { | ||
| return { label: "신규성 분석 완료", variant: "primary" }; | ||
| } | ||
| if (inventiveCompleted) { | ||
| return { label: "진보성 분석 완료", variant: "primary" }; | ||
| } | ||
|
|
||
| return { label: detail.statusLabel, variant: "primary" }; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
상태 배지 판정 규칙이 두 파일에 중복 구현되어 있어요 — 공유 유틸로 뽑아보는 건 어떨까요?
"REPORT_COMPLETED면 완료 → 신규성+진보성 모두 완료(or reportAvailable)면 기술분석완료 → 신규성만 완료면 신규성분석완료 → 진보성만 완료면 진보성분석완료" 라는 동일한 비즈니스 규칙이 useCaseDetail.ts(상세 페이지용)와 caseStatus.ts(목록 페이지용)에 각각 따로 구현돼 있어요. 지금은 두 로직이 우연히 일치하지만, 나중에 라벨 문구나 조건이 바뀌면 한쪽만 수정되고 다른 쪽은 그대로 남는 divergence 버그가 생기기 쉬운 구조예요.
{ noveltyCompleted, inventiveCompleted, technicalAnalysisCompleted } → { label, variant } 변환 부분만 공통 함수로 뽑고, 각 파일에서는 자신의 데이터 모델(CaseDetail vs CaseSummary)에 맞게 완료 여부만 계산해서 넘겨주는 구조로 정리하면 좋을 것 같아요.
src/hooks/useCaseDetail.ts#L12-L61:deriveAnalysisCompletion의 완료 여부 계산은 유지하되,deriveStatusBadge의 라벨/variant 매핑 부분을 공용 함수(예:src/lib/caseStatus.ts에 위치)로 위임.src/lib/caseStatus.ts#L3-L27: 완료 여부 계산 후 라벨/variant를 반환하는 공용 함수를 정의하고,deriveCaseSummaryStatusBadge와useCaseDetail.ts의deriveStatusBadge가 이를 함께 재사용하도록 export.
React/TypeScript 관점에서도 동일 규칙을 한 곳에 두면 타입 추론과 테스트도 한 번만 작성하면 되니, 유지보수성이 확실히 좋아질 거예요.
♻️ 제안: 공용 배지 매핑 함수
// src/lib/caseStatus.ts
export function mapAnalysisCompletionToBadge(params: {
status: string;
statusLabel: string;
noveltyCompleted: boolean;
inventiveCompleted: boolean;
technicalAnalysisCompleted: boolean;
}): { label: string; variant: "primary" | "secondary" } {
const { status, statusLabel, noveltyCompleted, inventiveCompleted, technicalAnalysisCompleted } = params;
if (status === "REPORT_COMPLETED") return { label: "완료", variant: "secondary" };
if (technicalAnalysisCompleted) return { label: "기술 분석 완료", variant: "primary" };
if (noveltyCompleted) return { label: "신규성 분석 완료", variant: "primary" };
if (inventiveCompleted) return { label: "진보성 분석 완료", variant: "primary" };
return { label: statusLabel, variant: "primary" };
}📍 Affects 2 files
src/hooks/useCaseDetail.ts#L12-L61(this comment)src/lib/caseStatus.ts#L3-L27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useCaseDetail.ts` around lines 12 - 61, The status badge mapping is
duplicated across both files and should be centralized. In src/lib/caseStatus.ts
lines 3-27, export a shared mapping function for the REPORT_COMPLETED,
technical, novelty, inventive, and fallback label/variant rules, and update
deriveCaseSummaryStatusBadge to reuse it. In src/hooks/useCaseDetail.ts lines
12-61, preserve deriveAnalysisCompletion’s completion calculations but replace
deriveStatusBadge’s local mapping with the shared function, passing the detail
status, statusLabel, and computed completion flags.
[FEAT] 기술 분석 흐름 및 탐색 상태 관리 개선
📌 작업 내용
✅ 변경 사항
🔗 관련 이슈
Closes #
🧪 체크리스트
📸 스크린샷
없음
💬 리뷰 요청 사항
기타
Summary by CodeRabbit
새로운 기능
버그 수정