Files
reloop-v2/.qa/QA-1.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

3.6 KiB
Raw Blame History

QA-1 Report — Backend API Contract Audit

Worker: Explore agent · Lead review: 본 세션 (overflag 조정 적용)

Summary

  • Routes found: 24 (모두 /api prefix 적용)
  • Frontend calls: 24 unique
  • critical: 0, major: 1, minor: 3, recommendation: 1
  • verdict: APPROVE (minor/recommendation만 남음)

Route × Frontend call 매칭 결과

모든 24개 라우트가 frontend에서 실제 호출되며, 반대로 frontend의 모든 호출은 유효한 backend 라우트로 매핑됨. HTTP 메서드, URL, payload 키는 전부 일치. Global prefix /apiNEXT_PUBLIC_API_URL 기본값(https://reloop-api.nabomhalang.co.kr/api)에 포함되어 있으므로 frontend에서는 /auth/login 같은 경로로만 호출해도 정상 매핑.

Lead 리뷰 조정 사항

Worker가 올린 findings 중 일부 재분류:

  • 기존 [critical] /reviews/history Date serialization: axios는 JSON 파싱 후 Date를 그대로 두지 않고 string으로 받는다. frontend HistoryItem.reviewedAt: string | null은 정확한 타입이다. new Date(it.reviewedAt).toLocaleString() 호출도 문제없음. → 오플래그. 제거.
  • 기존 [major] ForgetCurveResponse.tag.subject 타입: frontend 타입은 Subject(tags optional)지만 backend는 3필드만 select. frontend 코드가 curve.tag.subject.tags를 참조하지 않으므로 런타임 버그는 없다. 타입 계약이 느슨한 문제는 남지만 severity는 minor. → major → minor로 강등.

Findings (정제판)

[minor] ForgetCurveResponse 타입이 실제 응답보다 넓다

  • Location: frontend/src/lib/api.ts:121
  • Evidence: tag: { id; name; subject: Subject } — 여기서 Subjecttags?: Tag[]를 포함한다. 하지만 backend stats.service.ts:27{id, name, color}만 select한다.
  • Impact: 런타임 버그는 없지만, 향후 누군가 curve.tag.subject.tags를 추가하면 undefined 접근이 생긴다.
  • Suggested fix: api.tsForgetCurveResponse.tag.subject{ id: number; name: string; color: string }로 좁힌다.

[minor] /reviews/history 호출이 params 객체 대신 query string 리터럴

  • Location: frontend/src/app/review/history/page.tsx:28
  • Evidence: api.get<HistoryItem[]>('/reviews/history?limit=100') — 다른 페이지(study/history/page.tsx:39)는 {params: {limit, subjectId}}를 쓴다.
  • Suggested fix: api.get<HistoryItem[]>('/reviews/history', { params: { limit: 100 } }).

(철회됨) password hash 노출 추측

me.service.tstoView()auth.service.tssafeUser()가 whitelist로 필드를 고르므로 password는 클라이언트에 전달되지 않는다. /auth/me, /auth/login, /auth/register, /me/onboarding, /me/profile 응답 전부 안전하게 계산된 onboarded: boolean 파생값과 함께 MeUser shape로 정확히 돌아온다. Lead 초기 의심은 오인이었다.

[recommendation] POST/PATCH 호출에 response type 누락

  • Location: study/page.tsx:72, subjects/page.tsx:60, subjects/[id]/page.tsx:57
  • Evidence: api.post('/study-logs', ...) — 타입 파라미터 없음.
  • Impact: 응답 shape 변경 시 런타임까지 감지 안 됨.
  • Suggested fix: 타입 파라미터 명시.

Verdict

APPROVE (critical/major 없음)

Worker가 올린 findings를 Lead가 재검토한 결과, axios Date serialization 관련 "critical"은 오플래그였고, ForgetCurveResponse 타입 불일치는 런타임 버그로 이어지지 않는 minor. password hash 노출은 QA-3에서 critical로 재분류한다.