146 Commits

Author SHA1 Message Date
reloop
1dab709c5b plans: Phase 7D 완료 (12/12) — 12 페이지 재디자인 2026-04-12 05:21:17 +09:00
reloop
22ee6ab659 style(study): visual refresh matching phase 7D theme — 7D.12 2026-04-12 05:20:55 +09:00
reloop
22f3be27e2 feat(profile): redesigned /profile per design-09 — 7D.11 2026-04-12 05:16:34 +09:00
reloop
0e61f0e083 feat(stats): redesigned /stats per design-11 — 7D.10 2026-04-12 05:08:26 +09:00
reloop
08505072f5 feat(subjects): redesigned /subjects per design-07 — 7D.9 2026-04-12 05:01:11 +09:00
reloop
46c6a45531 feat(history): redesigned /study/history per design-10 — 7D.8 2026-04-12 04:54:41 +09:00
reloop
fdc3e4fe4a feat(review): focus mode session per design-05 — 7D.7 2026-04-12 04:49:18 +09:00
reloop
e79c4d8d17 feat(dashboard): empty state per design-08 — 7D.6 2026-04-12 04:40:52 +09:00
reloop
eeb772a755 feat(dashboard): redesigned /dashboard per design-01 — 7D.5 2026-04-12 04:37:28 +09:00
reloop
92e054c691 feat(pricing): redesigned /pricing per design-03 — 7D.4 2026-04-12 04:30:08 +09:00
reloop
750378c03a feat(auth): unified auth card per design-04 — 7D.3 2026-04-12 04:24:15 +09:00
reloop
0605ba846b feat(landing): redesigned / per design-02 — 7D.2 2026-04-12 04:18:53 +09:00
reloop
d35fc087c6 feat(shell): redesigned sidebar per design-01 — 7D.1 2026-04-12 04:09:26 +09:00
reloop
9c3efeb464 plans(7D): remap design numbers + archive 12 design HTML mockups 2026-04-12 04:02:03 +09:00
reloop
35195f3693 plans: Phase 7A 완료 (8/8) — KICE pdftotext 파이프라인 2026-04-12 03:46:44 +09:00
reloop
35dcf822d2 feat(kice): pdftotext-based import pipeline — 7A.2-7A.8
- scanKiceData: 2025/2026 국어/영어/한국사/생활과 윤리 PDF 스캔
- extractText: pdftotext -raw (problems) / -layout (answers), pdf-parse 폐기
- parseAnswerTable: 홀/짝 분리 + ①..⑤ → 1..5 정규화
- parseProblemPaper: [N~M] passage 범위 + 번호 monotonic 체크
- importKiceSet: ProblemSet/Passage/Problem 멱등 upsert
- seed.ts: DEMO_PROBLEMSET(2024 수학) 제거, User/Subject/Tag 만 유지
- CLI: cli:kice-import --year --subject --only --dry-run --sample

270 problems / 26 passages / 9 problem sets 로컬 검증 완료
2026-04-12 03:45:48 +09:00
reloop
e2e3e4bd6f feat(prisma): kice text fields + Passage model — 7A.1
- ProblemSet.audioUrls Json? (영어 듣기 mp3 배열)
- Problem 에 bodyText/choices/answerNumber/passageId/needsReview 추가
- Problem @@index([passageId]) 로 역방향 조회 보장
- 신규 Passage 모델 (공통 지문 [N~M] 분리용)
- examRound 추가 거부 — 기존 examType 이 이미 'sat'|'mock-june'|'mock-sept'|'academy' 커버
- .gitignore 에 data/kice/ (저작권 자료)

migration: 20260411183246_kice_text_fields
shadow DB INDEX 권한 이슈는 mariadb 에서 GRANT 로 해결
2026-04-12 03:32:59 +09:00
reloop
b12192eddf plans: Phase 7C 완료 (5/5) — theme/icon/fonts/primitives/toast
- 7C.1 theme.ts 확장 [ba4d6e4] — color/shadow/font.display/animations 추가
- 7C.2 Phosphor React Icon 어댑터 [0d2303a] — 57 아이콘 매핑
- 7C.3 Inter + JetBrains Mono next/font [140a7c3] — CSS variable 바인딩
- 7C.4 primitives.tsx 증축 [b9874ad] — Button white, Card glow, StatCard, KbdHint, PageHeader, AppShellLayout
- 7C.5 Toast 컴포넌트 [eb10dfe] — slideIn + undo + progress ring + hover pause

