- 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)
3.6 KiB
QA-1 Report — Backend API Contract Audit
Worker: Explore agent · Lead review: 본 세션 (overflag 조정 적용)
Summary
- Routes found: 24 (모두
/apiprefix 적용) - 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 /api는 NEXT_PUBLIC_API_URL 기본값(https://reloop-api.nabomhalang.co.kr/api)에 포함되어 있으므로 frontend에서는 /auth/login 같은 경로로만 호출해도 정상 매핑.
Lead 리뷰 조정 사항
Worker가 올린 findings 중 일부 재분류:
- 기존 [critical]
/reviews/historyDate serialization: axios는 JSON 파싱 후 Date를 그대로 두지 않고 string으로 받는다. frontendHistoryItem.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 }— 여기서Subject는tags?: Tag[]를 포함한다. 하지만 backendstats.service.ts:27은{id, name, color}만 select한다. - Impact: 런타임 버그는 없지만, 향후 누군가
curve.tag.subject.tags를 추가하면 undefined 접근이 생긴다. - Suggested fix:
api.ts의ForgetCurveResponse.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.ts의 toView()와 auth.service.ts의 safeUser()가 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로 재분류한다.