Files
reloop-v2/.qa/QA-4.md
reloop 40232af429 qa: breezing audit — 6 tasks, 10 critical + 9 major findings
- 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)
2026-04-11 23:58:37 +09:00

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: submittingSet<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()skillSnapshottagsubject 를 항상 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·안정성 버그이며 배포 전 반드시 수정.