# QA-4 Report — Frontend Runtime Bug Scan > Worker: Explore agent · Lead review: critical/major 재조정 ## Summary - Pages audited: 13 + 3 layout + primitives + chart - **verdict**: REQUEST_CHANGES - critical: 2, major: 2, minor: 5, recommendation: 2 ## Critical Findings ### [critical] C1 Review page double-submit race - **Location**: `frontend/src/app/review/page.tsx:34-52` - **Evidence**: ```tsx const submit = async (id: number, result: StudyResult) => { setSubmitting(true); try { await api.post(`/reviews/${id}/submit`, { result }); ... } finally { setSubmitting(false); } }; ``` `submitting` 이 전역 boolean 이라 동일 아이템에 대해 "맞음"→"스킵" 으로 빠르게 더블클릭하면 두 요청이 모두 나감. 두 번째는 backend 에서 "already processed" 로 반려되지만, 모바일 터치 반응이 느려서 중복 POST 발생 가능성이 높다. - **Impact**: 중복 POST → 백엔드 race (QA-5 C2 와 합성되면 실제 데이터 오염). - **Suggested fix**: `submitting` 을 `Set` 로 바꿔 per-review lock. ```tsx const [pending, setPending] = useState>(new Set()); const isPending = (id: number) => pending.has(id); // 버튼 disabled={isPending(q.id)} ``` ### [critical] C2 `/subjects/[id]` 에 숫자가 아닌 값이 오면 무한 로딩 - **Location**: `frontend/src/app/subjects/[id]/page.tsx:31, 45-49` - **Evidence**: ```tsx const subjectId = Number(params.id); // → NaN 가능 const load = useCallback(() => { api.get(`/subjects/${subjectId}`) // `/subjects/NaN` 요청 ... }, [subjectId]); useEffect(() => { if (!Number.isFinite(subjectId)) return; // 이미 load 는 만들어진 상태 load(); }, [subjectId, load]); ``` `useParams` 는 string 만 주고, `Number('abc')` 는 `NaN`. useEffect guard 는 호출을 막아주지만, **렌더 직후 `return `** 로 넘어가서 사용자는 로딩 스피너만 본다. 유효하지 않은 URL 처리 없음. - **Impact**: `/subjects/abc` 같은 URL 이 로딩 화면에서 멈춤. 404 리다이렉트 없음. - **Suggested fix**: ```tsx const raw = Number(params.id); const subjectId = Number.isFinite(raw) ? raw : null; // 컴포넌트 초입: if (subjectId === null) return ; ``` ## Major Findings ### [major] M1 히스토리 페이지들이 에러 상태 처리 없음 - **Location**: `frontend/src/app/review/history/page.tsx:26-30`, `frontend/src/app/study/history/page.tsx:31-41`, `frontend/src/app/stats/page.tsx:50-57` - **Evidence**: `.then(...)` 만 있고 `.catch` 가 없다. API 500 이 오면 `items === null` 로 남아 `로딩 중...` 이 영원히 뜬다. - **Impact**: 런타임 에러 묵살. - **Suggested fix**: `try/catch` 또는 `.catch(err => setError(err))` 와 재시도 버튼 UI 제공. ### [major] M2 네이티브 `confirm()` / `alert()` 사용 - **Location**: `frontend/src/app/subjects/page.tsx:72, 77`, `frontend/src/app/subjects/[id]/page.tsx:70` 등 - **Impact**: 일부 모바일 WebView, PWA 컨텍스트에서 차단될 수 있음. 삭제 취소 후 사용자 피드백 없음. - **Suggested fix**: styled-components 기반 custom modal 또는 toast 로 교체. ## Minor Findings ### [minor] m1 `baseCorrectRate` state 타입이 string이라 number DTO 계약과 비대칭 - **Location**: `frontend/src/app/study/page.tsx:38, 77` - **Evidence**: `useState('')` + `type="number"` 입력. 전송 시 `Number(...) / 100`. 현재는 런타임 정상 작동하지만 타입 계약이 흐릿. - **Suggested fix**: state 를 `useState('')` 로 명시. ### [minor] m2 dashboard `s.tag.subject.color` optional chaining 부재 - **Location**: `frontend/src/app/dashboard/page.tsx:113` - **Evidence**: backend `DashboardService.summary()` 가 `skillSnapshot` → `tag` → `subject` 를 항상 include 하므로 **런타임 crash 없음**. Worker 는 major 로 올렸지만 재확인 결과 minor. - **Suggested fix**: 일관성 유지 목적이면 `s.tag.subject?.color ?? theme.color.accent`. ### [minor] m3 `profile/page.tsx` setTimeout cleanup 없음 - **Location**: `profile/page.tsx:77` - **Impact**: unmount 후 setState 경고 (dev only). - **Suggested fix**: `useEffect(() => { if (saved) { const t = setTimeout(...); return () => clearTimeout(t); } }, [saved])`. ### [minor] m4 `AppShell` 에 서버 500 에러 처리 없음 - **Location**: `frontend/src/components/layout/AppShell.tsx:30-42` - **Evidence**: 401 은 로그인 페이지로 rebound, 기타 에러는 `clearToken + /login` 으로 같이 처리됨 → **과잉 로그아웃**. 500 일 때도 토큰 지움. - **Suggested fix**: `catch` 에서 status 분기. 401 만 로그아웃. ### [minor] m5 form 에러 메시지 `role="alert"` 부재 - **Location**: login/register/study/profile 페이지 - **Impact**: screen reader 미공지. - **Suggested fix**: ``. ## Recommendations - axios 전역 `timeout: 10_000` 추가 (`frontend/src/lib/api.ts`). - `router.back()` fallback — `window.history.length` 체크 후 `/study/history` 로. ## Verdict **REQUEST_CHANGES** — critical 2건, major 2건. 대부분 UX·안정성 버그이며 배포 전 반드시 수정.