통합 빌드: 16/16 페이지 생성, TS 에러 0.
다음: Phase 7A (KICE PDF 파싱 파이프라인).
2026-04-12 03:24:22 +09:00
reloop
eb10dfe34f feat(toast): slide-in toast with undo + progress ring — 7C.5
Single-instance ToastProvider with requestAnimationFrame countdown,
hover-pause, per-variant SVG icons, circular SVG progress ring,
and slide-in-from-bottom animation. Wraps children in layout.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 03:23:15 +09:00
reloop
b9874ad25a feat(primitives): Button white + Card glow + StatCard + KbdHint + PageHeader + AppShellLayout — 7C.4 2026-04-12 03:20:21 +09:00
reloop
140a7c38f0 feat(fonts): inter + jetbrains mono via next/font — 7C.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 03:17:46 +09:00
reloop
0d2303ac1b feat(icon): phosphor react adapter — 7C.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 03:15:35 +09:00
reloop
ba4d6e4841 feat(theme): extend tokens for phase 7 design — 7C.1
Add color, shadow, font.display tokens and keyframe animations from
phase 7 design files. All existing tokens preserved unchanged.
2026-04-12 03:12:12 +09:00
reloop
96b2be58e1 plan(phase7): archive 0-6 history + revise 7A/7C for text parsing + styled-components
- Plans.md: 203행 → 140행. Phase 0-6 완료 이력을 .claude/archive/plans-phase0-6.md 로 분리
- Phase 7 title 에서 'Tailwind' 제거
- 7A 재작성: pdf-parse + 정규식 텍스트 파싱 파이프라인 (국어/영어/한국사/생활과윤리 4과목)
  + Passage 모델(공통지문), needsReview flag, 8 ProblemSet × ~240문항 MVP
- 7C 재작성: Tailwind 도입 취소. theme.ts 토큰 확장 + styled-components primitives 증축
  + Phosphor React + Inter/JetBrains Mono next/font + Toast 컴포넌트
- 기술 결정 D1/D2/D3 확정 반영 (사용자 컨펌)
- 수학/과탐은 Phase 8 별도 트랙 (이미지 crop 또는 Vision API)
2026-04-12 03:01:04 +09:00
reloop
e7127cdfdd plan(phase7): KICE import + UserProblemSet + Tailwind + 12 page redesign
harness-plan create 로 Phase 7 전체 스코프 분해:
- 7A (7 tasks): KICE PDF 파이프라인 — schema 확장, 정답표 파싱, /static/kice/ 서빙
- 7B (5 tasks): UserProblemSet — join 테이블, me/problem-sets API, /study 내 문제집 탭
- 7C (5 tasks): Tailwind 도입 — 디자인 토큰 이식, Phosphor React, 새 primitives
- 7D (12 tasks): 12 페이지 재디자인 (AppShell, Landing, Auth, Pricing,
  Dashboard filled/empty, Review, Study History, Subjects, Stats, Profile, Study 폼)
- 7E (3 tasks): 배포 + harness-review

