chore(review): Phase 6 2nd-pass APPROVE — fix cluster 전부 해결 확인
- harness-review 2nd pass (base6d8deef→ head71cf469): APPROVE - 1st pass 의 major 1 + minor 4 전부 해결 검증 - 회귀 없음, AI residuals clean - 새 recommendation 2건 (iterationsToTarget null 가드, Problem IDOR) 은 Phase 7 확장 시 주의사항으로 tracking .claude/state/review-result.json: 2nd pass 결과 .claude/state/review-result.1st.json: 1st pass 결과 (히스토리)
This commit is contained in:
111
.claude/state/review-result.1st.json
Normal file
111
.claude/state/review-result.1st.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"schema_version": "review-result.v1",
|
||||
"verdict": "REQUEST_CHANGES",
|
||||
"reviewer_profile": "static",
|
||||
"base_ref": "e0ac7a4",
|
||||
"head_ref": "871ca89",
|
||||
"scope": "phase-6",
|
||||
"commits": [
|
||||
"9939f29 feat(phase6): mastery-path API + stats UI 재설계 + Pretendard + 드래그 방지",
|
||||
"f22f837 feat(stats): mastery-path ceilingP + effectiveTarget",
|
||||
"871ca89 feat(phase6): BM tiers + Landing + Pricing + 평가원 기출 ProblemSet"
|
||||
],
|
||||
"stats": {
|
||||
"files_changed": 27,
|
||||
"insertions": 2398,
|
||||
"deletions": 115
|
||||
},
|
||||
"ai_residuals": {
|
||||
"tool": "review-ai-residuals",
|
||||
"scan_mode": "diff",
|
||||
"summary": { "verdict": "APPROVE", "major": 0, "minor": 0, "recommendation": 0, "total": 0 }
|
||||
},
|
||||
"critical_issues": [],
|
||||
"major_issues": [
|
||||
{
|
||||
"id": "P6.major.1",
|
||||
"category": "Security",
|
||||
"location": "backend/src/study-logs/study-logs.service.ts:64-79",
|
||||
"issue": "problemId 가 서버에서 존재 검증 없이 INSERT 된다. 존재하지 않는 problemId 를 넘기면 Prisma FK 위반으로 500 이 튀어 raw stack trace 가 노출될 수 있고, 500 vs 400 응답 차이로 problemId 존재 여부가 leak 된다.",
|
||||
"suggestion": "tagId 와 동일한 패턴으로 사전 조회 후 없으면 NotFoundException. subjectId/tagId 체크 블록 바로 뒤에 삽입."
|
||||
}
|
||||
],
|
||||
"observations": [
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Security",
|
||||
"location": "backend/src/problem-sets/problem-sets.controller.ts, backend/src/stats/stats.controller.ts:58",
|
||||
"issue": "subscriptionTier 가 저장만 되고 어디서도 enforce 안 됨. Landing/Pricing 이 mastery-path/problem-sets 를 Pro 전용으로 광고하지만 실제 gating 은 없음.",
|
||||
"suggestion": "Phase 7 결제 연동 시 TierGuard 또는 inline 체크. 현재는 TODO 주석 필수."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/stats/page.tsx:256-262",
|
||||
"issue": "masteryPath 가 이미 목표 도달 상태일 때 iterationsToTarget=0, daysToTarget=0 으로 반환하고 프론트 카피가 '앞으로 0회차 더 복습하면 약 0일 뒤 마스터' 같은 어색한 문구를 낸다.",
|
||||
"suggestion": "iterationsToTarget === 0 특수 분기 추가: '이미 목표 P 에 도달한 상태야 ✨'."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Accessibility",
|
||||
"location": "frontend/src/app/stats/page.tsx:142-156, frontend/src/app/pricing/page.tsx:37-43, frontend/src/app/study/page.tsx",
|
||||
"issue": "마스터리/망각 토글, 월/연 토글, study 탭에 aria-pressed 또는 role=tab/aria-selected 가 없음. 스크린리더가 상태 전달 못함. WCAG 2.1 SC 4.1.2.",
|
||||
"suggestion": "각 토글 버튼에 aria-pressed={$active} 추가."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "AI Residuals",
|
||||
"location": "frontend/src/app/page.tsx:58, backend/prisma/seed.ts:105",
|
||||
"issue": "Landing 에 demo@reloop.local / demo1234 가 공개. seed log 에도 평문 비밀번호 stdout 출력. 데모 단계엔 괜찮지만 런칭 시 제거 대상.",
|
||||
"suggestion": "TODO 주석 추가. Phase 7 런칭 전 제거 또는 /demo 라우트로 분리."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Performance",
|
||||
"location": "backend/src/problem-sets/problem-sets.service.ts:19-28",
|
||||
"issue": "getOne() 의 problems include 에 take 없음. 현재 스케일(10문제) 문제 없음. 전과목 번들이 되면 주의.",
|
||||
"suggestion": "방어적으로 take: 100 또는 나중에 커서 페이지네이션."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Performance",
|
||||
"location": "frontend/src/app/stats/page.tsx:9-10",
|
||||
"issue": "MasteryPathChart, ForgetCurveChart 둘 다 정적 import. recharts 는 무거운 라이브러리(~140KB gz). 토글당 한쪽만 렌더되므로 dynamic import 가치.",
|
||||
"suggestion": "next/dynamic 으로 각 차트 lazy 로드 (ssr:false)."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/pricing/page.tsx:86-89",
|
||||
"issue": "'7일 무료 체험 시작' 버튼이 /register 로만 보내고 실제 체험 로직 없음. 사용자 신뢰 리스크.",
|
||||
"suggestion": "Phase 7 전까지 '무료로 시작하기 (Pro 체험 Phase 7 예정)' 로 문구 변경 또는 disabled."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Security",
|
||||
"location": "frontend/src/app/pricing/page.tsx:108",
|
||||
"issue": "<a href='mailto:op@nabomhalang.co.kr'> 평문 노출로 스팸 harvesting 리스크.",
|
||||
"suggestion": "문의 폼 또는 JS 난독화."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/page.tsx:13-23",
|
||||
"issue": "Landing 이 useEffect 안에서 hasToken() 체크 후 setReady. SSR 초기는 null 렌더. 초기 flash/CLS 가능.",
|
||||
"suggestion": "Next.js middleware 쿠키 기반 redirect 또는 full landing 을 initial state 로."
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"critical": 0,
|
||||
"major": 1,
|
||||
"minor": 4,
|
||||
"recommendation": 5,
|
||||
"verdict_rule_applied": "1 major → REQUEST_CHANGES"
|
||||
},
|
||||
"next_actions": [
|
||||
"Fix P6.major.1 in study-logs.service.ts with NotFoundException guard",
|
||||
"Minor fixes 3건 (마스터리 카피, aria-pressed, demo credential 주석) 묶어서 한 commit",
|
||||
"Subscription tier enforcement TODO 주석 추가 — Phase 7 연동",
|
||||
"Recommendation 5건은 별도 tracking 태스크로 Phase 7 이후 일정"
|
||||
]
|
||||
}
|
||||
@@ -1,19 +1,31 @@
|
||||
{
|
||||
"schema_version": "review-result.v1",
|
||||
"verdict": "REQUEST_CHANGES",
|
||||
"verdict": "APPROVE",
|
||||
"reviewer_profile": "static",
|
||||
"base_ref": "e0ac7a4",
|
||||
"head_ref": "871ca89",
|
||||
"scope": "phase-6",
|
||||
"scope": "phase-6-fix-cluster-2nd-pass",
|
||||
"pass": 2,
|
||||
"first_pass_ref": ".claude/state/review-result.1st.json",
|
||||
"base_ref": "6d8deef",
|
||||
"head_ref": "71cf469",
|
||||
"commits": [
|
||||
"9939f29 feat(phase6): mastery-path API + stats UI 재설계 + Pretendard + 드래그 방지",
|
||||
"f22f837 feat(stats): mastery-path ceilingP + effectiveTarget",
|
||||
"871ca89 feat(phase6): BM tiers + Landing + Pricing + 평가원 기출 ProblemSet"
|
||||
"779e2cf fix(study-logs): validate problemId existence — P6-F1 (review major)",
|
||||
"74c05a5 fix(stats): already-mastered case copy — P6-F2 (review minor)",
|
||||
"26cfec0 a11y(toggles): aria-pressed on stats/pricing/study toggle buttons — P6-F3 (review minor)",
|
||||
"9887586 chore(todo): mark demo credential exposure for phase7 removal — P6-F4 (review minor)",
|
||||
"ddce6c9 chore(todo): mark mastery-path + problem-sets for phase7 tier gating — P6-F5 (review minor)",
|
||||
"71cf469 plans: P6-F1..F5 completed (harness-work breezing 5/5)"
|
||||
],
|
||||
"stats": {
|
||||
"files_changed": 27,
|
||||
"insertions": 2398,
|
||||
"deletions": 115
|
||||
"files_changed": 8,
|
||||
"insertions": 28,
|
||||
"deletions": 9
|
||||
},
|
||||
"finding_resolution": {
|
||||
"P6.major.1_problemId": "해결 — study-logs.service.ts:52-57 에 findUnique + NotFoundException('problem'). $transaction 진입 전 early-exit. tagId 와 동일 패턴.",
|
||||
"minor_subscription_tier": "해결(defer) — stats.controller.ts:58-59 + problem-sets.controller.ts:27-28 에 TODO(phase7) 주석.",
|
||||
"minor_mastery_zero_iterations": "해결 — stats/page.tsx:258-259 에 iterationsToTarget === 0 최우선 분기.",
|
||||
"minor_toggle_aria_pressed": "해결 — 3 파일 6 토글 전부 aria-pressed 부여, $active 유지.",
|
||||
"minor_demo_credential": "해결(defer) — page.tsx:58, seed.ts:105 에 TODO(phase7) 주석."
|
||||
},
|
||||
"ai_residuals": {
|
||||
"tool": "review-ai-residuals",
|
||||
@@ -21,91 +33,34 @@
|
||||
"summary": { "verdict": "APPROVE", "major": 0, "minor": 0, "recommendation": 0, "total": 0 }
|
||||
},
|
||||
"critical_issues": [],
|
||||
"major_issues": [
|
||||
{
|
||||
"id": "P6.major.1",
|
||||
"category": "Security",
|
||||
"location": "backend/src/study-logs/study-logs.service.ts:64-79",
|
||||
"issue": "problemId 가 서버에서 존재 검증 없이 INSERT 된다. 존재하지 않는 problemId 를 넘기면 Prisma FK 위반으로 500 이 튀어 raw stack trace 가 노출될 수 있고, 500 vs 400 응답 차이로 problemId 존재 여부가 leak 된다.",
|
||||
"suggestion": "tagId 와 동일한 패턴으로 사전 조회 후 없으면 NotFoundException. subjectId/tagId 체크 블록 바로 뒤에 삽입."
|
||||
}
|
||||
],
|
||||
"major_issues": [],
|
||||
"observations": [
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Security",
|
||||
"location": "backend/src/problem-sets/problem-sets.controller.ts, backend/src/stats/stats.controller.ts:58",
|
||||
"issue": "subscriptionTier 가 저장만 되고 어디서도 enforce 안 됨. Landing/Pricing 이 mastery-path/problem-sets 를 Pro 전용으로 광고하지만 실제 gating 은 없음.",
|
||||
"suggestion": "Phase 7 결제 연동 시 TierGuard 또는 inline 체크. 현재는 TODO 주석 필수."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/stats/page.tsx:256-262",
|
||||
"issue": "masteryPath 가 이미 목표 도달 상태일 때 iterationsToTarget=0, daysToTarget=0 으로 반환하고 프론트 카피가 '앞으로 0회차 더 복습하면 약 0일 뒤 마스터' 같은 어색한 문구를 낸다.",
|
||||
"suggestion": "iterationsToTarget === 0 특수 분기 추가: '이미 목표 P 에 도달한 상태야 ✨'."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "Accessibility",
|
||||
"location": "frontend/src/app/stats/page.tsx:142-156, frontend/src/app/pricing/page.tsx:37-43, frontend/src/app/study/page.tsx",
|
||||
"issue": "마스터리/망각 토글, 월/연 토글, study 탭에 aria-pressed 또는 role=tab/aria-selected 가 없음. 스크린리더가 상태 전달 못함. WCAG 2.1 SC 4.1.2.",
|
||||
"suggestion": "각 토글 버튼에 aria-pressed={$active} 추가."
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"category": "AI Residuals",
|
||||
"location": "frontend/src/app/page.tsx:58, backend/prisma/seed.ts:105",
|
||||
"issue": "Landing 에 demo@reloop.local / demo1234 가 공개. seed log 에도 평문 비밀번호 stdout 출력. 데모 단계엔 괜찮지만 런칭 시 제거 대상.",
|
||||
"suggestion": "TODO 주석 추가. Phase 7 런칭 전 제거 또는 /demo 라우트로 분리."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Performance",
|
||||
"location": "backend/src/problem-sets/problem-sets.service.ts:19-28",
|
||||
"issue": "getOne() 의 problems include 에 take 없음. 현재 스케일(10문제) 문제 없음. 전과목 번들이 되면 주의.",
|
||||
"suggestion": "방어적으로 take: 100 또는 나중에 커서 페이지네이션."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Performance",
|
||||
"location": "frontend/src/app/stats/page.tsx:9-10",
|
||||
"issue": "MasteryPathChart, ForgetCurveChart 둘 다 정적 import. recharts 는 무거운 라이브러리(~140KB gz). 토글당 한쪽만 렌더되므로 dynamic import 가치.",
|
||||
"suggestion": "next/dynamic 으로 각 차트 lazy 로드 (ssr:false)."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/pricing/page.tsx:86-89",
|
||||
"issue": "'7일 무료 체험 시작' 버튼이 /register 로만 보내고 실제 체험 로직 없음. 사용자 신뢰 리스크.",
|
||||
"suggestion": "Phase 7 전까지 '무료로 시작하기 (Pro 체험 Phase 7 예정)' 로 문구 변경 또는 disabled."
|
||||
"location": "frontend/src/app/stats/page.tsx:261-266",
|
||||
"issue": "data.iterationsToTarget 타입은 number | null. capped/non-capped 분기에서 직접 interpolation 하므로 백엔드가 reached=true && null 을 반환할 경우 'null회차' 문자열이 렌더될 가능성. 현재 백엔드 구현상 발생 안 하지만 타입 계약이 런타임 안전성을 보장 못 함.",
|
||||
"suggestion": "discriminated union 으로 reached=true 일 때 iterationsToTarget: number 로 좁히거나 nullish 가드 추가."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Security",
|
||||
"location": "frontend/src/app/pricing/page.tsx:108",
|
||||
"issue": "<a href='mailto:op@nabomhalang.co.kr'> 평문 노출로 스팸 harvesting 리스크.",
|
||||
"suggestion": "문의 폼 또는 JS 난독화."
|
||||
},
|
||||
{
|
||||
"severity": "recommendation",
|
||||
"category": "Quality",
|
||||
"location": "frontend/src/app/page.tsx:13-23",
|
||||
"issue": "Landing 이 useEffect 안에서 hasToken() 체크 후 setReady. SSR 초기는 null 렌더. 초기 flash/CLS 가능.",
|
||||
"suggestion": "Next.js middleware 쿠키 기반 redirect 또는 full landing 을 initial state 로."
|
||||
"location": "backend/src/study-logs/study-logs.service.ts:52-57",
|
||||
"issue": "problemId 검증이 ownership/visibility 필드를 안 본다. 현재 Problem 이 공유 카탈로그라 무해하지만, 향후 Pro 유저의 커스텀 업로드가 생기면 동일 패턴이 IDOR 로 진화할 수 있음.",
|
||||
"suggestion": "Phase 7 에서 Problem 에 isPublic 또는 userId 필드를 추가할 때 이 쿼리에 필터 포함."
|
||||
}
|
||||
],
|
||||
"regression_check": {
|
||||
"tagId_null_problemId_present": "독립 분기라 안전",
|
||||
"tagId_present_problemId_null": "기존 동작 유지",
|
||||
"reached_true_iterations_null": "백엔드 구현상 불가. 런타임 crash 없음",
|
||||
"aria_pressed_forward": "표준 HTML attr 이라 styled-components forward 정상"
|
||||
},
|
||||
"summary": {
|
||||
"critical": 0,
|
||||
"major": 1,
|
||||
"minor": 4,
|
||||
"recommendation": 5,
|
||||
"verdict_rule_applied": "1 major → REQUEST_CHANGES"
|
||||
},
|
||||
"next_actions": [
|
||||
"Fix P6.major.1 in study-logs.service.ts with NotFoundException guard",
|
||||
"Minor fixes 3건 (마스터리 카피, aria-pressed, demo credential 주석) 묶어서 한 commit",
|
||||
"Subscription tier enforcement TODO 주석 추가 — Phase 7 연동",
|
||||
"Recommendation 5건은 별도 tracking 태스크로 Phase 7 이후 일정"
|
||||
]
|
||||
"major": 0,
|
||||
"minor": 0,
|
||||
"recommendation": 2,
|
||||
"verdict_rule_applied": "minor/recommendation only → APPROVE"
|
||||
}
|
||||
}
|
||||
|
||||
9
Plans.md
9
Plans.md
@@ -63,6 +63,15 @@ Phase 6 구현은 harness 규칙 없이 직선 실행됐고, 그 뒤 `harness-re
|
||||
|
||||
Recommendation 5건 (pagination cap, dynamic import, pricing CTA 문구, mailto 난독화, Landing SSR 플래시) 은 별도 tracking 으로 Phase 7 이후.
|
||||
|
||||
## Phase 6 2nd-pass 감사 결과
|
||||
|
||||
- base `6d8deef` → head `71cf469` (fix cluster 5 commit)
|
||||
- verdict: **APPROVE**
|
||||
- finding resolution: major 1 + minor 4 전부 해결, 회귀 없음
|
||||
- 새 recommendation 2건 (iterationsToTarget null 가드, Problem IDOR 방지) — Phase 7 확장 시 주의사항
|
||||
- review-result: `.claude/state/review-result.json` (2nd pass), `.1st.json` (1st pass)
|
||||
- AI Residuals: clean (0/0/0)
|
||||
|
||||
## Execution
|
||||
|
||||
Mode: **Breezing (명시 플래그 --breezing) — 2 Wave**
|
||||
|
||||
Reference in New Issue
Block a user