Skip to content

[FEAT] 기술 분석 흐름 및 탐색 상태 관리 개선 - #51

Merged
GirimNam merged 8 commits into
mainfrom
fix/analysis-flow
Jul 29, 2026
Merged

[FEAT] 기술 분석 흐름 및 탐색 상태 관리 개선#51
GirimNam merged 8 commits into
mainfrom
fix/analysis-flow

Conversation

@minsxo

@minsxo minsxo commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📌 작업 내용

  • 신규성·진보성 분석 API 연동과 활동 기록 내 분석 흐름을 개선했습니다.
  • 탐색 중 페이지를 이탈해도 진행 상태를 복원하고, 완료 후 입력값을 초기화하도록 수정했습니다.
  • 활동 기록 목록과 상세 화면의 분석 완료 상태를 동기화했습니다.

✅ 변경 사항

  • 새로운 기능 추가
  • 버그 수정
  • 코드 개선 및 수정
  • 문서 작성 및 업데이트
  • 배포 관련 수정
  • 브랜치 관련 수정
  • 기타:

🔗 관련 이슈

Closes #

🧪 체크리스트

  • 제가 작성한 코드를 리뷰했습니다.
  • 문서에 변경 사항을 반영했습니다.
  • 새로운 경고를 생성하지 않습니다.
  • 새로운 기능이나 수정 사항이 기존 테스트와 충돌하지 않음을 확인했습니다.

📸 스크린샷

없음

💬 리뷰 요청 사항

기타

  • 신규성 분석은 기존 결과가 있으면 재분석하지 않고 저장된 결과를 조회합니다.
  • 리포트 POST/PATCH는 화면과 API 명세가 확정되지 않아 이번 PR 범위에서 제외했습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 진행 중인 검색을 저장하고, 페이지 이동 후에도 검색 상태를 이어갈 수 있습니다.
    • 검색 완료 후 결과 페이지로 자동 이동하며, 중단된 검색 상태를 정리합니다.
    • 분석 결과에 따라 완료 단계와 상태 배지가 더 정확하게 표시됩니다.
    • 분석 리포트 메뉴가 이용 가능한 경우에만 표시됩니다.
  • 버그 수정

    • 인증 준비 전 불필요한 요청을 방지하고, 인증이 필요한 상황을 명확히 안내합니다.
    • 신규성·진보성 분석 결과를 올바르게 반영해 분석 상태를 표시합니다.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

분석 존재 여부와 인증 상태를 조회 훅에 반영하고, 상태 배지와 리포트 메뉴 표시를 통합했습니다. 검색 결과는 영속 스토어에 저장되며, 검색 화면과 로딩 폴링이 이를 복원·정리하도록 변경되었습니다.

Changes

분석 상태와 데이터 조회

Layer / File(s) Summary
분석 상태 계산과 인증 가드
src/types/case.type.ts, src/hooks/useCaseDetail.ts, src/hooks/useMyHistory.ts, src/hooks/usePriorArtsList.ts, src/hooks/useRecentCases.ts, src/lib/caseStatus.ts
분석 존재 플래그와 완료 판정이 추가되었고, 데이터 조회 훅이 인증 초기화 및 토큰 상태에 따라 요청·로딩·오류를 처리합니다.

상태 및 리포트 UI

Layer / File(s) Summary
상태 배지와 리포트 메뉴
src/app/(main)/myhistory/[id]/page.tsx, src/app/(main)/myhistory/page.tsx, src/components/myhistory/AnalysisMenu.tsx
상세·목록 화면이 공통 상태 배지를 사용하며, 리포트 메뉴와 신규성 분석 사전 조회 분기가 추가되었습니다.

활성 검색 흐름

Layer / File(s) Summary
활성 검색 상태 저장
src/store/activeSearchStore.ts, src/hooks/useSearchForm.ts
검색 시작 결과를 영속 Zustand 스토어에 저장하고 검색 식별자, 결과 수, 제목을 관리합니다.
검색 복원과 폴링 정리
src/app/(main)/search/page.tsx, src/app/(main)/search/loading/page.tsx
활성 검색이 있으면 로딩 경로로 복원하고, 인증 후 폴링하며 완료·중단 시 관련 상태를 초기화합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 기술 분석 API 연동, 탐색 상태 복원, 완료 상태 동기화라는 핵심 변경을 잘 요약한 제목입니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analysis-flow

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

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

@github-actions

Copy link
Copy Markdown

@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 (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가 되어, 실제로는 분석이 존재하는데 목록에서는 "미완료"로 잘못 표시될 수 있습니다. ApiErrorerrorCode(예: "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_COMPLETEDNOT_STARTED처럼 두 분석 결과가 굳이 필요 없는 케이스에서도, 사용자는 제목·출원인 같은 기본 정보조차 두 추가 API 응답을 기다려야 화면에서 볼 수 있어요.

바로 아래 useMyHistory.tsresolveAnalysisState는 이미 status를 보고 필요한 경우에만 추가 호출을 하도록 잘 최적화되어 있는데, 같은 PR 안에서 이 훅만 그 패턴을 따르지 않고 있는 점도 아쉬워요.

개선 방향 두 가지를 제안드려요:

  1. status가 이미 완료/미시작을 확정하는 경우 추가 API 호출 자체를 건너뛰기 (resolveAnalysisState와 동일 패턴)
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 62f5785 and 38375ee.

📒 Files selected for processing (13)
  • src/app/(main)/myhistory/[id]/page.tsx
  • src/app/(main)/myhistory/page.tsx
  • src/app/(main)/search/loading/page.tsx
  • src/app/(main)/search/page.tsx
  • src/components/myhistory/AnalysisMenu.tsx
  • src/hooks/useCaseDetail.ts
  • src/hooks/useMyHistory.ts
  • src/hooks/usePriorArtsList.ts
  • src/hooks/useRecentCases.ts
  • src/hooks/useSearchForm.ts
  • src/lib/caseStatus.ts
  • src/store/activeSearchStore.ts
  • src/types/case.type.ts

Comment on lines +100 to +109
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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' -S

Repository: 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 || true

Repository: 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'
fi

Repository: CEOS-IPX/IPX-FE

Length of output: 8244


URL caseIdparseCaseId로 먼저 정규화하세요.

src/app/(main)/search/loading/page.tsxsearchParams.get("caseId")를 문자열로 그대로 사용하다 Number(caseId) 변환으로 NaN이 될 수 있습니다. 잘못된 값이 cancelSearch(Number(caseId)), getSearchStatus(Number(caseId)), activeSearch 저장에 전달되면 잘못된 API 경로와 복원 상태를 만드는 원인이 됩니다.

이미 src/lib/parseCaseId.tsNumber.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.

Suggested change
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

Comment on lines +146 to +152
if (!caseId || isStopping || !isAuthInitialized || !accessToken) return;

setActiveSearch({
caseId: Number(caseId),
resultCount,
title: title ?? "",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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로 저장된 activeSearchloading 조회가 실패한 뒤에도 남아 있겠죠. src/app/(main)/search/page.tsxactiveSearch일 때 /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

Comment on lines +12 to +61
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" };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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를 반환하는 공용 함수를 정의하고, deriveCaseSummaryStatusBadgeuseCaseDetail.tsderiveStatusBadge가 이를 함께 재사용하도록 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.

@GirimNam
GirimNam merged commit 7d447b1 into main Jul 29, 2026
2 checks passed
GirimNam added a commit to GirimNam/IPX-FE that referenced this pull request Jul 29, 2026
[FEAT] 기술 분석 흐름 및 탐색 상태 관리 개선
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.

2 participants