D1~D5 기술 결정 포인트 5건 사용자 컨펌 대기 상태로 표시.
2026-04-12 02:53:29 +09:00
reloop
cc946e4722 chore(review): Phase 6 2nd-pass APPROVE — fix cluster 전부 해결 확인
- harness-review 2nd pass (base 6d8deef → head 71cf469): 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 결과 (히스토리)
2026-04-12 01:52:06 +09:00
reloop
71cf469c43 plans: P6-F1..F5 completed (harness-work breezing 5/5) 2026-04-12 01:47:49 +09:00
reloop
ddce6c9a85 chore(todo): mark mastery-path + problem-sets for phase7 tier gating — P6-F5 (review minor) 2026-04-12 01:47:19 +09:00
reloop
98875866b0 chore(todo): mark demo credential exposure for phase7 removal — P6-F4 (review minor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:46:07 +09:00
reloop
26cfec0b5b a11y(toggles): aria-pressed on stats/pricing/study toggle buttons — P6-F3 (review minor) 2026-04-12 01:44:51 +09:00
reloop
74c05a5e12 fix(stats): already-mastered case copy — P6-F2 (review minor) 2026-04-12 01:43:39 +09:00
reloop
779e2cfb3c fix(study-logs): validate problemId existence — P6-F1 (review major) 2026-04-12 01:41:54 +09:00
reloop
6d8deefabb chore(review): Phase 6 harness-review 감사 결과 기록
- .claude/state/review-result.json: review-result.v1 스키마
  verdict=REQUEST_CHANGES, critical 0, major 1, minor 4, rec 5.
- Plans.md: Phase 6 구현 태스크(P6-T1~T5)를 소급 기록 +
  follow-up fix cluster(P6-F1~F5)를 cc:WIP 로 등록.
2026-04-12 01:37:43 +09:00
reloop
871ca89da0 feat(phase6): BM tiers + Landing + Pricing + 평가원 기출 ProblemSet — tracks 2/4
트랙 2 — BM / Landing / Pricing
- schema.prisma: SubscriptionTier enum(free/pro/school) + User.subscriptionTier
  + User.subscriptionUntil. Migration 20260411161837_bm_and_problemsets.
- auth.service.ts / me.service.ts safeUser/toView 에 구독 필드 포함.
- frontend/lib/api.ts MeUser 에 subscriptionTier/subscriptionUntil 추가.
- app/page.tsx 전면 재작성: 토큰 있으면 /dashboard, 없으면 비로그인 Landing
  (히어로 + 왜 ReLoop 3카드 + 페르소나 4장 + 4단계 루프 + 가격 티저 +
  FAQ 4개 + footer).
- app/pricing/page.tsx 신규: Free/Pro/School 3 티어 카드(Pro 강조 PopBadge),
  월/년 cycle 토글, 기능 비교 테이블(데스크탑 전용), FAQ 5개, 바닥 CTA.
  Pro ₩7,900/월, 연간 ₩79,000(2개월 무료), School ₩20,000/월·30시트.
- app/profile/page.tsx: 구독 섹션 + 티어 배지 + pricing 으로 가는 업그레이드
  버튼.

트랙 4 — 평가원 기출 ProblemSet 데모
- schema.prisma: ProblemSet + Problem 신규 모델. StudyLog.problemId
  (optional, SetNull). 각 ProblemSet 은 (year, examType, subjectName)
  복합 unique.
- prisma/seed.ts: 2024학년도 수능 수학 샘플 10 문항 (1/5/8/11/13/15/20/22/
  28/30번) 대략치 난이도+정답률로 upsert. 실제 KICE 수치로 교체 가능하도록
  주석 명시.
- backend/src/problem-sets/: ProblemSetsModule + Service + Controller.
  GET /api/problem-sets (list, subject/year 필터) + GET /api/problem-sets/:id
  (문제 배열 포함). JwtAuthGuard 적용.
- study-logs.service/controller: problemId optional 필드 수용.
- frontend/lib/api.ts: ProblemSetSummary/ProblemSetDetail/Problem 타입.
- frontend/app/study/page.tsx 재설계: 상단 탭 [직접 입력 | 문제집에서 가져오기].
  문제집 탭은 set 드롭다운 + 문제 리스트 클릭 → title/difficulty/baseCorrectRate
  자동 채움 + subjectName 매핑으로 사용자 과목 자동 선택 + topic 을 tag
  이름과 매칭. 결과/메모/시간만 수동 입력.
2026-04-12 01:26:35 +09:00
reloop
f22f837fe2 feat(stats): mastery-path ceilingP + effectiveTarget — 고난이도 문제에도 희망 카피
- stats.service.ts: ceilingP = σ(k·(1-D)) 계산. 이 문제의 이론적 P 상한.
  effectiveTarget = min(targetP, ceilingP - 0.02) 로 좁혀서 실제 도달 가능한
  현실적 목표를 쓴다. 응답에 ceilingP/effectiveTarget 추가.
- api.ts: MasteryPathResponse 에 두 필드 추가.
- stats/page.tsx: capped 인 경우 '이 난이도의 한계는 X% 야. N회차 K일 뒤 그
  한계 근처에 도달해' 카피 + 목표 배지에 '(한계)' 표시.
- MasteryPathChart: 기준선을 effectiveTarget 으로.
2026-04-12 01:13:55 +09:00
reloop
9939f299d8 feat(phase6): mastery-path API + stats UI 재설계 + Pretendard + 드래그 방지
트랙 1 — Pretendard 웹폰트 확실히 로드
- frontend/src/app/layout.tsx: Pretendard Variable CDN link 추가
- frontend/src/styles/theme.ts: font.sans 에 'Pretendard Variable' 우선

트랙 3 — 통계 재설계 (까먹음 → 마스터리 경로)
- backend/src/stats/stats.service.ts: masteryPath() 추가. 이번 복습부터 계속
  correct 를 가정한 best-case 궤적을 10회차까지 시뮬레이션해서 각 회차별
  s0/p/daysFromNow 를 반환. P >= targetP 도달 시 iterationsToTarget +
  daysToTarget 도 함께 전달.
- backend/src/stats/stats.controller.ts: GET /api/stats/mastery-path 추가.
- frontend/src/lib/api.ts: MasteryPathResponse 타입 + ForgetCurveResponse
  의 tag.subject 를 실제 select 에 맞춰 좁힘.
- frontend/src/components/charts/MasteryPathChart.tsx: recharts LineChart
  로 X=iteration, Y=P% + 목표 수평선 + S₀ 보조 라인.
- frontend/src/app/stats/page.tsx: [마스터리 경로 | 망각 곡선] 토글.
  마스터리 뷰는 'N회차 + K일 뒤 마스터 예상' 카피 + 회차별 요약 테이블.

트랙 5 — 드래그/선택 기본 금지 + 예외
- frontend/src/styles/GlobalStyle.ts: body 에 user-select:none +
  input/textarea/[data-selectable='true'] 만 예외 허용. img 드래그 금지.
- frontend/src/components/ui/primitives.tsx: Selectable 컴포넌트 추가.
- review/page.tsx, review/history/page.tsx, study/history/page.tsx:
  문제 제목과 메모에 data-selectable 부여 (검색용 복사 허용).
2026-04-12 01:08:49 +09:00
reloop
e0ac7a4c42 chore(backend): fix tsconfig rootDir + @nestjs/jwt 11 expiresIn type + init migration
- backend/tsconfig.json: rootDir=./src + include/exclude to fix nest build
  output (was emitting dist/src/main.js, now dist/main.js)
- backend/src/auth/auth.module.ts: @nestjs/jwt 11 tightened signOptions.
  expiresIn to ms StringValue; cast env-sourced string via
  JwtModuleOptions['signOptions']['expiresIn'] to satisfy the type
  without pulling in @types/jsonwebtoken.
- backend/prisma/migrations/20260411153520_init: initial migration
  generated by 'prisma migrate dev --name init' against the corrected
  schema (SkillSnapshot.tagId nullable, SetNull cascades).

Verified end-to-end against https://reloop-api.nabomhalang.co.kr on
Dev VM: login → create study-log → queue → submit → 409 guard all
passing.
2026-04-12 00:45:04 +09:00
reloop
286d18f126 plans: mark QA fix clusters A-F as completed, close 18 fix proposals 2026-04-12 00:21:56 +09:00
reloop
3d7866be7e chore(deploy): postinstall, standalone, PM2 ecosystem, README — QA-6.C2 C4 M1 M2
backend/package.json
- Add 'postinstall: prisma generate' — fresh pnpm install now produces
  a usable @prisma/client without a manual step.
- Add engines.node >= 20 and packageManager pnpm@9.12.0.

frontend/package.json
- Same engines + packageManager pin.

frontend/next.config.js
- output: 'standalone' enabled for PM2's .next/standalone/server.js
  pattern (reduces production bundle size, removes runtime node_modules
  dependency for the web process).

frontend/.env.example (new)
- Documents NEXT_PUBLIC_API_URL as a BUILD-TIME variable.

ecosystem.config.js (new)
- PM2 config for reloop-api (cwd=backend, script=dist/main.js, port
  3001) and reloop-web (cwd=frontend, script=.next/standalone/server.js,
  port 3000, HOSTNAME=0.0.0.0). max_memory_restart=512M each with
  separate error/out log files.

README.md (new)
- Root deploy guide: stack, env vars table (backend + frontend),
  local dev quickstart, prod deploy steps (rsync → pnpm install
  --frozen-lockfile → prisma migrate deploy → build → copy .next/static
  and public into standalone → pm2 start), reverse proxy mapping,
  pm2 ops. Also covers Cluster F items: 'Dev VM 에서 prisma migrate
  dev --name init' for migrations and 'pnpm install' for lockfile
  generation.

.gitignore
- Ignore .claude/state/session* + .claude/state/*.lock +
  .claude/sessions/ (ephemeral harness runtime metadata).
2026-04-12 00:09:40 +09:00
reloop
e0974b8647 fix(frontend): NaN route guard, ConfirmDialog, history error states — QA-4.C2 M1 M2
components/ui/Modal.tsx (new)
- ConfirmDialog: overlay + card backed by theme tokens, role=dialog +
  aria-modal, backdrop click = cancel, stopPropagation on card.

app/subjects/[id]/page.tsx
- subjectId = Number.isFinite(raw) && raw > 0 ? raw : null
- Early return rendering InvalidWrap when subjectId is null; no API call
  with NaN, no infinite 'loading' deadend for /subjects/abc.
- Tag delete uses ConfirmDialog via confirmDelete state (no confirm()).

app/subjects/page.tsx
- Native confirm()/alert() eliminated. Delete flow uses ConfirmDialog
  with danger tone + ToastBar for success/failure feedback.

app/review/history/page.tsx
app/study/history/page.tsx
app/stats/page.tsx
- Fetches wrapped in useCallback, .catch() sets an error state, render
  shows ErrorBlock with a '다시 시도' retry button.
- stats/page.tsx gets both subjectsError (full-page) and curveError
  (inline in the curve card) plus independent retries.
- Cancelled-flag pattern (let cancelled = false) prevents stale
  setState after unmount across all four pages.
2026-04-12 00:09:24 +09:00
reloop
7495416f3c fix(auth): email normalize + JWT revalidate + secret required + strict whitelist — QA-3
auth.service.ts
- register() / login() normalize email via trim().toLowerCase() before
  every query; stored email is already normalized.
- register() conflict message is now generic 'registration failed'
  (status still 409) to reduce enumeration surface.

jwt.strategy.ts
- validate() is async, injects PrismaService, re-fetches the user from
  DB with select { id, email }, throws UnauthorizedException('user not
  found') if the row is gone. Deleted/deactivated accounts can no
  longer reuse an outstanding 30d token.
- Constructor throws if JWT_SECRET env var is missing (hardcoded
  dev fallback 'dev-reloop-secret-change-me' removed).

auth.module.ts
- JwtModule.registerAsync with useFactory that throws if JWT_SECRET is
  absent. No silent fallback.

main.ts
- ValidationPipe: forbidNonWhitelisted flipped false → true. Unknown
  DTO fields now fail fast instead of being silently stripped.
2026-04-12 00:09:06 +09:00
reloop
bc5e14e3e5 fix(reviews): atomic submit/skip + per-item review lock — QA-5.C1 M1 + QA-4.C1
backend/src/reviews/reviews.service.ts
- submit(): entire write path (status flip + snapshot upsert + next
  schedule create) now runs inside prisma.$transaction(async tx => ...)
- updateMany where { id, status: 'pending' } + count===0 → ConflictException
  acts as the atomicity gate; concurrent POSTs can no longer double-write.
- Next iteration is re-fetched inside the tx via findFirst done+desc, so
  racing requests can't both compute iteration=N+1 off a stale read.
- Single const now = new Date() shared across snapshot and schedule call
  (also addresses QA-2 minor m2).
- skip() gets the same updateMany guard and throws ConflictException when
  the row has already been processed.

frontend/src/app/review/page.tsx
- Global 'submitting: boolean' replaced with 'pending: Set<number>' +
  isPending(id) helper.
- submit()/skip() guard re-entry via if (isPending(id)) return and use
  functional Set updates in finally.
- All four Button elements disabled={isPending(q.id)} — only the card in
  flight is locked, not the whole page.

Closes the double-submit race that corrupted sampleCount / iteration.
2026-04-12 00:08:54 +09:00
reloop
e8121e5bd0 fix(prisma): relax tag cascade to SetNull — QA-5.fix.C2 + M2
- SkillSnapshot.tagId: Int → Int?, Tag relation → onDelete: SetNull
- StudyLog.tag: explicit onDelete: SetNull (was implicit Restrict)
- DashboardService.summary() filters { tagId: { not: null } } so orphaned
  snapshots don't leak into the top-skills UI
- Schema header notes that prisma migrate dev --name relax_tag_cascade
  must run on Dev VM before deploy

Prevents silent data loss when users delete a tag (previous cascade
erased all skill history for that tag permanently).
2026-04-12 00:08:40 +09:00
reloop
b976342a97 plans: mark QA-1..6 as completed with verdict summary 2026-04-11 23:59:01 +09:00
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
reloop
99d5892eb4 chore: reloop-v2 initial import for harness breezing QA 2026-04-11 23:44:22 +09:00