runtime-tab-pattern
WASM(SQL/Pyodide)이랑 Server-side(Quiz) 런타임 구현 패턴 다름.
runtime tab 구현 방식 두 가지로 나뉨.
- In-browser WASM (SQL, Pyodide) SQL은 sql.js, ML은 예전에 pyodide 썼는데 지금은 server-side로 갈아탐 (pyodide worker 삭제됨).
[구현 패턴]
- public/[x]-worker.js: worker 로직. init -> ready, run -> result|error 프로토콜.
- lib/[x]/client.ts: worker랑 통신하는 bridge.
- [x]-problems/[slug]/: 문제 데이터 폴더.
SQL은 데이터 직접 수정하면 안 됨. 스크립트로 생성해야 함.
rm -rf sql-problems && node scripts/gen-sql-problems.mjs
- SQL 특징: 정처기 스타일로 한글 식별자 사용. difficulty는 easy/medium만 있음.
- Grading: DDL/DML은
verify쿼리 돌려서 state 비교하는 방식으로 퉁침. -
🤖 클로드 해설:
lib/sql-notes.ts에서 unit 기반으로 팁 가져와서 보여줌. -
Server-side Execution (Quiz: C/Java/Python) 이건 런타임 아니라 output-prediction 퀴즈임. 코드는 read-only.
[구현 패턴]
- app/api/run/route.ts: 실제 컴파일/실행 담당 (Server-only).
- scripts/verify-quiz.mjs [lang]: 작성한 정답이 실제 컴파일러(gcc/javac/python3) 결과랑 맞는지 검증. 이거 필수.
[Sandboxing & Security]
- ulimit -u 쓰면 안 됨. container uid 1000이 host랑 겹쳐서 fork 에러 남.
- 대신 docker-compose에서 pids_limit: 512, mem_limit: 1500m으로 조짐.
- process group 전체에 SIGKILL 날려서 좀비 방지.
[주의사항] - SQL/Quiz 문제 추가하면 무조건 verify 스크립트 돌릴 것. wtf how can i forget it - ML tab은 이제 pyodide 안 씀. 서버에서 돌림.
여기서 배울 것
- WASM worker랑 server-side runtime은 아예 설계부터 다르게 가져가야 함.
- Docker 환경에서 ulimit -u 쓰면 fork 에러 나니까 cgroup(pids_limit)으로 조질 것.
- 데이터 직접 건드리지 말고 무조건 gen 스크립트 돌려서 생성하기.
원본 파일 보기 (.claude/projects/-home-son-prj-learn-code-test/memory/runtime-tab-pattern.md)
---
name: runtime-tab-pattern
description: "How learn_code_test/fe adds an in-browser code-runtime learning tab (pyodide, sql.js)"
metadata:
node_type: memory
type: project
originSessionId: fb5fbd91-881e-46d4-8dce-766690fc3172
---
`/home/son/prj/learn_code_test/fe` is a Next.js 16 (Turbopack, `basePath: "/code-test"`, `output: standalone`) learning platform. Tabs split into two runtime styles: **(a) in-browser WASM Web Worker** (SQL=sql.js) and **(b) server-side execution via `/api/run`** (Quiz C/Java/Python + the ML tab). **As of 2026-06 the ML tab no longer uses pyodide** — it runs server-side (see [[ml-server-runtime]]); the pyodide worker/client were deleted. All editable runtimes share the same run shortcut **Ctrl/Cmd+'** (ML, SQL, Quiz workspaces each bind it).
To add a new runtime tab, mirror the existing pattern (Python=pyodide, SQL=sql.js):
- `public/<x>-worker.js` — classic worker; message protocol `init→ready`, `run→result|error` (worker URL must prepend `process.env.__NEXT_ROUTER_BASEPATH`).
- `lib/<x>/client.ts` — worker bridge (copy `lib/pyodide/client.ts` / `lib/sql/client.ts`).
- `<x>-problems/<slug>/` folders loaded by `lib/<x>-problems.ts` (meta.json/description.md/starter/solution/hints.json); SQL uses a shared `_shared/seed.sql`. SQL content is **generated** — edit `scripts/sql-data/*.mjs` (select/where/aggregate/advanced/dml/ddl) + `sql-data-seed.mjs`, then `rm -rf sql-problems && node scripts/gen-sql-problems.mjs` (don't hand-edit the generated folders). Dockerfile must `COPY sql-problems` into the runner stage.
SQL specifics (as of 2026-06): **all identifiers are Korean** (사원/부서/이름/급여… — 정처기 style; SQLite accepts unquoted non-ASCII). 61 problems calibrated to 정처기 실기: difficulty is **easy(기본 문법, 메서드당 3문제 변형) or medium(실전)**, never hard — 45 easy + 16 medium. Problem `order` is **shuffled deterministically (FNV-1a hash of slug, easy block then medium block) in `sql-data-problems.mjs`** so methods aren't grouped — the learner infers which SQL to write; modules' own `order` fields are overridden there. The `unit` category label is **hidden in the SQL UI** (`ProblemList showUnit={false}`, SqlWorkspace shows only difficulty) so it doesn't telegraph the method. Hints render ONLY on the hint button (HintPanel `level>0`); never surface method/keyword by default. Titles are method-name-free; starters are comment-only (no answer SQL); inline examples live in `hints.example` (shown on first hint). Do NOT add friendly error tips — user rejected that. **INSERT/UPDATE/DELETE/DDL are graded by state**: a problem may define `verify` (a SELECT run AFTER the mutation — e.g. `SELECT * FROM 사원…`, `pragma_table_info('학생')`, `sqlite_master`); the worker's `runAndCapture` runs userSql then verifySql and compares the resulting state. GRANT/REVOKE/COMMIT/ROLLBACK can't run in SQLite → excluded. Verify offline with `scripts/verify-sql-problems.mjs` (handles verify.sql) and `verify-sql-grading.mjs` (SELECT + state-based grading sims).
- `components/<X>Workspace.tsx` reusing `ResizableLayout`, `Markdown`, `CodeEditor` (has `language` prop), `useHintPanel`.
- Route `app/<x>/page.tsx` + `app/<x>/[slug]/page.tsx`; add entry to `components/TabNav.tsx`.
Verify without a browser: SQL/grading correctness runs on `better-sqlite3` (already a dep, same SQLite engine as sql.js) — see `scripts/verify-sql-problems.mjs` / `verify-sql-grading.mjs`. `npm run build` is the real gate; the existing `*Workspace.tsx` files trip `react-hooks/refs` + `set-state-in-effect` eslint errors but build passes (lint is not a build gate here).
**Quiz tabs (C/Java/Python, added 2026-06)** are a DIFFERENT pattern — output-prediction quizzes, NOT runtimes. 정처기 실기 형식: show code (read-only Monaco via `CodeEditor readOnly`), learner types predicted stdout, grade by string compare (`lib/quiz/check.ts` normalizeOutput) — no browser runtime, the expected answer is precomputed. Content in `scripts/quiz-data/{c,java,python}.mjs` (code via `String.raw` so `\n`/`\0` survive), generated by `gen-quiz-problems.mjs` → `quiz-problems/<lang>/<slug>/`. CRITICAL: every authored `answer` is verified against REAL gcc/javac(17)/python3 output by `scripts/verify-quiz.mjs [c|java|python|all]` — run it after any edit; the machine has all three toolchains. Routes `app/quiz/[lang]/{page,[slug]/page}.tsx`, components `QuizWorkspace`/`QuizSidePanel`/`QuizHint`, loader `lib/quiz-problems.ts`. The quiz code editor is now **editable + runnable**: a server route `app/api/run/route.ts` → `lib/run/exec.ts` (SERVER-ONLY) compiles/runs the submitted code with REAL gcc/javac/python3 (installed in the Dockerfile runner stage via `apk add gcc musl-dev libc-dev openjdk17-jdk python3`). Sandboxing: per-run temp dir, stdin closed, `ulimit -t/-f` + Node-side wall-clock SIGKILL of the process GROUP (spawn detached, `process.kill(-pid)`), output cap, 20KB code cap, in-memory per-IP rate limit (30/min). **Do NOT use `ulimit -u`** — the container `node` uid (1000) shares the kernel nproc count with the host's uid 1000, so it instantly fails gcc/javac forks; fork-bomb/OOM protection is instead `pids_limit: 512` + `mem_limit: 1500m` in docker-compose.yml (cgroup, uid-independent). Residual risk: no per-process network isolation (acknowledged). `lib/run/client.ts` calls it; `lib/quiz/check.ts` still grades the prediction quiz separately. Reuses `ProblemList` (map QuizMeta→{...m,unit:topic,tags:[]}, showUnit=false), shuffle + easy/medium + button-only hints like SQL. Dockerfile must `COPY quiz-problems`. 171 problems (54 C / 54 Java / 63 Python), split across `quiz-data/{c,c2,c3,java,java2,java3,python,python2,python3}.mjs`; `gen-quiz-problems.mjs` and `verify-quiz.mjs` both concat the 3 files per lang (verify-quiz MODS lists all 3). When adding quiz problems, ALWAYS run `node scripts/verify-quiz.mjs all` (real gcc/javac/python3) before deploy — a wrong authored answer ships a broken quiz. Each problem also has a **"🤖 클로드 해설"** note (button next to 해설 보기, indigo) = topic-level syntax-tips cheat-sheet from `scripts/quiz-data/tips.mjs` (`TIPS[lang][topic]`, keyed by meta.topic), or a per-problem `note` field override; generated to `note.md`, loaded as `QuizContent.note`, rendered in `QuizSidePanel` when `showNote`. It shows tips/common-mistakes WITHOUT revealing the answer (separate from 해설 보기). Add a new topic's tip to tips.mjs when introducing a new `topic`.
**🤖 클로드 해설 on ML + SQL tabs too**: same indigo button + `ClaudeNote` component, but notes come from **loader-side unit→note maps** (no file regen): `lib/ml-notes.ts` (`mlNote(unit)`, ~28 ML concept tips + ALIAS map for unit-string variants like 평가지표→평가 지표 + GENERIC fallback) and `lib/sql-notes.ts` (`sqlNote(unit)`, ~22 SQL syntax tips). `loadProblem`/`loadSqlProblem` attach `note: mlNote/sqlNote(meta.unit)` to the content type; `ProblemWorkspace` and `SqlWorkspace` (via `SqlToolbar` noteButton prop) render `<ClaudeNote>` on toggle. Notes are concept/mistake tips, shown WITHOUT revealing the answer.
Ports: docker serves on 60024; a local instance already occupies **127.0.0.1:60099** (don't kill it) — use another port for `next start` test probes. File-length limit: 150 lines/source file (see global CLAUDE.md), which is why the toolbar/result table are split out.