Feat/27#28
Conversation
임시 mock API와 캡차 토글 설정을 제거합니다. 로딩 화면은 백엔드 SSE step/status 기준으로 진행 상태를 표시합니다.
분석 시각과 인증서 정보를 백엔드 응답에서 읽어 결과 화면에 전달합니다. 점수 기반 위험도 분류와 결과 매퍼 테스트를 보강합니다.
결과 카드의 가짜 인증서 기본 문구를 제거합니다. 긴 URL과 제목이 모바일 화면에서 카드 밖으로 넘치지 않도록 줄바꿈을 보강합니다.
캡처 기반 PDF 생성 코드를 제거하고 브라우저 기본 출력으로 PDF 저장을 지원합니다. 출력 시 앱 헤더와 액션 버튼은 숨기고 리포트 본문만 정리해서 표시합니다.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 40 minutes and 9 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
WalkthroughCaptcha 제공자 선택/디버그 제거, BE1/BE3 및 타임아웃 환경 설정 추가, Axios 기반 BE 클라이언트(axiosBe1/axiosBe3) 도입, SSE 구독으로 스캔 진행 추적 구현, 스캔 세션·진행·게스트 UUID 스토어 추가, Result/Report/Loading/ScanList 등 페이지 흐름 대대적 재구성입니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Browser as 브라우저
participant Store as 로컬 스토어<br/>(Guest/Session/Progress)
participant BE as 백엔드 API
participant SSE as SSE 서버
User->>Browser: QR 이미지 업로드 / 캡처
Browser->>BE: POST /api/v1/scan (axiosBe1: submitQrImage)
BE-->>Browser: scan response (scan id / payload)
Browser->>Store: resetForNewScan() / setScanResponse()
Browser->>SSE: EventSource 연결 (/api/v1/scan/subscribe?guest_uuid=...)
activate SSE
loop 진행 중
SSE-->>Browser: progress 메시지 (step/status/percent)
Browser->>Store: updateFromProgressEvent(payload)
Browser->>Browser: Loading UI 갱신 (useLoadingProgress → LoadingPage)
end
SSE-->>Browser: final 메시지 (analysisDetail / final payload)
Browser->>Store: setAnalysisDetail() / setFinalResult()
Browser->>BE: GET /api/v1/scan/detail?url=... (필요시 axiosBe3)
BE-->>Browser: analysis detail
Browser->>Store: setAnalysisDetail()
Browser->>Browser: 결과 톤/데이터 변환 (toResult/toReport)
Browser->>Browser: 라우팅 → /result/{tone} 또는 /report
deactivate SSE
|
There was a problem hiding this comment.
Actionable comments posted: 19
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/Captcha/ui/CaptchaWidget.tsx (1)
113-150:⚠️ Potential issue | 🟠 Major비동기 reCAPTCHA 렌더링에 cleanup guard를 추가해야 합니다.
스크립트 로드나
enterprise.ready()대기 중 컴포넌트가 언마운트되거나recaptchaSiteKey가 변경되면, 진행 중인 promise 체인의 콜백이 계속 실행되어 언마운트된 컴포넌트에서onTokenChange를 호출할 수 있습니다. 또한enterprise.render()의 콜백들(token, error, expired)도 언마운트 후 실행될 수 있습니다.🛡️ 제안 수정
useEffect(() => { + let isActive = true; const container = widgetContainerRef.current; if (!container || !recaptchaSiteKey) { return; } container.innerHTML = ''; loadEnterpriseScript() .then(() => { + if (!isActive) { + return; + } + const enterprise = window.grecaptcha?.enterprise; if (!enterprise) { onTokenChange(null); return; } enterprise.ready(() => { + if (!isActive || widgetContainerRef.current !== container) { + return; + } + enterprise.render(container, { callback: (nextToken) => { + if (!isActive) { + return; + } onTokenChange(nextToken); }, 'error-callback': () => { + if (!isActive) { + return; + } onTokenChange(null); }, 'expired-callback': () => { + if (!isActive) { + return; + } onTokenChange(null); }, sitekey: recaptchaSiteKey, theme: 'light', }); }); }) .catch(() => { + if (!isActive) { + return; + } onTokenChange(null); }); + + return () => { + isActive = false; + container.innerHTML = ''; + }; }, [onTokenChange, recaptchaSiteKey]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/ui/CaptchaWidget.tsx` around lines 113 - 150, The effect that loads and renders reCAPTCHA (the useEffect using widgetContainerRef, loadEnterpriseScript, window.grecaptcha?.enterprise, enterprise.ready and enterprise.render) lacks an unmount/cleanup guard causing callbacks (enterprise.ready, enterprise.render callbacks and the promise .catch) to call onTokenChange after the component is unmounted or recaptchaSiteKey changes; fix by adding an isMounted/aborted flag (e.g., let cancelled = false) at the top of the effect, check this flag before calling onTokenChange or interacting with widgetContainerRef inside all promise callbacks and inside enterprise.render callback functions, and in the returned cleanup function set cancelled = true and optionally call enterprise.reset/remove widget from container to prevent stray callbacks; ensure checks reference the same flag in loadEnterpriseScript.then/catch, enterprise.ready, and the render callbacks so no calls occur after unmount or key change.
🟡 Minor comments (7)
src/features/scan-history/lib/historyItemAccess.test.ts-23-24 (1)
23-24:⚠️ Potential issue | 🟡 Minor히스토리 항목 ID가 중복되지 않도록 테스트 기대값을 바꿔 주세요.
현재 기대값은
index를 무시하므로 같은 URL이 같은 초에 여러 번 스캔되면 동일한id가 만들어질 수 있습니다. 이 값이 리스트 key로 쓰이면 React reconciliation이 흔들릴 수 있으니, 백엔드 고유 ID가 없다면index도 ID에 포함하는 동작을 고정하는 편이 안전합니다.제안 수정
expect(buildHistoryItemId(historyItem, 0)).toBe( - 'URL|https://r-generator.com/1Ugy3mkc|2026-04-21 02:50:44', + '0|URL|https://r-generator.com/1Ugy3mkc|2026-04-21 02:50:44', );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/scan-history/lib/historyItemAccess.test.ts` around lines 23 - 24, The test expectation currently assumes buildHistoryItemId(historyItem, 0) omits the index, causing potential duplicate IDs; update the implementation of buildHistoryItemId to incorporate the passed index into the generated ID (e.g., append or embed the index) and then change the test expectation in historyItemAccess.test.ts to match the new ID format returned for buildHistoryItemId(historyItem, 0) so the ID includes the index and is unique.src/pages/ResultNonUrl/ResultNonUrlPage.tsx-39-45 (1)
39-45:⚠️ Potential issue | 🟡 Minor미리보기 항목에서는 실행 버튼을 숨겨 주세요.
감지된 액션이 아닌 미리보기 선택 시
displayedTargetValue가undefined가 되어, 사용자가 실행 버튼을 클릭하면 "실행을 위한 정보가 없습니다" 같은 오류 메시지만 표시됩니다. 실행 불가능한 버튼을 보여주는 것은 혼란스러운 UX입니다.제안 수정
const displayedActionType = selectedPreviewItem?.actionType ?? resultNonUrlPageData.detectedActionType; + const isDetectedAction = + displayedActionType === resultNonUrlPageData.detectedActionType; const displayedTargetValue = - displayedActionType === resultNonUrlPageData.detectedActionType - ? resultNonUrlPageData.targetValue - : undefined; + isDetectedAction ? resultNonUrlPageData.targetValue : undefined; @@ - <button className={styles.executionButton} onClick={handleExecuteAction} type="button"> - 주의하여 실행하기 - </button> + {isDetectedAction ? ( + <button className={styles.executionButton} onClick={handleExecuteAction} type="button"> + 주의하여 실행하기 + </button> + ) : null}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ResultNonUrl/ResultNonUrlPage.tsx` around lines 39 - 45, The preview state can make displayedTargetValue undefined and lead to a non-functional Execute button; update the render logic (where the execute button is produced) to only show/enable it when the action is actually executable—e.g., compute an isExecutable flag using displayedActionType === resultNonUrlPageData.detectedActionType && displayedTargetValue != null and use that to conditionally render or disable the Execute button tied to handleExecuteAction/resolveNonUrlActionExecution so users don’t see a clickable but non-functional control.src/pages/Captcha/api/submitCaptchaVerification.ts-16-28 (1)
16-28:⚠️ Potential issue | 🟡 Minor공백 토큰도 빈 값으로 처리해 주세요.
Line 16의
!token검사는' '같은 공백 문자열을 통과시켜 잘못된captchaToken요청을 보낼 수 있습니다.🐛 제안 수정
export async function submitCaptchaVerification({ token, }: SubmitCaptchaVerificationPayload): Promise<CaptchaVerifyResponse> { - if (!token) { + const captchaToken = token.trim(); + + if (!captchaToken) { return { message: 'Captcha token is empty.', success: false, }; } @@ const guestUuid = useGuestStore.getState().ensureGuestUuid(); const response = await axiosBe1.post(apiEndpoints.captchaVerify, { - captchaToken: token, + captchaToken, guestUuid, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/api/submitCaptchaVerification.ts` around lines 16 - 28, The current check uses `!token` which allows whitespace-only strings like `' '` to pass; update the validation in submitCaptchaVerification to trim the `token` (e.g., const trimmed = token?.trim()) and treat an empty trimmed string as missing, returning the same `{ message: 'Captcha token is empty.', success: false }` when trimmed is empty before calling `useGuestStore.getState().ensureGuestUuid()` or `axiosBe1.post(apiEndpoints.captchaVerify, ...)`; ensure subsequent code uses the trimmed value for `captchaToken`.src/pages/Result/lib/toResultPageData.ts-25-41 (1)
25-41:⚠️ Potential issue | 🟡 MinorSSL 인증서 날짜의 로컬 타임존 변환 이슈.
new Date(rawDate)후getFullYear/getMonth/getDate는 로컬 타임존 기준입니다. 인증서의validFrom/validTo는 보통 UTC(ISOZ)로 오기 때문에, 사용자의 타임존에 따라 하루 밀려 표시될 수 있습니다(예:2024-01-01T00:00:00Z→ KST 환경에서는2024.01.01, UTC−5에서는2023.12.31).인증서 유효기간은 UTC 기준으로 표기하는 것이 일반적이므로
getUTC*를 사용하는 것을 권장합니다.♻️ 예시 패치
- const year = parsedDate.getFullYear(); - const month = `${parsedDate.getMonth() + 1}`.padStart(2, '0'); - const day = `${parsedDate.getDate()}`.padStart(2, '0'); + const year = parsedDate.getUTCFullYear(); + const month = `${parsedDate.getUTCMonth() + 1}`.padStart(2, '0'); + const day = `${parsedDate.getUTCDate()}`.padStart(2, '0');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Result/lib/toResultPageData.ts` around lines 25 - 41, The function formatDateOnlyLabel currently parses rawDate with new Date(rawDate) and uses getFullYear/getMonth/getDate which are local-time methods causing UTC dates (like certificate validFrom/validTo) to shift by timezone; update formatDateOnlyLabel to use the UTC date accessors (getUTCFullYear, getUTCMonth, getUTCDate) when building year/month/day so certificate dates render consistently in UTC, and keep the existing null/invalid-date handling in place.src/shared/store/guestStore.ts-12-18 (1)
12-18:⚠️ Potential issue | 🟡 Minor
Math.random()폴백은 암호학적으로 안전하지 않음.
guestUuid는 스캔 이력 조회 헤더(guest_uuid)와 캡차 검증 바디에서 사용자를 식별하는 값으로 쓰이므로, 예측 가능하거나 충돌 가능한 난수로 생성되면 다른 게스트의 이력과 섞일 위험이 있습니다.crypto.randomUUID가 없는 환경에서도crypto.getRandomValues는 대부분 사용 가능하므로 이를 이용한 폴백을 권장합니다. CodeQL 경고(line 71)와도 일치합니다.🛡️ 제안 수정
function createGuestUuid(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } - return `guest-${Date.now()}-${Math.random().toString(16).slice(2)}`; + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + // RFC4122 v4 + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + } + + return `guest-${Date.now()}-${Math.random().toString(16).slice(2)}`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/store/guestStore.ts` around lines 12 - 18, The current createGuestUuid() uses Math.random() for a fallback which is not cryptographically secure; replace that fallback with a UUIDv4 generation using crypto.getRandomValues when crypto.randomUUID is unavailable: detect that crypto and crypto.getRandomValues exist, generate 16 secure random bytes, set the RFC4122 variant/version bits and format into the standard UUID string, and only if neither crypto.randomUUID nor crypto.getRandomValues are available (very rare) fall back to the existing Date/Math-based string; update the createGuestUuid function accordingly.src/shared/api/errors/apiError.ts-43-69 (1)
43-69:⚠️ Potential issue | 🟡 Minor제네릭
Error경로의 빈 메시지 처리 보완58–63번:
error instanceof Error분기에서error.message를 바로 사용하고 있습니다. Axios 경로의resolveAxiosErrorMessage에서는 메시지를 trim한 후 비어 있으면 기본값으로 폴백하지만, 제네릭 Error 경로는error.message가 빈 문자열이거나 공백만 있어도 그대로 전달됩니다. 일관성을 위해 제네릭 Error 경로도 trim 후 비어 있으면'Unknown API error.'로 폴백시키는 것이 좋습니다.제안 수정
if (error instanceof Error) { + const message = + typeof error.message === 'string' && error.message.trim().length > 0 + ? error.message + : 'Unknown API error.'; return new ApiError({ cause: error, - message: error.message, + message, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/errors/apiError.ts` around lines 43 - 69, In toApiError, the generic Error branch uses error.message directly which can be empty/whitespace; update that branch to trim the message and fallback to "Unknown API error." if the trimmed string is empty (same behavior as resolveAxiosErrorMessage). Specifically, inside toApiError's `if (error instanceof Error)` branch, compute a trimmed message from `error.message`, and pass that trimmed value or `'Unknown API error.'` into the new ApiError construction (preserve setting `cause: error`).src/shared/store/scanProgressStore.ts-160-169 (1)
160-169:⚠️ Potential issue | 🟡 Minor완료 상태로 전환할 때 이전 오류 메시지도 지워주세요.
setCompleted()는 Zustand merge 특성상 기존errorMessage를 유지합니다. 오류 후 재시도/완료 플로우에서 완료 상태인데도 이전 오류 문구가 남을 수 있습니다.수정 제안
setCompleted: () => { set({ backendMessage: '분석이 완료되었습니다.', backendStatus: 'completed', backendStep: 'completed', completedStepIds: [...mappedLoadingStepOrder], currentStepId: 'completed', + errorMessage: null, percent: 100, status: 'completed', }); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/store/scanProgressStore.ts` around lines 160 - 169, The setCompleted function currently sets final state fields but leaves any previous errorMessage intact due to Zustand's merge behavior; update setCompleted (in scanProgressStore.ts) to explicitly clear errorMessage (and any related error fields if present, e.g., errorCode) when setting completed state so prior errors are not shown after a successful retry/completion.
🧹 Nitpick comments (13)
src/shared/api/types/analysisResponse.ts (1)
1-3: 느슨한 타입 별칭에 대한 추후 강화 권장.
Record<string, unknown>은 런타임 매퍼(toResultPageData,historyItemAccess등)에서 필드를 좁히는 경계 타입으로는 적절하지만, 백엔드 응답 스키마가 안정되면 최소한의 필수 필드(예:scanId,status,riskLevel,score)를 포함한 구조적 타입으로 승격시키는 편이 타입 안전성에 도움이 됩니다. 지금은 placeholder로 두고, API 계약이 확정된 시점에 좁혀 나가시는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/types/analysisResponse.ts` around lines 1 - 3, The types BackendAnalysisDetailResponse and BackendHistoryItemResponse are currently loose Record<string, unknown> placeholders; update them to structural interfaces with required minimal fields (e.g., scanId, status, riskLevel, score) that match the backend contract so downstream mappers like toResultPageData and historyItemAccess can rely on typed properties; if the full schema isn't finalized, create incremental interfaces (e.g., BackendAnalysisDetailResponse { scanId: string; status: string; riskLevel?: string; score?: number; } and similarly for BackendHistoryItemResponse) and adjust any parsing/casting in toResultPageData/historyItemAccess to use these concrete types or narrow with runtime guards.src/pages/Captcha/api/captchaVerifyResponse.test.ts (1)
5-40: 테스트 정책 정합성 및 커버리지 보강 권장.
captchaVerifyResponse.ts의 기본값을 fail-closed로 변경하면 Line 6–16의 첫 번째 케이스(guestUuid/message만 존재)는 기대값을success: false로 바꿔야 합니다. 현재 테스트는 현 구현의 "모호 → 성공" 정책을 고정시키고 있어 관련 수정을 막는 회귀 가드로 작용합니다.- 아래 추가 케이스를 보강해두면 분기 커버리지가 높아집니다.
{ status: 'success' }→success: true{ status: 'failed' }→success: false{ status: 'pending' }→ (정책에 따른 fallback)- 명시
success: true인데error_code가 함께 오는 충돌 케이스의 우선순위🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/api/captchaVerifyResponse.test.ts` around lines 5 - 40, Update the tests for toCaptchaVerifyResponse to align with the new fail-closed default: change the first case expecting success for payloads with only guestUuid/message to expect success: false, and add explicit unit tests covering status-based branches and conflict resolution: add cases for {status: 'success'} -> success: true, {status: 'failed'} -> success: false, {status: 'pending'} -> expected fallback per policy, and a conflict case where success: true is present alongside an error_code to assert which takes priority; reference the toCaptchaVerifyResponse function in captchaVerifyResponse.ts when adding these assertions.src/pages/Report/constants/riskDetectionCatalog.ts (1)
32-46: 미매핑 폴백 제목에서 원시riskType노출 시 사용자 표시 품질 저하 가능.폴백 경로에서
title: \${riskType} 감지``로 원시 값을 그대로 붙이므로 다음 케이스에서 표시 품질이 떨어집니다.
riskType이 빈 문자열/공백만 있으면감지또는감지처럼 선행 공백이 남습니다.riskType이ml_anomaly같은 백엔드 내부 키일 때 사용자에게 그대로 노출됩니다.최소 trim을 적용하고, 비어 있으면 톤별 폴백 제목(
unknownThreatTextByTone[riskLevel].title)을 그대로 쓰는 편이 안전합니다.♻️ 제안 수정
- return { - ...unknownThreatTextByTone[riskLevel], - title: `${riskType} 감지`, - }; + const trimmedRiskType = riskType.trim(); + const fallback = unknownThreatTextByTone[riskLevel]; + return { + ...fallback, + title: trimmedRiskType ? `${trimmedRiskType} 감지` : fallback.title, + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Report/constants/riskDetectionCatalog.ts` around lines 32 - 46, The fallback branch in resolveRiskDetectionContent exposes the raw riskType in the title (title: `${riskType} 감지`), which can produce poor UI when riskType is empty/whitespace or an internal key; trim riskType and if the trimmed value is empty use unknownThreatTextByTone[riskLevel].title instead of composing from riskType; implement this by calling normalize/trim on riskType (or use normalizeRiskType) to get a safe label, then if that label is truthy use `${label} 감지` otherwise fall back to unknownThreatTextByTone[riskLevel].title, keeping the rest of the returned object merging with unknownThreatTextByTone[riskLevel].package.json (1)
67-70:pnpm-lock.yaml에 안전한 버전이 고정되어 있음을 확인하세요.Line 67의
axios@^1.15.1과 Line 70의zustand@^.0.12는pnpm-lock.yaml에서 각각1.15.1과5.0.12로 안전하게 고정되어 있습니다. 알려진 Axios 악성 버전(1.14.1, 0.30.4)이나plain-crypto-js주입은 발견되지 않습니다. 다만 배포 경로에서 frozen lockfile 설치가 보장되지 않는다면 exact pinning(1.15.1,5.0.12)으로 변경하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 67 - 70, Dependencies axios and zustand in package.json are using caret ranges (^1.15.1 and ^5.0.12) which can be loosened in environments that don’t enforce a frozen lockfile; update the package.json entries for "axios" and "zustand" to exact pinned versions ("1.15.1" and "5.0.12") and ensure your CI install step uses pnpm install --frozen-lockfile (or otherwise enforces pnpm-lock.yaml) so the resolved safe versions in pnpm-lock.yaml are always used.src/pages/ScanList/ScanListPage.tsx (1)
112-146: 결과 페이지 이동은 TanStack Router 일관성을 위해 클라이언트 라우팅으로 변경하세요.현재
<a href>는 전체 문서 새로고침을 발생시킵니다. 앱의 다른 페이지들(QRScanPage, ResultPage 등)에서 이미 TanStack Router의Link/useNavigate를 사용하고 있으므로, ScanListPage에서도 일관성 있게 클라이언트 라우팅으로 변경하는 것이 좋습니다. 추가로 불필요한 전체 페이지 리로드를 피하고 SPA의 이점을 유지할 수 있습니다.제안 수정
+import { Link } from '@tanstack/react-router'; + ... - <a + <Link className={`${styles.card} ${styles.cardTone[item.status]}`} - href={`${resultRouteByStatus[item.status]}?url=${encodeURIComponent(item.url)}`} + search={{ url: item.url }} + to={resultRouteByStatus[item.status]} onClick={() => { handleSelectScanResult(item); }} > ... - </a> + </Link>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ScanList/ScanListPage.tsx` around lines 112 - 146, Replace the plain anchor that causes a full reload with TanStack Router client navigation: import and use Link (or useNavigate) instead of <a>, and navigate to resultRouteByStatus[item.status] + '?url=' + encodeURIComponent(item.url>); keep the existing onClick handler handleSelectScanResult(item) (or call it from the Link's onClick) and retain the same className, badge, icon and content rendering (statusLabelByTone, qrIconByTone, CalendarIcon, scannedAt) so the card still looks the same but performs client-side routing via TanStack Router.src/shared/api/axios/createAxios.ts (1)
17-22:ensureGuestUuid()대신readGuestUuidFromStorage()를 사용하면 헤더가 누락될 수 있습니다.앱 부트스트랩에서
ensureGuestUuid()가 호출되어 초기 렌더링 전에 UUID가 생성되므로 현재 코드는 안전합니다. 다만 인터셉터에서 직접ensureGuestUuid()를 호출하는 것이 더 견고한 설계입니다. 이렇게 하면 부트스트랩 호출에 대한 의존성을 제거하고, 코드 리팩토링 시에도 UUID 헤더가 항상 설정됨을 보장할 수 있습니다.제안 수정
-import { readGuestUuidFromStorage } from '@/shared/store/guestStore'; +import { ensureGuestUuid } from '@/shared/store/guestStore'; ... instance.interceptors.request.use((config) => { - const guestUuid = readGuestUuidFromStorage(); + const guestUuid = ensureGuestUuid(); if (guestUuid) { config.headers.set('guest_uuid', guestUuid);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/axios/createAxios.ts` around lines 17 - 22, 대응: request 인터셉터에서 readGuestUuidFromStorage() 대신 ensureGuestUuid()를 호출해 항상 UUID가 존재하도록 보장하세요; 구체적으로 instance.interceptors.request.use 콜백 내부에서 먼저 await ensureGuestUuid()를 실행한 후(또는 ensureGuestUuid()가 동기면 바로 호출) 그 다음 readGuestUuidFromStorage()로 값을 읽어 config.headers.set('guest_uuid', guestUuid)을 수행하도록 변경해 부트스트랩 호출에 의존하지 않고도 항상 헤더가 설정되게 만드세요.src/pages/Captcha/CaptchaPage.tsx (1)
38-47: 토큰 준비 전에는 제출 버튼을 비활성화하는 편이 좋겠습니다.Line 40은
isVerifying만 보므로 캡차 완료 전에도 제출이 가능하고, 빈 토큰 오류 메시지를 사용자에게 노출할 수 있습니다.useCaptchaPage에서canSubmit같은 상태를 반환해 같이 반영해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/CaptchaPage.tsx` around lines 38 - 47, Disable the submit button until the captcha token is ready by using the readiness flag from useCaptchaPage (e.g., canSubmit) in addition to isVerifying; update the button's disabled prop to disabled={!canSubmit || isVerifying} and ensure handleSubmit still gets called as before, and return/rename the readiness state from useCaptchaPage if necessary so the component can access canSubmit.src/shared/api/types/sseEvents.test.ts (1)
5-58: 반환 payload도 함께 검증하면 테스트 신뢰도가 더 올라갑니다.현재는
type만 확인해서,progress/final/error로 분류된 뒤 필요한 payload 필드가 누락되거나 변형되는 회귀를 잡기 어렵습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/types/sseEvents.test.ts` around lines 5 - 58, The tests only assert parsedMessage.type — update each spec that calls parseSseMessage (references: parseSseMessage and the parsedMessage variable) to also assert the returned payload fields match the input payload: for progress cases assert parsedMessage.step, parsedMessage.status, and parsedMessage.message (or guestUuid when present); for the completed-step case assert parsedMessage.step and status; for the final case assert parsedMessage.originalUrl, parsedMessage.redirect, parsedMessage.riskLevel, and parsedMessage.score; and for the error case assert parsedMessage.message and parsedMessage.status to ensure payload integrity.src/shared/store/scanProgressStore.test.ts (1)
10-84: LGTM — SSE 스텝 매핑/종료 처리 커버리지 양호.
COMPLETED일 때만completedStepIds에 적재되는 조건부 로직과 터미널 스텝(completed) 처리,setCompleted()일괄 완료 처리까지 의미 있는 케이스가 모두 검증되어 있습니다.다만 한 가지 경계 케이스로, 알 수 없는
step이 들어왔을 때(예: 백엔드가 새 스텝을 추가한 경우)의 graceful handling을 검증하는 테스트도 추가하시면 회귀 방지에 도움이 될 것 같습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/store/scanProgressStore.test.ts` around lines 10 - 84, Add a test that verifies graceful handling when updateFromProgressEvent receives an unknown step string: call useScanProgressStore.getState().updateFromProgressEvent with a made-up step (e.g., 'NEW_BACKEND_STEP') and both IN_PROGRESS and COMPLETED statuses, then assert that state.completedStepIds does not include an unintended mapping, state.backendStep/backendStatus/backendMessage reflect the raw backend values, and currentStepId/status remain stable (or map to a safe default) to prevent regressions in useScanProgressStore.updateFromProgressEvent and related selectors like completedStepIds/currentStepId/status.src/pages/Result/ui/ResultStatusPage.tsx (1)
88-90: 푸터 연도/버전 하드코딩.
(c) 2024가 문자열에 박혀 있어 해가 바뀌면 수동으로 갱신해야 합니다. 버전 문자열과 함께 상수(또는 빌드 시 주입 값)로 분리하고 연도는 동적으로 계산하는 것을 권장드립니다.♻️ 예시 패치
- <p className={styles.footer}> - Veri-Q Security Engine v2.4.1 (c) 2024. All rights reserved. - </p> + <p className={styles.footer}> + {`Veri-Q Security Engine v${ENGINE_VERSION} (c) ${new Date().getFullYear()}. All rights reserved.`} + </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Result/ui/ResultStatusPage.tsx` around lines 88 - 90, 현재 ResultStatusPage 컴포넌트의 푸터 문구에 "(c) 2024"와 버전이 하드코딩되어 있어 연도/버전 갱신이 수동입니다; ResultStatusPage의 푸터(<p className={styles.footer}>)에서 연도는 new Date().getFullYear()로 동적으로 계산하고 버전 문자열은 상수(VERSION)나 빌드 주입 환경변수로 분리하여 템플릿 문자열로 조합하도록 변경하세요 (예: const currentYear = new Date().getFullYear(); const VERSION = process.env.APP_VERSION ?? 'v2.4.1') — 하드코딩된 "Veri-Q Security Engine v2.4.1 (c) 2024" 텍스트를 해당 변수들을 사용한 조합으로 교체하고, 필요한 경우 상단에서 VERSION 상수를 export/import하거나 환경변수 주입을 문서화하세요.src/pages/ResultNonUrl/hooks/useResultNonUrlPage.ts (1)
32-60:useReportPage와 동일한 재실행 리스크 — 공통화 고려.
useReportPage.ts에 남긴 코멘트와 동일하게,[message, navigate]deps에 의한 중복 로드 가능성을 확인해 주세요. 또한SCAN_SESSION_REQUIRED분기 +showApiError+/로 리다이렉트 +handleRescan로직이useReportPage,useResultPageBase와 거의 동일해 보이므로 공용 훅(예:useSessionGuardedPageData)으로 추출하면 중복을 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ResultNonUrl/hooks/useResultNonUrlPage.ts` around lines 32 - 60, The effect in useResultNonUrlPage (loadResultNonUrlPageData) risks duplicate reloads because it depends on [message, navigate] and duplicates logic found in useReportPage/useResultPageBase (SCAN_SESSION_REQUIRED handling, showApiError, redirect, handleRescan); refactor by extracting the common pattern into a new shared hook (e.g., useSessionGuardedPageData) that accepts the page-specific fetcher (fetchResultNonUrlPageData), message, navigate, and optional handleRescan, handles isMounted, error handling for 'SCAN_SESSION_REQUIRED' (calling navigate('/') directly), calls showApiError on other errors, and returns the loaded data and a reload function; replace the effect and loadResultNonUrlPageData in useResultNonUrlPage with a call to useSessionGuardedPageData to remove duplicated code and avoid re-run risk.src/routes/router.tsx (1)
12-17: lazy route 로딩 중 빈 화면 대신 fallback UI를 보여 주세요.현재
fallback={null}이라 느린 네트워크에서 페이지 전환 중 아무것도 보이지 않습니다. 최소한의 로딩 표시를 두면 라우트 lazy-loading UX가 더 안정적입니다.♻️ 제안 수정
function RootLayout() { return ( - <Suspense fallback={null}> + <Suspense fallback={<div aria-live="polite">페이지를 불러오는 중...</div>}> <Outlet /> </Suspense> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/router.tsx` around lines 12 - 17, RootLayout currently uses <Suspense fallback={null}>, which leaves a blank screen during lazy route loading; replace the null fallback with a lightweight loading UI (e.g., a <LoadingSpinner /> or simple <div>Loading...</div>) so users see progress while routes load. Update the Suspense fallback in the RootLayout function to render that loading component, and if no loading component exists create a small presentational component (e.g., Loading or RouteLoader) and import/use it in RootLayout.src/pages/ScanList/hooks/useScanListPage.ts (1)
17-22: 렌더 중 스토어set호출은 React 안티패턴.
state.guestUuid가null일 때 렌더 경로에서ensureGuestUuid()를 호출하면 내부에서set({ guestUuid: … })이 실행되어 렌더 중에 스토어를 변경합니다.useGuestStore를 구독하는 다른 컴포넌트가 동시에 렌더 중이라면 "Cannot update a component while rendering a different component" 경고가 발생할 수 있고, Strict Mode의 더블 렌더와 맞물려 예기치 않은 상태 변경이 발생할 수 있습니다.
src/main.tsx에서 이미 부팅 시ensureGuestUuid()를 호출하므로, 본 훅에서는 단순 구독만 하고, 추가 초기화가 필요하다면useEffect내부로 옮기는 편이 안전합니다.♻️ 제안
- const guestUuid = - useGuestStore((state) => state.guestUuid) ?? useGuestStore.getState().ensureGuestUuid(); + const guestUuid = useGuestStore((state) => state.guestUuid); + + useEffect(() => { + if (!guestUuid) { + useGuestStore.getState().ensureGuestUuid(); + } + }, [guestUuid]);이후
getInitialScanListPageData(guestUuid)는guestUuid ?? ''등으로 null-safe 하게 처리해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ScanList/hooks/useScanListPage.ts` around lines 17 - 22, The hook currently calls useGuestStore.getState().ensureGuestUuid() during render via the guestUuid initializer, which can trigger a store.set during render; change to only subscribe to guestUuid with useGuestStore((s)=>s.guestUuid) and move any ensureGuestUuid() invocation into a useEffect so it runs after mount (e.g., call useGuestStore.getState().ensureGuestUuid() inside useEffect). Also make getInitialScanListPageData null-safe by passing guestUuid ?? '' (or otherwise handling null) when initializing scanListPageData so the initializer no longer depends on triggering state changes during render; keep setHistorySelection usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.env.example:
- Around line 4-6: The example environment values VITE_BE1_BASE_URL and
VITE_BE3_BASE_URL currently use http and concrete IPs; update the .env.example
to use HTTPS-safe placeholders (e.g., https://your-backend.example.com or
https://BE1_BASE_URL and https://BE3_BASE_URL) so consumers replace them with
their deployed HTTPS endpoints and avoid mixed-content issues in browsers; edit
the VITE_BE1_BASE_URL and VITE_BE3_BASE_URL entries accordingly.
In `@scripts/setupEnv.mjs`:
- Around line 20-31: The defaultEnvContent currently hardcodes production IPs
and plain HTTP endpoints (VITE_BE1_BASE_URL, VITE_BE3_BASE_URL, etc.) — replace
those specific IPs and http:// values with non-identifying placeholders or
localhost defaults (e.g., blank, PLACEHOLDER_BE1_URL, or http://localhost:8081)
and update the same variables (VITE_BE1_BASE_URL, VITE_BE3_BASE_URL,
VITE_API_TIMEOUT_MS, VITE_UPLOAD_TIMEOUT_MS, VITE_SSE_RECONNECT_MAX) in
scripts/setupEnv.mjs so the fallback template contains no hardcoded infra IPs or
plaintext external endpoints; ensure comments/documentation in the template
explain that real values must be provided via .env or secret management.
In `@src/features/scan-url/api/uploadErrors.ts`:
- Around line 13-16: The current captcha check only inspects
error.data.error_code and error.message; extend it to also normalize and inspect
payload fields like data.message, data.detail, data.error (and any nested
strings returned by pickString) so the final variables (errorCode and message)
include those payload values before testing for 'captcha'/'캡차'; update the logic
around normalizeValue(pickString(...)) and the message extraction to combine
error.message plus pickString(error.data, ['message','detail','error', ...]) so
the subsequent includes(...) checks catch captcha indicators in those payload
fields as well.
In `@src/pages/Captcha/api/captchaVerifyResponse.ts`:
- Around line 30-57: The toCaptchaVerifyResponse function currently treats
ambiguous cases as success; update its decision logic so that when
explicitSuccess (from pickBoolean) is null/undefined and there is no errorCode
and status is not in successStatuses, it defaults success = false (fail-closed)
instead of true; adjust the conditional block around explicitSuccess in
toCaptchaVerifyResponse and ensure references to pickBoolean, pickString,
normalizeStatus, failureStatuses, and successStatuses are used to determine
failure before setting success true; also update the test suite
(captchaVerifyResponse.test.ts) to reflect the new default behavior for the case
that previously treated empty/unknown payloads as success.
In `@src/pages/Loading/hooks/useLoadingPage.ts`:
- Around line 123-125: The code currently swallows any non-404 errors from
ensureScanDetail by treating them the same as a 404 and proceeding; update the
logic so only a 404 from isApiError(error) allows falling back to default
results — for all other errors (network/server/unknown), do not set
detailResolutionStartedRef.current = false and do not let onFinal complete the
flow: instead surface the error (rethrow or set an explicit error state) and
stop progression so the UI can show an error or offer retry; change the branch
around ensureScanDetail, isApiError, and error.statusCode checks to only treat
statusCode === 404 as the fallback path and handle other cases by propagating
the error, and apply the same fix to the similar block referenced around lines
218-228.
- Around line 175-188: startPolling currently re-schedules itself indefinitely
because resolveDetailAndOpenResult() returning false after 60 404s does not
prevent further timeouts; add a global polling guard (e.g., maxAttempts or
overallPollingDeadline) and an attempts counter or deadline check inside
startPolling so that when the maxAttempts is reached or the deadline elapsed you
do not set pollTimerId or call startPolling again, and instead mark
detailResolutionStartedRef.current or isDisposed (or call a new
setFinalPollError state) and clear any existing pollTimerId; update references
in startPolling, resolveDetailAndOpenResult, pollTimerId,
DETAIL_POLL_START_DELAY_MS and detailResolutionStartedRef to enforce and persist
the final error/timeout state so polling stops permanently when the
limit/deadline is hit.
In `@src/pages/QRScan/hooks/useQRScanPage.ts`:
- Around line 138-147: The isNonWebScanResponse function only checks camelCase
fields (schemeType, isUrl) so backend responses using snake_case (scheme_type,
is_url) are misclassified; update isNonWebScanResponse to also read and
normalize scheme_type and is_url (e.g., check scanResponse.schemeType ||
scanResponse.scheme_type and scanResponse.isUrl !== undefined ||
scanResponse.is_url !== undefined) and use those normalized values when
computing schemeType and the boolean isUrl, preserving the existing logic that
treats non-boolean isUrl and non-empty non-"WEB" schemeType as non-web.
In `@src/pages/Result/hooks/useResultPageBase.ts`:
- Around line 44-54: In useResultPageBase's catch block add the same isMounted
guard used in the success path: at the top of the catch handler check if
(!isMounted) return; so that console.error, showApiError, and navigate are not
executed after the component unmounts; ensure the guard uses the same isMounted
variable referenced in the success path and place it before any calls to
console.error, showApiError, or navigate.
In `@src/shared/api/apiConfig.ts`:
- Around line 21-35: getBe1BaseUrl and getBe3BaseUrl silently return an empty
string when VITE_BE1_BASE_URL or VITE_BE3_BASE_URL are missing which causes
relative requests; update these functions (or update trimEnvValue they call) to
validate the trimmed value and throw a clear Error when the env var is empty or
undefined (include which variable name in the message) so initialization fails
loudly in production instead of returning ''. Ensure the thrown error occurs
only when import.meta.env.DEV is false and reference getBe1BaseUrl,
getBe3BaseUrl and trimEnvValue when applying the check.
In `@src/shared/api/fetchScanDetail.ts`:
- Around line 6-9: The current fetch in fetchScanDetail.ts sends the entire
target URL as a GET query param (axiosBe3.get to apiEndpoints.scanDetail with
params: { url }), which risks leaking sensitive data and hitting URL length
limits; update the client to first check with the backend whether a POST-based
lookup or opaque scanId lookup is available and, if so, switch to using a POST
payload or scanId; if the GET contract cannot change, add client-side URL length
validation (reject or truncate >2000 chars) and ensure any logging/middleware
(axios interceptors or places that reference url) masks the URL before logging;
locate and modify the axiosBe3.get call and related fetchScanDetail logic to
implement these checks and masking.
In `@src/shared/api/responseAccess/payloadAccess.ts`:
- Around line 53-67: getCandidateRecords only inspects the root record and
one-level nested keys, missing deeper envelopes like {data:{result:{score}}};
update getCandidateRecords to recursively traverse nested envelopes by walking
records discovered via nestedRecordKeys (using asRecord) and pushing unique
results with pushUniqueRecord until no new nested records are found, so the
function returns all candidate UnknownRecord objects at any depth; keep usage of
asRecord, nestedRecordKeys and pushUniqueRecord to locate and add nested
records.
- Around line 103-108: The code currently uses Number.parseFloat(value) which
accepts malformed strings like "12abc"; update the string-to-number parsing
logic in payloadAccess.ts so it only accepts fully numeric strings (e.g. use a
strict regex like /^-?\d+(\.\d+)?$/ or Number(value) combined with a full-match
check) before calling parseFloat/Number, and keep the subsequent
Number.isFinite(parsedValue) guard; apply this to the parsing branch that
handles value (the variable checked with typeof value === 'string') so consumers
such as scanProgressStore (percent/progress) and resolveResultTone
(trustScore/score) only get valid numeric values.
In `@src/shared/api/types/sseEvents.ts`:
- Around line 55-95: The code currently prefers payloadType over the explicit
SSE event type and checks for progress keys before final/result detection,
causing an explicit "final" event to be treated as progress; change the
candidateType assignment to prefer the explicit event type (use
normalizedEventType first, then payloadType) and move the
final/result/complete/done candidateType check so it runs before the
hasAnyDefinedKey progress check and before the candidateType
progress/status/step/update check; keep the error checks first (payloadStatus
and candidateType error checks) and then run the final check, then the
progress-key and progress-candidate checks. Ensure you update references to
candidateType, payloadType, normalizedEventType, payloadStatus, payloadRecord,
and hasAnyDefinedKey accordingly.
In `@src/shared/lib/sse/useScanSubscription.ts`:
- Around line 193-204: The onopen handler in useScanSubscription currently
resets reconnectAttempts immediately, allowing a flapping connection to bypass
the reconnect limit; change the logic so reconnectAttempts is only reset after
the connection is proven stable — e.g., reset reconnectAttempts in
handleParsedMessage when a first valid 'message' arrives or after a minimum
stable period (start a stability timer in onopen, e.g., 5s, and clear it on
onerror), keep resetInactivityTimer behavior, and ensure
scheduleReconnect/onerror increments reconnectAttempts as before; update
references in useScanSubscription (onopen, onmessage/handleParsedMessage,
onerror/scheduleReconnect, reconnectAttempts, resetInactivityTimer) accordingly
so the counter is not zeroed immediately on every open but only after the
stability condition is met.
In `@src/shared/store/scanProgressStore.ts`:
- Around line 171-179: The setConnecting function currently only resets
backendMessage/backendStatus/backendStep/errorMessage/status but leaves percent,
currentStepId, and completedStepIds from any previous scan, causing a new
connection to appear complete; modify setConnecting to also reset percent to 0
(or null per store convention), clear currentStepId, and clear completedStepIds
so a new scan starts with a clean progress state (update the setConnecting
function in scanProgressStore to include resets for percent, currentStepId, and
completedStepIds).
- Around line 206-245: When a terminal "completed" backend event arrives with no
current step, the code keeps nextCurrentStepId as the previous step so
isCompleted can be false; update the set block to coerce nextCurrentStepId to
'completed' when the incoming status is a terminal/completed status and no
explicit currentStepId was provided. Concretely: after computing
nextCurrentStepId (from currentStepId ?? state.currentStepId) check if status
indicates completed (the same condition used elsewhere for completed terminal
events) and if currentStepId is null/undefined, then set nextCurrentStepId =
'completed' before calling deriveCompletedStepIds/derivePercent and computing
isCompleted so the UI transitions to completed even when the backend only sent {
status: "COMPLETED" }.
In `@src/shared/store/scanSessionStore.ts`:
- Around line 17-21: ScanHistorySelection currently loses schemeType/isUrl and
setHistorySelection forcibly sets isUrl: true and schemeType: 'WEB', causing
non-URL history items to be treated as URLs; update the ScanHistorySelection
type to include schemeType and isUrl, change setHistorySelection to preserve and
accept those fields instead of forcing WEB/isUrl=true, and update all callers to
pass through the history payload's schemeType and isUrl (also fix the other
occurrence referenced around the same logic). Ensure you reference and modify
the ScanHistorySelection type, the setHistorySelection function, and any call
sites that build the history payload so the original scheme information is
preserved and used for URL-detection logic.
- Around line 51-57: The createScanSessionStorage function accesses
window.localStorage directly which can throw (e.g., SecurityError) in some
browsers; wrap the access in a try/catch and return noopStorage on any exception
or if localStorage is falsy. Specifically, inside createScanSessionStorage catch
exceptions when referencing window.localStorage and fall back to noopStorage,
preserving the existing typeof window === 'undefined' check and returning
window.localStorage only when safely accessible.
In `@vite.config.ts`:
- Around line 56-69: Replace the hardcoded proxy targets in the Vite config (the
server.proxy entries for '/be1' and '/be3' where target is
'http://34.64.182.89:8081' and ':8083') to read from environment variables using
Vite's loadEnv; read VITE_BE1_BASE_URL and VITE_BE3_BASE_URL (falling back to
the current literals only for local/dev fallback if desired), then set those
values as the proxy target for the corresponding entries while keeping
changeOrigin and the rewrite functions intact; ensure you import/use loadEnv in
the vite.config.ts setup so the proxy targets are sourced from process.env
(VITE_*) at config build time.
---
Outside diff comments:
In `@src/pages/Captcha/ui/CaptchaWidget.tsx`:
- Around line 113-150: The effect that loads and renders reCAPTCHA (the
useEffect using widgetContainerRef, loadEnterpriseScript,
window.grecaptcha?.enterprise, enterprise.ready and enterprise.render) lacks an
unmount/cleanup guard causing callbacks (enterprise.ready, enterprise.render
callbacks and the promise .catch) to call onTokenChange after the component is
unmounted or recaptchaSiteKey changes; fix by adding an isMounted/aborted flag
(e.g., let cancelled = false) at the top of the effect, check this flag before
calling onTokenChange or interacting with widgetContainerRef inside all promise
callbacks and inside enterprise.render callback functions, and in the returned
cleanup function set cancelled = true and optionally call
enterprise.reset/remove widget from container to prevent stray callbacks; ensure
checks reference the same flag in loadEnterpriseScript.then/catch,
enterprise.ready, and the render callbacks so no calls occur after unmount or
key change.
---
Minor comments:
In `@src/features/scan-history/lib/historyItemAccess.test.ts`:
- Around line 23-24: The test expectation currently assumes
buildHistoryItemId(historyItem, 0) omits the index, causing potential duplicate
IDs; update the implementation of buildHistoryItemId to incorporate the passed
index into the generated ID (e.g., append or embed the index) and then change
the test expectation in historyItemAccess.test.ts to match the new ID format
returned for buildHistoryItemId(historyItem, 0) so the ID includes the index and
is unique.
In `@src/pages/Captcha/api/submitCaptchaVerification.ts`:
- Around line 16-28: The current check uses `!token` which allows
whitespace-only strings like `' '` to pass; update the validation in
submitCaptchaVerification to trim the `token` (e.g., const trimmed =
token?.trim()) and treat an empty trimmed string as missing, returning the same
`{ message: 'Captcha token is empty.', success: false }` when trimmed is empty
before calling `useGuestStore.getState().ensureGuestUuid()` or
`axiosBe1.post(apiEndpoints.captchaVerify, ...)`; ensure subsequent code uses
the trimmed value for `captchaToken`.
In `@src/pages/Result/lib/toResultPageData.ts`:
- Around line 25-41: The function formatDateOnlyLabel currently parses rawDate
with new Date(rawDate) and uses getFullYear/getMonth/getDate which are
local-time methods causing UTC dates (like certificate validFrom/validTo) to
shift by timezone; update formatDateOnlyLabel to use the UTC date accessors
(getUTCFullYear, getUTCMonth, getUTCDate) when building year/month/day so
certificate dates render consistently in UTC, and keep the existing
null/invalid-date handling in place.
In `@src/pages/ResultNonUrl/ResultNonUrlPage.tsx`:
- Around line 39-45: The preview state can make displayedTargetValue undefined
and lead to a non-functional Execute button; update the render logic (where the
execute button is produced) to only show/enable it when the action is actually
executable—e.g., compute an isExecutable flag using displayedActionType ===
resultNonUrlPageData.detectedActionType && displayedTargetValue != null and use
that to conditionally render or disable the Execute button tied to
handleExecuteAction/resolveNonUrlActionExecution so users don’t see a clickable
but non-functional control.
In `@src/shared/api/errors/apiError.ts`:
- Around line 43-69: In toApiError, the generic Error branch uses error.message
directly which can be empty/whitespace; update that branch to trim the message
and fallback to "Unknown API error." if the trimmed string is empty (same
behavior as resolveAxiosErrorMessage). Specifically, inside toApiError's `if
(error instanceof Error)` branch, compute a trimmed message from
`error.message`, and pass that trimmed value or `'Unknown API error.'` into the
new ApiError construction (preserve setting `cause: error`).
In `@src/shared/store/guestStore.ts`:
- Around line 12-18: The current createGuestUuid() uses Math.random() for a
fallback which is not cryptographically secure; replace that fallback with a
UUIDv4 generation using crypto.getRandomValues when crypto.randomUUID is
unavailable: detect that crypto and crypto.getRandomValues exist, generate 16
secure random bytes, set the RFC4122 variant/version bits and format into the
standard UUID string, and only if neither crypto.randomUUID nor
crypto.getRandomValues are available (very rare) fall back to the existing
Date/Math-based string; update the createGuestUuid function accordingly.
In `@src/shared/store/scanProgressStore.ts`:
- Around line 160-169: The setCompleted function currently sets final state
fields but leaves any previous errorMessage intact due to Zustand's merge
behavior; update setCompleted (in scanProgressStore.ts) to explicitly clear
errorMessage (and any related error fields if present, e.g., errorCode) when
setting completed state so prior errors are not shown after a successful
retry/completion.
---
Nitpick comments:
In `@package.json`:
- Around line 67-70: Dependencies axios and zustand in package.json are using
caret ranges (^1.15.1 and ^5.0.12) which can be loosened in environments that
don’t enforce a frozen lockfile; update the package.json entries for "axios" and
"zustand" to exact pinned versions ("1.15.1" and "5.0.12") and ensure your CI
install step uses pnpm install --frozen-lockfile (or otherwise enforces
pnpm-lock.yaml) so the resolved safe versions in pnpm-lock.yaml are always used.
In `@src/pages/Captcha/api/captchaVerifyResponse.test.ts`:
- Around line 5-40: Update the tests for toCaptchaVerifyResponse to align with
the new fail-closed default: change the first case expecting success for
payloads with only guestUuid/message to expect success: false, and add explicit
unit tests covering status-based branches and conflict resolution: add cases for
{status: 'success'} -> success: true, {status: 'failed'} -> success: false,
{status: 'pending'} -> expected fallback per policy, and a conflict case where
success: true is present alongside an error_code to assert which takes priority;
reference the toCaptchaVerifyResponse function in captchaVerifyResponse.ts when
adding these assertions.
In `@src/pages/Captcha/CaptchaPage.tsx`:
- Around line 38-47: Disable the submit button until the captcha token is ready
by using the readiness flag from useCaptchaPage (e.g., canSubmit) in addition to
isVerifying; update the button's disabled prop to disabled={!canSubmit ||
isVerifying} and ensure handleSubmit still gets called as before, and
return/rename the readiness state from useCaptchaPage if necessary so the
component can access canSubmit.
In `@src/pages/Report/constants/riskDetectionCatalog.ts`:
- Around line 32-46: The fallback branch in resolveRiskDetectionContent exposes
the raw riskType in the title (title: `${riskType} 감지`), which can produce poor
UI when riskType is empty/whitespace or an internal key; trim riskType and if
the trimmed value is empty use unknownThreatTextByTone[riskLevel].title instead
of composing from riskType; implement this by calling normalize/trim on riskType
(or use normalizeRiskType) to get a safe label, then if that label is truthy use
`${label} 감지` otherwise fall back to unknownThreatTextByTone[riskLevel].title,
keeping the rest of the returned object merging with
unknownThreatTextByTone[riskLevel].
In `@src/pages/Result/ui/ResultStatusPage.tsx`:
- Around line 88-90: 현재 ResultStatusPage 컴포넌트의 푸터 문구에 "(c) 2024"와 버전이 하드코딩되어 있어
연도/버전 갱신이 수동입니다; ResultStatusPage의 푸터(<p className={styles.footer}>)에서 연도는 new
Date().getFullYear()로 동적으로 계산하고 버전 문자열은 상수(VERSION)나 빌드 주입 환경변수로 분리하여 템플릿 문자열로
조합하도록 변경하세요 (예: const currentYear = new Date().getFullYear(); const VERSION =
process.env.APP_VERSION ?? 'v2.4.1') — 하드코딩된 "Veri-Q Security Engine v2.4.1 (c)
2024" 텍스트를 해당 변수들을 사용한 조합으로 교체하고, 필요한 경우 상단에서 VERSION 상수를 export/import하거나 환경변수
주입을 문서화하세요.
In `@src/pages/ResultNonUrl/hooks/useResultNonUrlPage.ts`:
- Around line 32-60: The effect in useResultNonUrlPage
(loadResultNonUrlPageData) risks duplicate reloads because it depends on
[message, navigate] and duplicates logic found in
useReportPage/useResultPageBase (SCAN_SESSION_REQUIRED handling, showApiError,
redirect, handleRescan); refactor by extracting the common pattern into a new
shared hook (e.g., useSessionGuardedPageData) that accepts the page-specific
fetcher (fetchResultNonUrlPageData), message, navigate, and optional
handleRescan, handles isMounted, error handling for 'SCAN_SESSION_REQUIRED'
(calling navigate('/') directly), calls showApiError on other errors, and
returns the loaded data and a reload function; replace the effect and
loadResultNonUrlPageData in useResultNonUrlPage with a call to
useSessionGuardedPageData to remove duplicated code and avoid re-run risk.
In `@src/pages/ScanList/hooks/useScanListPage.ts`:
- Around line 17-22: The hook currently calls
useGuestStore.getState().ensureGuestUuid() during render via the guestUuid
initializer, which can trigger a store.set during render; change to only
subscribe to guestUuid with useGuestStore((s)=>s.guestUuid) and move any
ensureGuestUuid() invocation into a useEffect so it runs after mount (e.g., call
useGuestStore.getState().ensureGuestUuid() inside useEffect). Also make
getInitialScanListPageData null-safe by passing guestUuid ?? '' (or otherwise
handling null) when initializing scanListPageData so the initializer no longer
depends on triggering state changes during render; keep setHistorySelection
usage unchanged.
In `@src/pages/ScanList/ScanListPage.tsx`:
- Around line 112-146: Replace the plain anchor that causes a full reload with
TanStack Router client navigation: import and use Link (or useNavigate) instead
of <a>, and navigate to resultRouteByStatus[item.status] + '?url=' +
encodeURIComponent(item.url>); keep the existing onClick handler
handleSelectScanResult(item) (or call it from the Link's onClick) and retain the
same className, badge, icon and content rendering (statusLabelByTone,
qrIconByTone, CalendarIcon, scannedAt) so the card still looks the same but
performs client-side routing via TanStack Router.
In `@src/routes/router.tsx`:
- Around line 12-17: RootLayout currently uses <Suspense fallback={null}>, which
leaves a blank screen during lazy route loading; replace the null fallback with
a lightweight loading UI (e.g., a <LoadingSpinner /> or simple
<div>Loading...</div>) so users see progress while routes load. Update the
Suspense fallback in the RootLayout function to render that loading component,
and if no loading component exists create a small presentational component
(e.g., Loading or RouteLoader) and import/use it in RootLayout.
In `@src/shared/api/axios/createAxios.ts`:
- Around line 17-22: 대응: request 인터셉터에서 readGuestUuidFromStorage() 대신
ensureGuestUuid()를 호출해 항상 UUID가 존재하도록 보장하세요; 구체적으로
instance.interceptors.request.use 콜백 내부에서 먼저 await ensureGuestUuid()를 실행한 후(또는
ensureGuestUuid()가 동기면 바로 호출) 그 다음 readGuestUuidFromStorage()로 값을 읽어
config.headers.set('guest_uuid', guestUuid)을 수행하도록 변경해 부트스트랩 호출에 의존하지 않고도 항상 헤더가
설정되게 만드세요.
In `@src/shared/api/types/analysisResponse.ts`:
- Around line 1-3: The types BackendAnalysisDetailResponse and
BackendHistoryItemResponse are currently loose Record<string, unknown>
placeholders; update them to structural interfaces with required minimal fields
(e.g., scanId, status, riskLevel, score) that match the backend contract so
downstream mappers like toResultPageData and historyItemAccess can rely on typed
properties; if the full schema isn't finalized, create incremental interfaces
(e.g., BackendAnalysisDetailResponse { scanId: string; status: string;
riskLevel?: string; score?: number; } and similarly for
BackendHistoryItemResponse) and adjust any parsing/casting in
toResultPageData/historyItemAccess to use these concrete types or narrow with
runtime guards.
In `@src/shared/api/types/sseEvents.test.ts`:
- Around line 5-58: The tests only assert parsedMessage.type — update each spec
that calls parseSseMessage (references: parseSseMessage and the parsedMessage
variable) to also assert the returned payload fields match the input payload:
for progress cases assert parsedMessage.step, parsedMessage.status, and
parsedMessage.message (or guestUuid when present); for the completed-step case
assert parsedMessage.step and status; for the final case assert
parsedMessage.originalUrl, parsedMessage.redirect, parsedMessage.riskLevel, and
parsedMessage.score; and for the error case assert parsedMessage.message and
parsedMessage.status to ensure payload integrity.
In `@src/shared/store/scanProgressStore.test.ts`:
- Around line 10-84: Add a test that verifies graceful handling when
updateFromProgressEvent receives an unknown step string: call
useScanProgressStore.getState().updateFromProgressEvent with a made-up step
(e.g., 'NEW_BACKEND_STEP') and both IN_PROGRESS and COMPLETED statuses, then
assert that state.completedStepIds does not include an unintended mapping,
state.backendStep/backendStatus/backendMessage reflect the raw backend values,
and currentStepId/status remain stable (or map to a safe default) to prevent
regressions in useScanProgressStore.updateFromProgressEvent and related
selectors like completedStepIds/currentStepId/status.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/pages/Captcha/hooks/useCaptchaPage.ts (1)
29-45:⚠️ Potential issue | 🟡 Minor제출 경로에서도
canSubmit과 같은 trim 기준을 사용해 주세요.Line 22는 공백 토큰을 제출 불가로 보지만, Line 37은 원본
token의 존재 여부만 확인합니다. 직접 호출이나 예외적인 위젯 반환값에서 공백 토큰이 API까지 내려갈 수 있습니다.🐛 제안 수정
const handleSubmit = useCallback(async () => { setFeedbackMessage(null); + const trimmedToken = token?.trim() ?? ''; if (recaptchaSiteKey.length === 0) { setFeedbackMessage('reCAPTCHA 사이트 키가 비어 있습니다. `.env.local`을 확인해 주세요.'); return; } - if (!token) { + if (trimmedToken.length === 0) { setFeedbackMessage('캡차를 먼저 완료해 주세요.'); return; } setIsVerifying(true); try { - const result = await submitCaptchaVerification({ token }); + const result = await submitCaptchaVerification({ token: trimmedToken });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/hooks/useCaptchaPage.ts` around lines 29 - 45, handleSubmit currently only checks token truthiness before submitting, but canSubmit uses a trim-based emptiness rule; ensure handleSubmit applies the same trim check to prevent whitespace-only tokens reaching submitCaptchaVerification. Update the handleSubmit function to validate token.trim().length > 0 (or call the same canSubmit helper if available) before calling submitCaptchaVerification and set the same feedback message as used elsewhere when the trimmed token is empty.src/pages/Captcha/ui/CaptchaWidget.tsx (1)
142-172:⚠️ Potential issue | 🟠 Major
ready콜백 내에서enterprise.render()예외를 처리하세요.Line 174의
.catch()는 Promise 체인의 거부만 처리하며,ready()콜백 내에서 발생하는 동기 예외는 잡지 못합니다.enterprise.render()가 실패하면 위젯이 초기화되지 않은 상태로 남을 수 있으므로, 콜백 내에서 try-catch로 직접 처리해야 합니다.제안 수정
enterprise.ready(() => { if (cancelled || widgetContainerRef.current !== container) { return; } + try { widgetId = enterprise.render(container, { callback: (nextToken) => { if (cancelled) { return; } onTokenChange(nextToken); }, 'error-callback': () => { if (cancelled) { return; } onTokenChange(null); }, 'expired-callback': () => { if (cancelled) { return; } onTokenChange(null); }, sitekey: recaptchaSiteKey, theme: 'light', }); + } catch { + if (!cancelled) { + onTokenChange(null); + } + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/ui/CaptchaWidget.tsx` around lines 142 - 172, The ready callback passed to enterprise.ready should protect against synchronous exceptions from enterprise.render: wrap the call to enterprise.render(...) inside a try-catch within the enterprise.ready callback (where widgetContainerRef.current/ container are checked) so that if enterprise.render throws you clear/leave widgetId unset, check cancelled, call onTokenChange(null) (and optionally log the error) and return; only assign widgetId when render succeeds. Ensure the same canceled checks (cancelled, widgetContainerRef.current !== container) remain in place and that error handling covers both thrown exceptions and the existing error/expired callbacks.
♻️ Duplicate comments (2)
src/shared/api/types/sseEvents.ts (1)
55-139:⚠️ Potential issue | 🟠 Major터미널 payload 신호가 progress로 가려질 수 있습니다.
Line 55에서 이벤트 타입이 있으면
payload.type을 버리고,payload.status: "completed"같은 완료 상태도 final로 보지 않습니다. 또한 final-analysis 키 판정보다step/currentStepId판정이 먼저라 generic 이벤트의 최종 payload가 진행 메타를 함께 보내면useScanSubscription.ts에서onFinal대신onProgress로 라우팅될 수 있습니다.🐛 제안 수정
- const candidateType = normalizedEventType || payloadType; + const candidateTypes = [normalizedEventType, payloadType].filter(Boolean); + const hasCandidateType = (...types: string[]) => + candidateTypes.some((candidateType) => types.includes(candidateType)); - if (candidateType === 'error' || candidateType === 'failed' || candidateType === 'failure') { + if (hasCandidateType('error', 'failed', 'failure')) { return 'error'; } if (payloadStatus === 'error' || payloadStatus === 'failed' || payloadStatus === 'failure') { return 'error'; } if ( - candidateType === 'final' || - candidateType === 'result' || - candidateType === 'complete' || - candidateType === 'completed' || - candidateType === 'done' + hasCandidateType('final', 'result', 'complete', 'completed', 'done') || + payloadStatus === 'complete' || + payloadStatus === 'completed' || + payloadStatus === 'done' ) { return 'final'; } if ( payloadRecord && hasAnyDefinedKey(payloadRecord, [ + 'analysisTime', + 'analysis_time', + 'externalApi', + 'external_api', + 'https', + 'internalDb', + 'internal_db', + 'ml', + 'originalUrl', + 'original_url', + 'redirect', + 'riskLevel', + 'risk_level', + 'score', + 'scoring', + 'serverInfo', + 'server_info', + 'shortUrl', + 'short_url', + ]) + ) { + return 'final'; + } + + if ( + payloadRecord && + hasAnyDefinedKey(payloadRecord, [ 'currentStepId', 'current_step_id', 'step', 'stepId', 'step_id', @@ } if ( - candidateType === 'progress' || - candidateType === 'status' || - candidateType === 'step' || - candidateType === 'update' + hasCandidateType('progress', 'status', 'step', 'update') ) { return 'progress'; } - - if ( - payloadRecord && - hasAnyDefinedKey(payloadRecord, [ - 'analysisTime', - 'analysis_time', - 'externalApi', - 'external_api', - 'https', - 'internalDb', - 'internal_db', - 'ml', - 'originalUrl', - 'original_url', - 'redirect', - 'riskLevel', - 'risk_level', - 'score', - 'scoring', - 'serverInfo', - 'server_info', - 'shortUrl', - 'short_url', - ]) - ) { - return 'final'; - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/types/sseEvents.ts` around lines 55 - 139, The SSE event-type resolution currently lets progress-detecting heuristics run before final-detection (and ignores payload.status like "completed"), causing terminal/finished payloads to be routed as progress; update the logic so after error handling you check for final conditions first: treat payloadStatus values such as "completed"/"complete"/"done" as 'final', run the candidateType final check block (the candidateType === 'final' | 'result' | 'complete' | 'completed' | 'done' clause) and the payloadRecord final-key hasAnyDefinedKey(...) block before any progress checks, and then run progress detection (the hasAnyDefinedKey(...) and candidateType progress/status/step/update blocks); keep the error checks (candidateType/payloadStatus) first and ensure useScanSubscription.ts will receive onFinal instead of onProgress.src/pages/Loading/hooks/useLoadingPage.ts (1)
259-283:⚠️ Potential issue | 🟠 Major최종 payload의
status를 위험도로 바로 인정하지 마세요.
status: "completed"같은 진행 상태도hasResolvedRiskLevel을 통과해 상세 조회를 건너뛰고, 이후 위험도 해석 실패 시 기본warning결과로 이동할 수 있습니다.resolveResultToneFromSources([payload], null)처럼 실제safe | warning | critical로 정규화되는 값만 확정 위험도로 처리하고, 상세 조회도 실패하면 결과 페이지를 열지 않도록 막아 주세요.🐛 제안 수정
- const hasResolvedRiskLevel = - Boolean(pickString(payload, ['riskLevel', 'risk_level', 'status', 'result'])) || - pickNumber(payload, ['trustScore', 'trust_score', 'score']) !== null; + const hasResolvedRiskLevel = + resolveResultToneFromSources([payload], null) !== null || + pickNumber(payload, ['trustScore', 'trust_score', 'score']) !== null;- if (detailResolutionFailedRef.current) { + if (detailResolutionFailedRef.current || !isResolved) { return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Loading/hooks/useLoadingPage.ts` around lines 259 - 283, hasResolvedRiskLevel currently treats any payload.status (e.g. "completed") as a resolved risk and skips detail fetch; change the check so it only considers a normalized tone returned by resolveResultToneFromSources([payload], null) (values "safe"|"warning"|"critical") or a non-null pickNumber result for trustScore, and do not treat raw status strings as resolved; also ensure that if resolveDetailAndOpenResult() throws or sets detailResolutionFailedRef.current, you do not proceed to set detailResolutionStartedRef.current, call setCompleted(), or call openResultRouteForCurrentSession(); keep references to hasResolvedRiskLevel, resolveResultToneFromSources, resolveDetailAndOpenResult, detailResolutionFailedRef, detailResolutionStartedRef, setCompleted, and openResultRouteForCurrentSession when implementing the guard.
🧹 Nitpick comments (5)
src/features/scan-url/api/uploadErrors.test.ts (1)
34-40: 429 비-CAPTCHA 케이스도 함께 검증해 주세요.현재 음성 케이스는
statusCode: 500이라isCaptchaRequiredUploadError의 429 게이트만 검증합니다. 429 응답이지만 captcha 마커가 없는 경우도false인지 확인하면 회귀 방지가 더 확실합니다.🧪 제안 테스트 보강
it('returns false for non-captcha responses', () => { const error = new ApiError({ message: 'Request failed.', statusCode: 500, }); expect(isCaptchaRequiredUploadError(error)).toBe(false); }); + + it('returns false for 429 responses without captcha markers', () => { + const error = new ApiError({ + data: { + error_code: 'RATE_LIMIT_EXCEEDED', + }, + message: 'Too many requests.', + statusCode: 429, + }); + + expect(isCaptchaRequiredUploadError(error)).toBe(false); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/scan-url/api/uploadErrors.test.ts` around lines 34 - 40, Add a unit test that verifies isCaptchaRequiredUploadError returns false for a 429 response that does not include the captcha marker: create an ApiError (or same error shape used in existing tests) with statusCode: 429 and no captcha-related properties and assert isCaptchaRequiredUploadError(error) === false; reference the existing test file and the isCaptchaRequiredUploadError symbol so the new test mirrors the non-captcha 500 case but uses 429 to prevent regressions.src/shared/store/scanSessionStore.ts (1)
47-63:noopStorage.getItem반환형을StateStorage시그니처에 맞춰 주세요.
zustand/middleware의StateStorage.getItem은string | null | Promise<string | null>을 기대하지만, 여기서는() => null만 선언되어 일부 TS 설정에서 매개변수 누락 타입 오류 또는 사용처 경고가 날 수 있습니다. 현재 동작은 문제없으나, 향후 리팩토링 방어를 위해 시그니처를 맞추는 편이 안전합니다.♻️ 제안
const noopStorage: StateStorage = { - getItem: () => null, - removeItem: () => {}, - setItem: () => {}, + getItem: (_name) => null, + removeItem: (_name) => {}, + setItem: (_name, _value) => {}, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/store/scanSessionStore.ts` around lines 47 - 63, The noopStorage object uses getItem: () => null which doesn't match the StateStorage signature; update noopStorage.getItem to accept the expected parameter and return type (e.g. getItem: (key: string) => string | null or Promise<string | null>), and likewise ensure removeItem and setItem match StateStorage parameter shapes; update the noopStorage declaration referenced in createScanSessionStorage() so TypeScript no longer complains about signature mismatch.src/shared/lib/page/useSessionGuardedPageData.ts (1)
38-64:fetchPageData/getInitialPageData가 호출 측에서 안정된 참조여야 합니다.
reload는fetchPageData를 dep으로 포함하고, 하단useEffect(() => void reload(), [reload])에서 매 변화마다 재실행됩니다. 호출부가 인라인 화살표 함수를 넘기면 매 렌더마다 reload가 반복 실행되어 불필요한 요청/리다이렉트 루프가 발생할 수 있습니다. 이번 PR 내 사용처는 모듈 레벨 함수라 괜찮지만, 공개 훅이므로 JSDoc로 "stable reference" 요구를 명시하거나 내부적으로 ref에 담아 고정하는 방어를 권장합니다.♻️ 방어적 예시
+ const fetchPageDataRef = useRef(fetchPageData); + fetchPageDataRef.current = fetchPageData; + const reload = useCallback(async () => { try { - const response = await fetchPageData(); + const response = await fetchPageDataRef.current(); ... } - }, [fetchPageData, loadErrorMessage, logMessage, message, navigate]); + }, [loadErrorMessage, logMessage, message, navigate]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/lib/page/useSessionGuardedPageData.ts` around lines 38 - 64, The hook useSessionGuardedPageData currently depends directly on fetchPageData (and getInitialPageData callers), causing reload to re-create on every render if callers pass inline functions; change the implementation to capture fetchPageData in a stable ref (e.g., const fetchRef = useRef(fetchPageData); update fetchRef.current whenever fetchPageData identity intentionally changes) and have reload read from fetchRef.current so useCallback/useEffect no longer re-run on every render, and add a JSDoc on useSessionGuardedPageData requiring a stable reference for fetchPageData/getInitialPageData as an alternative guidance for callers; keep existing behavior for isMountedRef, error handling, and navigate logic intact.src/shared/api/responseAccess/payloadAccess.ts (1)
148-166:pickStringArray의 쉼표 분리 동작이 값에 쉼표가 포함된 문자열에서 오분할됩니다.단일 문자열을 CSV로 가정해
,기준으로 split 합니다. 백엔드가"red,green,blue"형태로 내려주는 태그 필드에는 적합하지만, 한 개의 의미있는 문자열 필드(예:"서울특별시, 강남구"같은 주소, 또는 메시지 본문)를 다루는 키에 잘못 적용되면 1개짜리 배열이 다중 항목으로 쪼개집니다. 호출부가 명백히 CSV 의미를 가진 키에만 사용하도록 주석/키 목록 제한을 고려하거나,splitDelimiter를 옵션으로 주입받는 형태가 더 안전합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/responseAccess/payloadAccess.ts` around lines 148 - 166, pickStringArray currently always treats string values as CSV and splits on ',', which incorrectly splits meaningful single strings (e.g., addresses). Change pickStringArray to accept an optional splitDelimiter parameter (e.g., splitDelimiter?: string | null) and only perform split when a non-null delimiter is provided; otherwise, when value is a string return a single-element array with the trimmed string. Update the function signature and internal logic that reads value from pickUnknown, and then update callers to pass ',' only for keys that are known CSV lists (leave callers that expect single strings to omit the delimiter). Ensure type guards and trimming/filtering logic remain when handling arrays returned from pickUnknown.src/shared/api/responseAccess/payloadAccess.test.ts (1)
5-27: 테스트 커버리지가 얕습니다 — 주요 엣지 케이스 추가를 권장합니다.지금은
pickString1건,pickNumber2건만 검증하고 있습니다. 다음 케이스를 보완하면 회귀 방지에 큰 도움이 됩니다.
pickBoolean:"true"/"false"문자열, 그 외 문자열/숫자는 null 반환pickNumber: 음수("-12"), 부호+공백(" 12 "), 지수 표기("1e3"→ 현재 regex는 거부),0,NaN/Infinity원시값pickString: 공백만 있는 문자열 → nullpickSourceString/pickSourceRecord: 여러 소스 우선순위, 모두 null일 때getCandidateRecords순환 참조 (a.data = a) 무한 루프 회귀 방지🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/api/responseAccess/payloadAccess.test.ts` around lines 5 - 27, Add unit tests covering the missing edge cases: for pickBoolean verify that "true"/"false" strings return true/false and that other strings or numbers return null; for pickNumber add cases for negative string "-12", strings with surrounding whitespace " 12 ", exponent notation "1e3" (ensure current regex behavior is asserted), "0", and raw numeric NaN/Infinity values returning null; for pickString add a whitespace-only string case that should return null; for pickSourceString and pickSourceRecord add multiple-source priority tests and a case where all sources are null; and add a test for getCandidateRecords that constructs a self-referential object (a.data = a) to assert the function does not infinite-loop but safely returns/halts. Use the existing test helpers and target functions pickBoolean, pickNumber, pickString, pickSourceString, pickSourceRecord, and getCandidateRecords to locate code under test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/features/scan-history/api/fetchScanHistoryData.ts`:
- Around line 69-76: The mapping that sets url: pickHistoryTargetValue(item) ??
'URL 정보 없음' is injecting a display string where a real URL is expected (used by
handleOpenRecentScanResult), so replace that behavior: either filter out items
with no URL before the visibleItems.map (or inside map skip those entries) or
set url to null/undefined instead of the placeholder and ensure the UI/handlers
skip clicks when url is null; update references to pickHistoryTargetValue,
buildHistoryItemId, and any code that consumes the resulting url so
click/navigation is disabled when url is null.
- Around line 43-58: formatScannedAt currently only replaces the space with 'T'
and can leave timestamps like "2026.04.21 02:50:44" unnormalized; update
formatScannedAt to reuse the same normalization as resolveHistoryTimestamp
(e.g., convert dots to dashes and normalize the separator) before creating a
Date so parsing is consistent with resolveHistoryTimestamp, preserving the
existing null check and fallback behavior (return '날짜 정보 없음' for null and
rawScannedAt for unparsable dates).
In `@src/pages/QRScan/hooks/useQRScanPage.ts`:
- Around line 386-399: handleOpenRecentScanResult currently always uses
resultRouteByStatus[recentScanItem.status] which routes non-URL or non-WEB
scheme items to the web result pages; change the routing so non-web results go
to the non-url result route: inside handleOpenRecentScanResult (referenced by
function name), after setting setHistorySelection, compute the route as
(recentScanItem.isUrl && recentScanItem.schemeType === 'WEB') ?
resultRouteByStatus[recentScanItem.status] : resultRouteByStatus['non-url'] (or
the equivalent non-url route key your app uses) and pass that to openResultPage
along with recentScanItem.url; ensure you reference recentScanItem.schemeType
and recentScanItem.isUrl when deciding the route.
In `@src/pages/Result/lib/toResultPageData.ts`:
- Around line 25-41: formatDateOnlyLabel currently uses UTC getters
(getUTCFullYear/getUTCMonth/getUTCDate) which causes dates to shift for Korean
users; change the implementation to format dates in Korea time—either by using
local getters (getFullYear/getMonth/getDate) on parsedDate or by using
Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month:
'2-digit', day: '2-digit' }) and then map the result to the desired "YYYY.MM.DD"
string; keep the same invalid-date handling (return rawDate when parsedDate is
invalid) and the null check.
In `@src/shared/api/apiConfig.ts`:
- Around line 7-15: The parsePositiveInteger function currently uses
Number.parseInt which partially parses strings like "1000ms", "1.5", or "1e4";
change it to use the existing trimEnvValue helper and strict numeric validation:
call trimEnvValue(rawValue), convert with Number(trimmed), then require
Number.isInteger(result) && result > 0 before returning it, otherwise return the
fallback; update parsePositiveInteger to reference trimEnvValue and use Number()
+ Number.isInteger() so only strict positive integers are accepted.
In `@src/shared/lib/page/useSessionGuardedPageData.ts`:
- Around line 45-59: In useSessionGuardedPageData's catch block, stop forcing
navigation to '/' for every error: keep the current isMountedRef check and
console.error, keep the special-case branch that navigates when error instanceof
Error && error.message === 'SCAN_SESSION_REQUIRED', but remove the unconditional
void navigate({ to: '/' }) after showApiError so that non-session errors only
call showApiError (allowing the page to remain and the user to retry); verify
showApiError(message, error, loadErrorMessage) and the early returns remain
correct to avoid falling through to any navigation.
In `@src/shared/store/scanProgressStore.ts`:
- Around line 210-233: When a COMPLETED event arrives but mapSseStepId returned
null (backendStep !== null && currentStepId === null) the code overwrites
progressStatus to '' and prevents completion; change the conditional so that
when isCompletedStatus(status) is true you do not blank out progressStatus and
you force nextCurrentStepId to 'completed' regardless of backendStep mapping
failure. In practice, adjust the logic around variables progressStatus and
nextCurrentStepId (used by deriveCompletedStepIds and derivePercent) so that
isCompletedStatus(status) always leaves progressStatus = status (or otherwise
ensures completed is injected) and sets nextCurrentStepId = 'completed' when
status is completed, even if backendStep is non-null and currentStepId is null.
- Around line 184-190: setError가 현재 진행률 필드(percent, currentStepId,
completedStepIds 등)를 유지하는데 의도적으로 일관되게 유지할지 아니면 setConnecting과 동일하게 초기화할지 명확히 하고
구현을 맞추세요; 구현 선택에 따라 src/shared/store/scanProgressStore.ts의 setError를 수정하여
setConnecting과 동일한 필드(예: percent, totalSteps, currentStepId, completedStepIds,
backendMessage 등)를 초기값으로 재설정하거나(오류 시 이전 진행 지우기) 아니면 함수에 주석을 추가해 진행 상태를 보존하는 것이
명시적 설계라는 것을 문서화(그리고 관련 테스트/사용처인 resetProgress()와 useLoadingPage 훅의 흐름을 검토)하도록
변경하세요.
In `@src/shared/store/scanSessionStore.ts`:
- Around line 216-229: The current partialize in the store
(scanSessionStorageKey -> partialize) persists heavy/PII-bearing fields
(analysisDetail, finalResult, scanResponse) into localStorage; change partialize
to only include lightweight, non-PII derived fields (e.g., historySelection,
decodedUrl, schemeType, isUrl, riskLevel) and remove
analysisDetail/finalResult/scanResponse from the persisted snapshot, and either
keep those heavy objects in-memory only or persist them to sessionStorage (via a
separate createJSONStorage wrapper) if truly needed for the tab session; update
references to createScanSessionStorage/storage initialization to use the reduced
partialize or to a sessionStorage-backed createJSONStorage for the full response
so localStorage quotas and privacy risks are avoided.
---
Outside diff comments:
In `@src/pages/Captcha/hooks/useCaptchaPage.ts`:
- Around line 29-45: handleSubmit currently only checks token truthiness before
submitting, but canSubmit uses a trim-based emptiness rule; ensure handleSubmit
applies the same trim check to prevent whitespace-only tokens reaching
submitCaptchaVerification. Update the handleSubmit function to validate
token.trim().length > 0 (or call the same canSubmit helper if available) before
calling submitCaptchaVerification and set the same feedback message as used
elsewhere when the trimmed token is empty.
In `@src/pages/Captcha/ui/CaptchaWidget.tsx`:
- Around line 142-172: The ready callback passed to enterprise.ready should
protect against synchronous exceptions from enterprise.render: wrap the call to
enterprise.render(...) inside a try-catch within the enterprise.ready callback
(where widgetContainerRef.current/ container are checked) so that if
enterprise.render throws you clear/leave widgetId unset, check cancelled, call
onTokenChange(null) (and optionally log the error) and return; only assign
widgetId when render succeeds. Ensure the same canceled checks (cancelled,
widgetContainerRef.current !== container) remain in place and that error
handling covers both thrown exceptions and the existing error/expired callbacks.
---
Duplicate comments:
In `@src/pages/Loading/hooks/useLoadingPage.ts`:
- Around line 259-283: hasResolvedRiskLevel currently treats any payload.status
(e.g. "completed") as a resolved risk and skips detail fetch; change the check
so it only considers a normalized tone returned by
resolveResultToneFromSources([payload], null) (values
"safe"|"warning"|"critical") or a non-null pickNumber result for trustScore, and
do not treat raw status strings as resolved; also ensure that if
resolveDetailAndOpenResult() throws or sets detailResolutionFailedRef.current,
you do not proceed to set detailResolutionStartedRef.current, call
setCompleted(), or call openResultRouteForCurrentSession(); keep references to
hasResolvedRiskLevel, resolveResultToneFromSources, resolveDetailAndOpenResult,
detailResolutionFailedRef, detailResolutionStartedRef, setCompleted, and
openResultRouteForCurrentSession when implementing the guard.
In `@src/shared/api/types/sseEvents.ts`:
- Around line 55-139: The SSE event-type resolution currently lets
progress-detecting heuristics run before final-detection (and ignores
payload.status like "completed"), causing terminal/finished payloads to be
routed as progress; update the logic so after error handling you check for final
conditions first: treat payloadStatus values such as
"completed"/"complete"/"done" as 'final', run the candidateType final check
block (the candidateType === 'final' | 'result' | 'complete' | 'completed' |
'done' clause) and the payloadRecord final-key hasAnyDefinedKey(...) block
before any progress checks, and then run progress detection (the
hasAnyDefinedKey(...) and candidateType progress/status/step/update blocks);
keep the error checks (candidateType/payloadStatus) first and ensure
useScanSubscription.ts will receive onFinal instead of onProgress.
---
Nitpick comments:
In `@src/features/scan-url/api/uploadErrors.test.ts`:
- Around line 34-40: Add a unit test that verifies isCaptchaRequiredUploadError
returns false for a 429 response that does not include the captcha marker:
create an ApiError (or same error shape used in existing tests) with statusCode:
429 and no captcha-related properties and assert
isCaptchaRequiredUploadError(error) === false; reference the existing test file
and the isCaptchaRequiredUploadError symbol so the new test mirrors the
non-captcha 500 case but uses 429 to prevent regressions.
In `@src/shared/api/responseAccess/payloadAccess.test.ts`:
- Around line 5-27: Add unit tests covering the missing edge cases: for
pickBoolean verify that "true"/"false" strings return true/false and that other
strings or numbers return null; for pickNumber add cases for negative string
"-12", strings with surrounding whitespace " 12 ", exponent notation "1e3"
(ensure current regex behavior is asserted), "0", and raw numeric NaN/Infinity
values returning null; for pickString add a whitespace-only string case that
should return null; for pickSourceString and pickSourceRecord add
multiple-source priority tests and a case where all sources are null; and add a
test for getCandidateRecords that constructs a self-referential object (a.data =
a) to assert the function does not infinite-loop but safely returns/halts. Use
the existing test helpers and target functions pickBoolean, pickNumber,
pickString, pickSourceString, pickSourceRecord, and getCandidateRecords to
locate code under test.
In `@src/shared/api/responseAccess/payloadAccess.ts`:
- Around line 148-166: pickStringArray currently always treats string values as
CSV and splits on ',', which incorrectly splits meaningful single strings (e.g.,
addresses). Change pickStringArray to accept an optional splitDelimiter
parameter (e.g., splitDelimiter?: string | null) and only perform split when a
non-null delimiter is provided; otherwise, when value is a string return a
single-element array with the trimmed string. Update the function signature and
internal logic that reads value from pickUnknown, and then update callers to
pass ',' only for keys that are known CSV lists (leave callers that expect
single strings to omit the delimiter). Ensure type guards and trimming/filtering
logic remain when handling arrays returned from pickUnknown.
In `@src/shared/lib/page/useSessionGuardedPageData.ts`:
- Around line 38-64: The hook useSessionGuardedPageData currently depends
directly on fetchPageData (and getInitialPageData callers), causing reload to
re-create on every render if callers pass inline functions; change the
implementation to capture fetchPageData in a stable ref (e.g., const fetchRef =
useRef(fetchPageData); update fetchRef.current whenever fetchPageData identity
intentionally changes) and have reload read from fetchRef.current so
useCallback/useEffect no longer re-run on every render, and add a JSDoc on
useSessionGuardedPageData requiring a stable reference for
fetchPageData/getInitialPageData as an alternative guidance for callers; keep
existing behavior for isMountedRef, error handling, and navigate logic intact.
In `@src/shared/store/scanSessionStore.ts`:
- Around line 47-63: The noopStorage object uses getItem: () => null which
doesn't match the StateStorage signature; update noopStorage.getItem to accept
the expected parameter and return type (e.g. getItem: (key: string) => string |
null or Promise<string | null>), and likewise ensure removeItem and setItem
match StateStorage parameter shapes; update the noopStorage declaration
referenced in createScanSessionStorage() so TypeScript no longer complains about
signature mismatch.
🪄 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: CHILL
Plan: Pro
Run ID: 3dc9199d-ccae-44ad-a3fe-cf958421bdc4
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
.env.examplepackage.jsonscripts/setupEnv.mjssrc/features/scan-history/api/fetchScanHistoryData.tssrc/features/scan-history/lib/historyItemAccess.test.tssrc/features/scan-history/lib/historyItemAccess.tssrc/features/scan-history/types/scanHistory.types.tssrc/features/scan-url/api/uploadErrors.test.tssrc/features/scan-url/api/uploadErrors.tssrc/pages/Captcha/CaptchaPage.tsxsrc/pages/Captcha/api/captchaVerifyResponse.test.tssrc/pages/Captcha/api/captchaVerifyResponse.tssrc/pages/Captcha/api/submitCaptchaVerification.tssrc/pages/Captcha/hooks/useCaptchaPage.tssrc/pages/Captcha/ui/CaptchaWidget.tsxsrc/pages/Loading/hooks/useLoadingPage.tssrc/pages/QRScan/hooks/useQRScanPage.tssrc/pages/Report/constants/riskDetectionCatalog.tssrc/pages/Report/hooks/useReportPage.tssrc/pages/Result/hooks/useResultPageBase.tssrc/pages/Result/lib/toResultPageData.tssrc/pages/Result/ui/ResultStatusPage.tsxsrc/pages/ResultNonUrl/ResultNonUrlPage.tsxsrc/pages/ResultNonUrl/hooks/useResultNonUrlPage.tssrc/pages/ScanList/ScanListPage.tsxsrc/pages/ScanList/hooks/useScanListPage.tssrc/routes/router.tsxsrc/shared/api/apiConfig.tssrc/shared/api/axios/createAxios.tssrc/shared/api/errors/apiError.tssrc/shared/api/fetchScanDetail.tssrc/shared/api/responseAccess/payloadAccess.test.tssrc/shared/api/responseAccess/payloadAccess.tssrc/shared/api/types/analysisResponse.tssrc/shared/api/types/scanResponse.tssrc/shared/api/types/sseEvents.test.tssrc/shared/api/types/sseEvents.tssrc/shared/lib/page/useSessionGuardedPageData.tssrc/shared/lib/sse/useScanSubscription.tssrc/shared/store/guestStore.tssrc/shared/store/scanProgressStore.test.tssrc/shared/store/scanProgressStore.tssrc/shared/store/scanSessionStore.tsvite.config.ts
✅ Files skipped from review due to trivial changes (4)
- src/features/scan-history/lib/historyItemAccess.test.ts
- scripts/setupEnv.mjs
- .env.example
- src/shared/api/types/analysisResponse.ts
🚧 Files skipped from review as they are similar to previous changes (20)
- src/pages/ScanList/ScanListPage.tsx
- src/shared/api/axios/createAxios.ts
- src/shared/api/fetchScanDetail.ts
- src/shared/api/types/scanResponse.ts
- package.json
- src/pages/Captcha/api/captchaVerifyResponse.ts
- src/shared/api/types/sseEvents.test.ts
- src/pages/Captcha/api/captchaVerifyResponse.test.ts
- src/pages/Captcha/CaptchaPage.tsx
- src/pages/Result/ui/ResultStatusPage.tsx
- src/shared/lib/sse/useScanSubscription.ts
- src/features/scan-url/api/uploadErrors.ts
- src/shared/api/errors/apiError.ts
- src/pages/Report/constants/riskDetectionCatalog.ts
- src/pages/Result/hooks/useResultPageBase.ts
- src/shared/store/guestStore.ts
- src/pages/ResultNonUrl/ResultNonUrlPage.tsx
- src/routes/router.tsx
- src/shared/store/scanProgressStore.test.ts
- src/features/scan-history/lib/historyItemAccess.ts
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
src/pages/Loading/hooks/useLoadingPage.ts (1)
259-279:⚠️ Potential issue | 🟠 Major위험도를 확정하지 못하면 기본
warning으로 진행하지 마세요.
pickNumber(...) !== null때문에resolveResultToneFromSources()가 실제 톤을 못 만든 payload도 “해결됨”으로 간주됩니다. 또한 상세 조회가false를 반환해도 실패 상태가 아니면 아래 완료 라우팅으로 떨어져 기본warning결과가 열립니다.🐛 제안 수정
-import { pickNumber, pickString } from '@/shared/api/responseAccess/payloadAccess'; +import { pickString } from '@/shared/api/responseAccess/payloadAccess'; @@ - const hasResolvedRiskLevel = - resolveResultToneFromSources([payload], null) !== null || - pickNumber(payload, ['trustScore', 'trust_score', 'score']) !== null; + const hasResolvedRiskLevel = resolveResultToneFromSources([payload], null) !== null; if (!hasResolvedRiskLevel) { let isResolved = false; try { @@ if (detailResolutionFailedRef.current) { return; } + + failDetailResolution(DETAIL_RESOLUTION_ERROR_MESSAGE); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Loading/hooks/useLoadingPage.ts` around lines 259 - 279, The current check treats payloads with only numeric trust scores as "resolved" because pickNumber(...) !== null, causing resolveResultToneFromSources(...) failures to be ignored and falling back to a default warning; change hasResolvedRiskLevel to rely only on the tone resolution (replace the OR with only resolveResultToneFromSources([payload], null) !== null and remove the pickNumber(...) check), and keep the detail-resolution flow using resolveDetailAndOpenResult() and detailResolutionFailedRef.current as-is but ensure that when resolveDetailAndOpenResult() returns false and detailResolutionFailedRef.current is still false you do NOT treat the case as resolved/default to warning (i.e., do not return early to trigger default routing) so that only a successful tone resolution or an explicit failure via detailResolutionFailedRef.current results in completion.
🧹 Nitpick comments (1)
src/pages/Captcha/hooks/useCaptchaPage.ts (1)
6-14: 반환 타입의token필드는 이제 미사용입니다 — 제거 권장.훅의 유일한 소비처인
src/pages/Captcha/CaptchaPage.tsx는canSubmit,feedbackMessage,handleCaptchaTokenChange,handleSubmit,isVerifying,recaptchaSiteKey만 구조 분해하고 있습니다.token은 반환 타입과return객체에서 제거하여 공개 표면을 줄이고 캡슐화를 개선할 수 있습니다.♻️ 제안 패치
type UseCaptchaPageReturn = { canSubmit: boolean; feedbackMessage: string | null; handleCaptchaTokenChange: (nextToken: string | null) => void; handleSubmit: () => Promise<void>; isVerifying: boolean; recaptchaSiteKey: string; - token: string | null; };return { canSubmit, feedbackMessage, handleCaptchaTokenChange, handleSubmit, isVerifying, recaptchaSiteKey, - token, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Captcha/hooks/useCaptchaPage.ts` around lines 6 - 14, The exported UseCaptchaPageReturn type exposes a now-unused token field; remove token from the UseCaptchaPageReturn type and from the object returned by the useCaptchaPage hook (e.g., update the UseCaptchaPageReturn declaration and the return statement inside useCaptchaPage to exclude token) so only canSubmit, feedbackMessage, handleCaptchaTokenChange, handleSubmit, isVerifying and recaptchaSiteKey remain in the public API, eliminating the unused surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/Loading/hooks/useLoadingPage.ts`:
- Around line 89-100: The effect in useLoadingPage resets progress and calls
setConnecting whenever finalResult changes, which races with the onFinal handler
that sets finalResult; update the effect to skip the reset when the finalResult
belongs to the same scan as the current scanResponse. Concretely, inside the
useEffect referenced (useEffect, finalResult, scanResponse, resetProgress,
setConnecting), add a guard: if finalResult is present and finalResult.scanId
(or finalResult.id) equals scanResponse?.scanId (or scanResponse?.id) then
return early and do not call resetProgress()/setConnecting(); otherwise proceed
as before. Ensure you reference the correct identifier on
finalResult/scanResponse in the codebase.
In `@src/pages/QRScan/hooks/useQRScanPage.ts`:
- Around line 154-155: The current isWebHistoryItem treats only isUrl === true
as a web URL, causing items with null isUrl and schemeType 'WEB' to be
misrouted; change the predicate to accept nullable isUrl by checking that isUrl
is not explicitly false (e.g., item.isUrl !== false) and schemeType
trimmed/uppercased equals 'WEB' in isWebHistoryItem, and apply the same
nullable-aware check to the similar logic referenced around the other
scan-history handling (the block at ~408-412) so only explicit false or non-WEB
schemes are routed to /result/non-url.
In `@src/pages/Report/lib/toReportPageData.ts`:
- Around line 30-51: scannedUrlKeys currently prioritizes final/destination keys
which makes scannedUrl collapse to the same value as destinationUrl; change the
key order so scannedUrlKeys prefers session.decodedUrl / decodedUrl /
originalUrl (e.g., put 'decodedUrl','decoded_url','originalUrl','original_url'
before any final/destination keys) while leaving destinationUrlKeys focused on
final/destination keys; update the logic that falls back to
session.decodedUrl/originalUrl (referencing session.decodedUrl and the
scannedUrl resolution code) and adjust the duplicate occurrences around the
other blocks (the sections corresponding to the other occurrences you noted) so
resolveUrlComparisonSummary will correctly detect real redirects.
- Around line 74-92: In formatDateLabel, don't return the rawDate on parse
failure (the parsedDate.getTime() NaN branch); instead return the fallbackLabel
to avoid exposing backend raw timestamps in the UI—update the function
formatDateLabel to check Number.isNaN(parsedDate.getTime()) and return
fallbackLabel (retain existing formatted output when parsing succeeds); consider
adding a TODO comment in formatDateLabel about explicit timezone handling (e.g.,
using toLocaleString with a specified timeZone or date-fns-tz) once backend
timezone semantics are confirmed.
In `@src/pages/ScanList/ScanListPage.tsx`:
- Around line 94-95: The current WEB-detection treats isUrl === null as non-URL;
change the logic in isWebScanListItem so only an explicit false blocks WEB
routing: evaluate schemeType==='WEB' while allowing isUrl to be null/undefined
(e.g., use a condition like item.isUrl !== false &&
item.schemeType?.trim().toUpperCase() === 'WEB' in the isWebScanListItem
function), and apply the same pattern to the other occurrences of the check
around lines 117-130 to ensure only explicit non-URL values exclude WEB routing.
In `@src/shared/api/responseAccess/payloadAccess.ts`:
- Around line 73-142: The bug is that pickUnknown returns the first non-null
value which blocks later candidate keys from being tried by
pickString/pickNumber/pickBoolean; fix by making each picker (pickString,
pickNumber, pickBoolean) iterate over getCandidateRecords(source) and keys
directly and only return when a value both exists (not null/undefined) and
passes that picker's validation/parsing logic (e.g., for pickString: typeof ===
'string' then trimmed non-empty; for pickNumber: accept number or
numeric-trimmed string with regex + Number.isFinite(parsed); for pickBoolean:
use value.trim().toLowerCase() and accept 'true'/'false'); keep or simplify
pickUnknown but do not rely on it for typed picks so fallback candidate keys are
attempted correctly.
In `@src/shared/store/scanSessionStore.ts`:
- Around line 177-186: setAnalysisDetail currently only updates decodedUrl (and
schemeType via pickSourceString) but leaves isUrl unchanged, causing
inconsistent routing when ensureScanDetail is the primary source; update
setAnalysisDetail to derive schemeType and isUrl the same way ensureScanDetail
does: compute sources = [analysisDetail], compute schemeType =
pickSourceString(sources, ['schemeType','scheme_type']) ?? state.schemeType,
compute decodedUrl via resolveDecodedUrl(analysisDetail) as now, then set isUrl
based on the resolved schemeType (and fallback to checking decodedUrl/non-WEB if
schemeType is absent) so isUrl and schemeType are always updated together in
setAnalysisDetail.
---
Duplicate comments:
In `@src/pages/Loading/hooks/useLoadingPage.ts`:
- Around line 259-279: The current check treats payloads with only numeric trust
scores as "resolved" because pickNumber(...) !== null, causing
resolveResultToneFromSources(...) failures to be ignored and falling back to a
default warning; change hasResolvedRiskLevel to rely only on the tone resolution
(replace the OR with only resolveResultToneFromSources([payload], null) !== null
and remove the pickNumber(...) check), and keep the detail-resolution flow using
resolveDetailAndOpenResult() and detailResolutionFailedRef.current as-is but
ensure that when resolveDetailAndOpenResult() returns false and
detailResolutionFailedRef.current is still false you do NOT treat the case as
resolved/default to warning (i.e., do not return early to trigger default
routing) so that only a successful tone resolution or an explicit failure via
detailResolutionFailedRef.current results in completion.
---
Nitpick comments:
In `@src/pages/Captcha/hooks/useCaptchaPage.ts`:
- Around line 6-14: The exported UseCaptchaPageReturn type exposes a now-unused
token field; remove token from the UseCaptchaPageReturn type and from the object
returned by the useCaptchaPage hook (e.g., update the UseCaptchaPageReturn
declaration and the return statement inside useCaptchaPage to exclude token) so
only canSubmit, feedbackMessage, handleCaptchaTokenChange, handleSubmit,
isVerifying and recaptchaSiteKey remain in the public API, eliminating the
unused surface.
🪄 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: CHILL
Plan: Pro
Run ID: 9daea014-94c8-4416-a549-585fc7cc784c
📒 Files selected for processing (22)
src/features/scan-history/api/fetchScanHistoryData.tssrc/features/scan-history/lib/historyItemAccess.test.tssrc/features/scan-history/lib/historyItemAccess.tssrc/features/scan-url/api/uploadErrors.test.tssrc/pages/Captcha/hooks/useCaptchaPage.tssrc/pages/Captcha/ui/CaptchaWidget.tsxsrc/pages/Loading/hooks/useLoadingPage.tssrc/pages/QRScan/hooks/useQRScanPage.tssrc/pages/Report/lib/toReportPageData.tssrc/pages/Result/lib/toResultPageData.tssrc/pages/ResultNonUrl/api/fetchResultNonUrlPageData.tssrc/pages/ResultNonUrl/lib/toResultNonUrlPageData.tssrc/pages/ScanList/ScanListPage.tsxsrc/shared/api/apiConfig.tssrc/shared/api/responseAccess/payloadAccess.test.tssrc/shared/api/responseAccess/payloadAccess.tssrc/shared/api/types/sseEvents.test.tssrc/shared/api/types/sseEvents.tssrc/shared/lib/page/useSessionGuardedPageData.tssrc/shared/store/scanProgressStore.test.tssrc/shared/store/scanProgressStore.tssrc/shared/store/scanSessionStore.ts
✅ Files skipped from review due to trivial changes (1)
- src/features/scan-history/lib/historyItemAccess.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/features/scan-url/api/uploadErrors.test.ts
- src/shared/api/types/sseEvents.test.ts
- src/shared/api/apiConfig.ts
- src/pages/ResultNonUrl/api/fetchResultNonUrlPageData.ts
- src/shared/api/types/sseEvents.ts
- src/shared/api/responseAccess/payloadAccess.test.ts
- src/pages/Result/lib/toResultPageData.ts
- src/shared/store/scanProgressStore.ts
- src/shared/store/scanProgressStore.test.ts
Summary
해당 PR에 대한 작업 요약을 작성해주세요.
Tasks
-api 연결
Summary by CodeRabbit
릴리즈 노트
새로운 기능
개선
제거