광범위 문항 은행 + 출제 UX (3-스프린트 로드맵 2/3)
요약Sprint 215 퀴즈 미니게임 코어 위에 콘텐츠를 채우고 출제 UX를 다양화한 3-스프린트 로드맵 2/3. 215가 QuizCategory enum(NETWORK/OS/DATABASE)·i18n categories 키·QuizStart 동적 카테고리 렌더·getRandomQuestions 셔플·difficulty 필드를 전방 선언해 둔 덕에, 216은 신규 추상화 없이 (1)콘텐츠 채우기 (2)이미 깔린 difficulty를 출제 UX로 노출 (3)콘텐츠 품질 자동 검증 (4)채점 정규화 엣지 보강만으로 완성했다. 5분야(자료구조/알고리즘/네트워크/운영체제/데이터베이스) 각 30문항 = 총 150문항으로 확장(DS/ALGO 12→30 균등 보강 포함), 분야+난이도 필터를 추가했다. getQuestionsByFilter(category, difficulty) 헬퍼를 신설하고 getRandomQuestions 시그니처에 difficulty('ALL' 기본)를 추가하되 rng를 4번째 인자로 밀어 테스트 주입을 보존했다. best 기록 키는 카테고리 단위를 유지(난이도별 분기는 storage 스키마 변경이라 217로 이월). 문항 은행이 커지므로 check-quiz-content.mjs 콘텐츠 lint(7개 규칙)를 신설해 CI quality-frontend 잡에 연동했고, 채점 정규화는 파이프라인 맨 앞에 .normalize('NFKC')를 추가해 전각 문자만 보강했다. 백엔드 0·프론트엔드 only. Critic 교차 리뷰는 머지 게이트에서 codex review --base d431dcf로 실행 예정.
목표
- Sprint 215 미니게임 코어 위에 광범위 문항 은행을 채운다 — 5분야 각 30문항 = 총 150문항.
- 215가 전방 선언해 둔
difficulty필드를 출제 UX(난이도 필터) 로 노출한다. - 문항 은행이 커지므로 콘텐츠 품질을 코드로 게이트한다 — 스키마/중복/누락 검증 lint를 CI에 연동.
- 채점 정규화의 실제 갭을 검증 후 최소 보강한다(추측 보강 금지).
배경
Sprint 215는 CS 퀴즈 미니게임의 코어 플레이 루프를 완성하면서, 후속 스프린트를 염두에 두고 다음을 전방 선언해 두었다.
QuizCategoryenum에NETWORK/OS/DATABASE포함 (콘텐츠는 미충전)- i18n
categories키 (전 분야 라벨 ko/en) QuizStart의 동적 카테고리 렌더 (enum 순회 → 추가 분야가 자동 노출)getRandomQuestions의 셔플 로직- 문항 타입의
difficulty필드 (값만 존재, UX 미노출)
따라서 216은 신규 코드를 발명하는 스프린트가 아니다. 이미 깔린 골격 위에 (1)콘텐츠를 채우고, (2)이미 존재하는 difficulty를 출제 UX로 노출하고, (3)커지는 콘텐츠의 품질을 자동 검증하고, (4)채점 정규화의 엣지를 보강하는 것이 전부다. 백엔드 기록 연동은 [[sprint-window]]에 정리된 로드맵대로 Sprint 217로 이월하며, 216은 프론트엔드 전용이다.
3-스프린트 로드맵([[feedback-sprint-scoping]]) 상의 위치:
- Sprint 215: 프론트 미니게임 코어 (단답형 채점 + 게임 루프 + PoC 2분야 24문항).
- Sprint 216 (본 스프린트): 광범위 문항 은행(5분야 150문항) + 출제 UX(난이도 필터) + 콘텐츠 lint + 채점 정규화 보강.
- Sprint 217: 로그인 사용자 기록 연동(QuizRecord 엔티티 + 마이그레이션).
storage.ts추상화를 서버 API로 교체.
결정
D0. 콘텐츠 볼륨 — 5분야 각 30문항(150), 전 분야 균등 보강
5분야(자료구조/알고리즘/네트워크/운영체제/데이터베이스) 각 30문항, 총 150문항으로 확장한다. 215에서 PoC로 12문항씩 담았던 자료구조·알고리즘도 12→30으로 균등 보강하여 전 분야가 동일한 깊이를 갖도록 한다. 사용자가 (a)30/분야, (b)전 분야 균등 보강, (c)난이도 필터 추가를 명시 선택했다.
D1. 난이도 필터 — 전방 선언된 difficulty를 출제 UX로 노출
types.ts에 215부터 존재하던 difficulty 필드를 출제 UX로 노출한다.
getQuestionsByFilter(category, difficulty)헬퍼를 신설한다(기존getQuestionsByCategory패턴 계승).getRandomQuestions시그니처에difficulty('ALL'기본)를 추가하되,rng를 4번째 인자로 이동하여 테스트의 결정적 rng 주입을 보존한다. 기본값'ALL'덕에 기존 호출·테스트는 무회귀.QuizStart에 난이도fieldset을 추가한다(기존aria-pressed토글 패턴 재사용, 신규 ui 0 — Palette 권한 보존).
D2. best 기록 키 — 카테고리 단위 유지(난이도별 분기 없음)
best 기록 키는 카테고리 단위를 유지하고, 난이도별로 분기하지 않는다. 난이도별 best는 storage 스키마 변경을 수반하므로, Sprint 217 서버 연동 시 함께 설계한다. 216은 storage.ts를 무변경으로 두어 215에서 정한 217 이월 경계를 준수한다.
D3. 콘텐츠 lint — check-quiz-content.mjs 신설 + CI 연동
문항 은행이 150문항으로 커지면 수동 검수의 한계가 명확하다. 품질 자동 검증 스크립트 frontend/scripts/check-quiz-content.mjs를 신설하고 CI quality-frontend 잡에 연동한다.
- 텍스트 파싱 방식 —
.ts를 직접 import할 수 없으므로(빌드 의존성 회피), node 빌트인만으로 데이터 파일을 텍스트 파싱한다. - 7개 규칙 — ① 중복 id ② id 네이밍 ③ 빈
acceptedAnswers④ ko/en 누락 ⑤ category enum 일치 ⑥ difficulty 허용값 ⑦ 분야별 최소 30문항. --strict위반 시exit 1로 CI hard gate화.
D4. 채점 정규화 — NFKC 전각 폴딩만 보강(추측 보강 금지)
현 normalizeAnswer는 하이픈/언더스코어/복수공백을 이미 흡수한다(정규식이 한/영/숫자 외 문자를 제거하므로). 따라서 이들은 회귀 고정 테스트만 추가하고 로직은 손대지 않는다. 실제 갭은 전각(full-width) 문자다 — 파이프라인 맨 앞에 .normalize('NFKC')를 추가하여 전각 영숫자/기호/공백을 폴딩한다(예: SQL → sql). 자동 동의어 매핑은 과적합 위험이 있어 추가하지 않고, 입력 흔들림은 acceptedAnswers 명시 배열로 흡수한다(215 결정 계승).
구현
브랜치 feat/sprint-216-quiz-content-bank, start commit d431dcf, 4 atomic commit. 프론트엔드 only(백엔드 0).
데이터 (src/data/quiz/)
data-structure.ts/algorithm.ts— 12 → 30문항으로 균등 보강network.ts/os.ts/database.ts— 신규 분야 각 30문항index.ts— 신규 분야 병합 +getQuestionsByFilter추가
5분야 총 150문항.
분야별 난이도 분포:
| 분야 | Easy | Medium | Hard |
|---|---|---|---|
| 자료구조 (DS) | 10 | 11 | 9 |
| 알고리즘 (ALGO) | 6 | 6 | 6 |
| 네트워크 (NET) | 12 | 13 | 5 |
| 운영체제 (OS) | 10 | 15 | 5 |
| 데이터베이스 (DB) | 9 | 13 | 8 |
분포 편차는 lint의 최소 수(분야별 30) 기준과 무관하며, CS 기초 개념의 자연 분포 결과로 허용한다.
로직 / UI
getQuestionsByFilter(category, difficulty)헬퍼 (D1)getRandomQuestions(category, count, difficulty='ALL', rng)—difficulty추가 +rng4번째 인자로 이동 (D1)QuizStart난이도fieldset(기존 토글 패턴 재사용, 신규 ui 0) +page.tsx필터 전달 + i18n 키
콘텐츠 lint / CI
frontend/scripts/check-quiz-content.mjs— 7개 규칙 텍스트 파서,--strict위반 시 exit 1 (D3).github/workflows/ci.ymlquality-frontend잡에 lint 스텝 추가
채점 정규화
grade.tsnormalizeAnswer파이프라인 맨 앞에.normalize('NFKC')추가 + 전각/하이픈/언더스코어/복수공백 회귀 테스트 (D4)
커밋
| 해시 | 내용 |
|---|---|
1b3095a | Wave A — 5분야 150문항 (DS/ALGO 12→30, network/os/database 신규 30씩) + index 병합 |
51230d9 | Wave B — 난이도 필터 UX (getQuestionsByFilter, QuizStart fieldset, page.tsx, i18n) + 테스트 갱신 |
680d4af | Wave C — check-quiz-content.mjs 콘텐츠 lint + ci.yml quality-frontend 스텝 |
7197d0b | Wave D — grade.ts NFKC 정규화 + 회귀 테스트 |
f2bdafd | Critic R1 P2 fix — 가용 풀 기반 문항 수 옵션 동적 제한 (QuizStart) |
검증
Oracle 직접 검증 (Critic R1 P2 수정 반영 후):
tsc --noEmit→ 0next lint→ 0 errors / 0 warningsnode frontend/scripts/check-quiz-content.mjs --strict→ 150문항 PASS (exit 0), 분야별 30 확인jest --coverage→ 146 suites · 1474 tests PASS / 0 fail (JEST EXIT 0, 임계값 충족), 글로벌 lines 87.47% · branches 78.9% (게이트 83% / 71% 충족)- quiz 컴포넌트 5종 ·
grade·storage·data/quizindex 모두 커버리지 100% next build→ ✓ Compiled,ƒ /[locale]/quiz36.9kB (215의 12.4kB → 150문항 반영 증가)
신규 패턴
- 콘텐츠 품질 lint 게이트 패턴 — 데이터가 커지는 정적 콘텐츠 은행은 스키마·무결성 검증 스크립트를 CI에 연동해 회귀를 차단한다.
.ts직접 import 대신 node 빌트인 텍스트 파싱으로 빌드 의존성을 회피하고,--strict로 hard gate화하여 콘텐츠 추가가 스키마/중복/누락/최소 수 규칙을 위반하면 CI에서 막는다.
Critic 교차 리뷰
R1 — P2 1건 (Codex, codex review --base d431dcf, codex-cli 0.130.0)
난이도 필터로 좁아진
(분야, 난이도)풀이 UI 문항 수 옵션(5/10)보다 작을 때, 콘텐츠 게이트는 분야 총량(30)만 검사하므로 통과하지만 사용자가 선택한 개수가 보장되지 않는다. 예)NETWORK의HARD는 4문항뿐이라 Network + Hard + 5를 고르면 5를 선택했는데 4문항으로 시작된다. 분야/난이도별 최소 수를 강제하거나, 풀이 작은 조합은 문항 수 선택을 조정/비활성화하라.
- 조치: 콘텐츠를 인위적으로
(분야,난이도)별 10개씩 맞추면 난이도 분포가 왜곡되므로(HARD는 본래 적음), UX 측 적응으로 해소했다. 작성자 수정f2bdafd— QuizStart가getQuestionsByFilter(category, difficulty).length로 가용 풀 크기를 산출해 문항 수 옵션을 동적 제한한다(available>=10→[5,10] /5~9→[5] /<5→[available] 단일). 파생값 클램프로onStart에 항상 가용 이하 count를 전달하고, 시작 버튼에 방어 가드를 둔다. 회귀 테스트 4건 추가,QuizStart.tsx커버리지 100% 유지. 콘텐츠·storage·i18n 무변경. - Critical / High 0건.
R2 — CLEAN (Codex, codex review --base d431dcf, 재리뷰)
The changes appear internally consistent: the new quiz categories and difficulty filter are wired through data, UI, tests, and CI content validation without an evident breaking issue.
- P0 / P1 / P2 / P3 0건.
최종 — CLEAN. R1 P2 수정(f2bdafd) 반영 후 회귀 없음.