- QA-1 Backend API contract: APPROVE (minor type contract 2) - QA-2 Persona forget algorithm: APPROVE (math verified) - QA-3 Auth & security: REQUEST_CHANGES - email normalize missing / JWT revalidate / secret fallback / whitelist - QA-4 Frontend runtime: REQUEST_CHANGES - review double-submit race / subjects/[id] NaN / history error states / native alerts - QA-5 Prisma & transactions: REQUEST_CHANGES - reviews.submit snapshot outside tx / tag cascade data loss / iteration race - QA-6 Build & deploy: REQUEST_CHANGES - missing migrations, postinstall, lockfile, PM2 ecosystem Follow-up fix proposals saved to .claude/state/pending-fix-proposals.jsonl (18 tickets)
5.2 KiB
5.2 KiB
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:
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<number>로 바꿔 per-review lock.const [pending, setPending] = useState<Set<number>>(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:
const subjectId = Number(params.id); // → NaN 가능 const load = useCallback(() => { api.get<Subject>(`/subjects/${subjectId}`) // `/subjects/NaN` 요청 ... }, [subjectId]); useEffect(() => { if (!Number.isFinite(subjectId)) return; // 이미 load 는 만들어진 상태 load(); }, [subjectId, load]);useParams는 string 만 주고,Number('abc')는NaN. useEffect guard 는 호출을 막아주지만, 렌더 직후return <Loading/>로 넘어가서 사용자는 로딩 스피너만 본다. 유효하지 않은 URL 처리 없음. - Impact:
/subjects/abc같은 URL 이 로딩 화면에서 멈춤. 404 리다이렉트 없음. - Suggested fix:
const raw = Number(params.id); const subjectId = Number.isFinite(raw) ? raw : null; // 컴포넌트 초입: if (subjectId === null) return <NotFound ... />;
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<string>('')+type="number"입력. 전송 시Number(...) / 100. 현재는 런타임 정상 작동하지만 타입 계약이 흐릿. - Suggested fix: state 를
useState<number | ''>('')로 명시.
[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:
<ErrorText role="alert">.
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·안정성 버그이며 배포 전 반드시 수정.