Commit Graph

59 Commits

Author SHA1 Message Date
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
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
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
99d5892eb4 chore: reloop-v2 initial import for harness breezing QA 2026-04-11 23:44:22 +09:00