10주 개발 클래스
문서 템플릿 & 프롬프트

PRD · Design Doc · UI · ADR · Roadmap — 직접 써보세요

빈 템플릿을 복사해 직접 채워보고, 옆의 프롬프트 예시로 Claude Code에 물어보며 완성하세요. 채워진 예시는 접어뒀어요 — 먼저 해보고, 막히면 펼쳐서 비교하면 됩니다.

⚠️ 문서는 영어로 쓰세요 (Claude Code 성능↑, 한글은 품질↓ — 오늘은 한글로 써도 Claude Code가 영어로 바꿔줌). · roadmap은 매 세션 붙여넣는 기억 앵커.
💡 심화(선택) — PRD의 "Grill me"를 자동화하고 싶으면 grill-me 스킬(Matt Pocock)을 설치하세요. 터미널에 한 줄: npx -y skills add mattpocock/skills --agent claude-code → Claude Code에서 "grill me"라고 하면 decision tree 따라 한 번에 하나씩(추천 답변과 함께) 캐물어줍니다 → /to-prd로 PRD 자동.
설치법 자세히 → 1주차 페이지 ④ grill-me 설치
1주차
기획서 → 설계 → 화면 → 결정 → 로드맵
문서 5종

1PRD — 뭘·왜 만드나

① 빈 템플릿 — 복사해서 채우기
# PRD — <Project Name>

> Write in **English** (better LLM output). Fill each section for your service.
> At W1, keep it to the 5-box essence (§1–§5, §8); the rest grows W2–W5.
> Full filled example: `../filled/PRD-mini-chatbot.md`.
>
> **★ Altitude rule — the one mistake everyone makes.** A PRD says *what* and *why*, never
> *how*. If a sentence names a **function, a file, a framework, or a week of this class**, it
> belongs somewhere else: the screen → UI doc · the architecture → Design Doc · the technology
> choice → ADR · the schedule → roadmap. Test: your PRD should still make sense to someone who
> will never see the code.

**Status:** Draft → In Review → Approved  ·  **Owner:** <you> · **Reviewers:** <mentor> · **Last updated:** <YYYY-MM-DD>
<!-- Status lifecycle is the ATI Labs team standard (whoever owns it moves it along). At W1 you'll only ever be in Draft. -->


---

## 1. Summary
<2–3 sentences: what it is and why, in plain language.>

## 2. Problem & Motivation
<Whose pain, and what does it cost them today? Why now?>   ← the "왜" (5-box #1)

## 3. Users & Use Cases
<Who uses it? One primary user + the core use case. Also: who is NOT your user yet.>   ← "누구" (5-box #2)

## 4. Goals & Success Metrics
- **G1 — <the MVP outcome, exactly one>.**   ← "MVP 딱 하나" (5-box #3)
  <Write it as an outcome for the user ("a user gets X without Y"), not as a feature
  ("a button appears"). If it reads like a to-do, it is at the wrong altitude.>
- **Guardrail (counter-metric):** <what must NOT get worse — the thing you'd be tempted to
  sacrifice to hit G1>.

## 5. Non-Goals (defer) ★ most important
<What you will NOT build now, and which phase it moves to. This is what keeps the MVP
shippable. Defer by phase (what it depends on), not by whim.>

| Deferred | Phase | Why not now |
|---|---|---|
| <...> | <...> | <...> |

## 6. Functional Requirements (MoSCoW)
- **Must:** <...>   · **Should:** <...>   · **Could:** <...>   · **Won't (now):** <see §5>
- **Acceptance criteria:** <observable: when the user does X, Y appears>.
- <Hand off the "how": screen details → UI doc · architecture → Design Doc · a technology
  choice → ADR. Link them here instead of specifying them.>

## 7. UX & Edge Cases (product-level only)
- **Happy path:** <...>   · **Empty state:** <...>   · **Empty/invalid input:** <...>

## 8. Success Criteria & Verification ★
<How do you *observe* it's done? A concrete check someone else could run, not "should work".>   ← "성공 기준" (5-box #4)

## 9. Open Questions & Risks
<Unknowns and where you'll decide them. Include at least one real product risk — not just
a scheduling question.>
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 정리 시키기
"내 서비스 아이디어야: [한 줄]. PRD 5요소(문제·유저·MVP·성공·Non-Goals)로 정리하고, 빠지거나 애매한 건 나한테 먼저 물어봐."
🎯 Grill me (전문가처럼 캐묻게)
"staff engineer·solution architect처럼 날 몰아붙여줘 — 빠진 것·약점·엣지케이스를 캐물어."
⭐ 좁히기 (Non-Goals)
"지금 꼭 필요한 MVP 하나만 남기고 나머지는 Non-Goals로 옮겨줘."
⭐ AI 말을 그대로 받지 말 것 — 본인 수준에 맞게 이해하고, 판단은 내가.
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# PRD — Mini Chatbot

> **Sample PRD for the 10-week dev class.** Written in **English on purpose** — Claude Code
> (and every coding LLM) performs measurably better with English specs; Korean prompts
> degrade output quality. Write the *doc* in English even if you think in Korean.
>
> **Altitude rule (read this first):** a PRD says *what* and *why*. If a sentence names a
> function, a file, a framework, or a week of this class, it belongs in the Design Doc, the
> UI mockup, an ADR, or the roadmap — not here. This PRD should still make sense to someone
> who will never see the code. Notice how §6/§7 below hand off instead of specifying.
>
> **Format = synthesis of three sources:** big-tech PRD standard · ATI Labs house format
> (`passenger-flow-gen/docs/prd.md`) · the Meta-engineer PRD prompt (guardrail metrics,
> acceptance criteria, MoSCoW). The **W1 5-box PRD is the seed of §1–§5/§8 below**; the rest
> (§6 functional, §7 UX) grows over W2–W5.

**Status:** Draft · **Owner:** <you> · **Reviewers:** <mentor / senior> · **Last updated:** 2026-07-16

---

## 1. Summary
An assistant that answers a question in the place where the question came up, instead of
sending the person out to a search engine. MVP: one question in, one answer back.
(This is the class's reference app — grown week by week, W1 mockup → real model → deploy.)

## 2. Problem & Motivation
When a quick question comes up, people stop what they are doing, open a browser, search, skim
a page of blue links, and come back. The expensive part is not the seconds spent searching —
it is the broken context, and the fact that search returns **documents** when the person
wanted an **answer**. **Why now:** language models made a usable answer cheap. The missing
piece is no longer the answer; it is a thin, trustworthy surface to ask on.

## 3. Users & Use Cases
- **Primary user:** someone in the middle of a task with a one-off question, who will not
  invest a single minute in learning a tool.
- **Core use case:** ask in plain language → read the answer → get back to work.
- **Not our user (for now):** the researcher who needs sources, depth, and a trail (→ §5).

## 4. Goals & Success Metrics
- **G1 (the one MVP outcome) — a user gets a usable answer to a one-off question without
  leaving what they were doing.** One question, one answer, no detour.
- **Guardrail (counter-metric):** never worse than the search box we are replacing —
  (a) no dead end: every question gets *something* back, and (b) no confidently wrong answer
  that sends the user down the wrong path. In W1 the stubbed reply makes this trivially true;
  at W6, when a real model answers, this is the metric that bites.

## 5. Non-Goals (defer) ★ the most important section
Deciding what **not** to build is what makes the MVP shippable. Defer by **phase** — the class
week is just when we happen to get there.

| Deferred | Phase | Why not now |
|---|---|---|
| Accounts / login | Phase 2 (W2) | Nobody needs an identity to ask one question. |
| Remembering the conversation | Phase 2 (W2–4) | Single turn is enough to prove G1. |
| A real model behind the answer | Phase 3 (W6) | Today's answer is a stub — deliberately. |
| Voice, images, streaming, multi-turn memory | Later | Not on the path to G1. |
| Scale, caching, cost | Phase 3 (W3–5) | No users yet — optimizing now is guessing. |

## 6. Functional Requirements (MoSCoW)
- **Must:** the user can ask a question and see an answer appear, in the same place, without
  a reload. · **Should:** an empty question does nothing (no dead bubble). · **Could:** the
  wait *feels* like an answer being written. · **Won't (now):** everything in §5.
- **Acceptance criteria:** given an empty screen, when the user submits a question, an answer
  is visible in the same view within ~1 second, and the question is still visible next to it.

> *How* it renders (bubbles, alignment, auto-scroll, enter-to-send) is a screen decision →
> `UI-mini-chatbot.md`. *How* the answer is produced (one swappable seam) is an architecture
> decision → `design-doc-mini-chatbot.md` and `ADR-001-reply-seam.md`. Neither belongs here.

## 7. UX & Edge Cases (product-level only)
- **Happy path:** ask → answer, in place.
- **Empty state:** before the first question, the screen tells the user what it is for.
- **Empty / invalid input:** nothing happens; the user is never punished for a stray Enter.
- **Later (Phase 2+):** the answer fails to arrive; the question is too long; the user is
  not allowed to ask.

## 8. Success Criteria & Verification (the acceptance gate) ★ class pillar ③
**Observable, not asserted:** put the screen in front of **someone who is not you**, say
nothing, and watch them type a question and get an answer on the first try. That is "done"
for the MVP. (Not: "the send handler should work.") Everything in §4 G1 is visible in that
one moment — which is exactly why G1 was written as an outcome and not as a feature list.

## 9. Open Questions & Risks
- What backs the answer when the stub goes away — hosted model or our own? *(Decide at the
  Phase 3 boundary; it gets its own ADR — that is where a technology choice belongs.)*
- Does the guardrail (b) — "no confidently wrong answer" — need a human check, or can the
  product refuse to answer when unsure? **This is the real product risk.** Revisit before
  a real model ships.

2Design Doc (TRD) — 어떻게 만드나 · = RFC · Design Proposal

① 빈 템플릿 — 복사해서 채우기
# Design Doc (TRD) — <Project Name>

> The "how" doc. Same thing as **RFC · Design Proposal · TRD** — pick your team's word.
> Write in **English**. Decisions you make here become **ADRs**.
> **At W1, fill §1–§3 only** (context · scope · one diagram with the seam). §4–§9 grow W2–W5 —
> the filled example looks complete because it is the *finished* shape, not day-one homework.
> Full filled example: `../filled/design-doc-mini-chatbot.md`.

**Status:** Draft → In Review → Approved → Implemented  ·  **Author (owner):** <you> · **Reviewers:** <senior/staff> ·
**Related:** <PRD link> · <ADR links> · **Last updated:** <YYYY-MM-DD>
<!-- Status lifecycle = ATI Labs team working agreement (owner drives it Draft→…→Implemented). At W1 you'll only be in Draft. -->


---

> **Every `##` section below opens with a collapsed plain-words summary.** Keep them — a reader
> grasps a section without reading it, and skips it outright when they already know the material.
> Write what the section *decides and why*, not what it contains. Three short paragraphs max.
> `>` on every line including blank ones, or the callout splits into two boxes.
> Delete this note; keep the `<details>` blocks.

---

## 0. Baseline — what already exists (not under review)

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What already works today, and what this design assumes from it. State plainly that none of it
> needs approval — it is here so later sections can say "we reuse this" in one line.>

</details>

<Table: capability | where it lives | what this design assumes from it. Delete this section only
if the work is genuinely greenfield. Re-verify it against `origin/main` immediately before a
review — a draft date is not evidence, and this section goes stale within days.>

## 1. Context & Summary

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <The problem in plain language, and the one-sentence version of what is missing.>

</details>

<Which PRD does this implement? One-paragraph shape of the solution + the key seam.>

## 2. Goals & Non-Goals

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What is in scope and — more importantly — what is deliberately out. Non-Goals are the highest-signal part of this doc and the first thing juniors drop.>

</details>

<Scope boundary for the design (distinct from the PRD's product scope).>

## 3. Architecture

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <The shape of the solution in one breath, and where the seam is — the one place you can swap later without touching the rest.>

</details>

<Components + data flow. A diagram beats prose:>

```mermaid
flowchart LR
  User --> UI --> Seam[the seam function] --> Backend --> Store[(DB)]
```

<Call out the **seam**: the one place you'll swap later without touching the rest.>

## 4. Data & API Design

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What gets stored and what the API exposes. Say why this shape rather than the obvious simpler one.>

</details>

- **Data model:** <tables / schema, or "none yet">.
- **API:** <`METHOD /path { in } → { out }`, status codes>.

## 5. Non-Functional Requirements

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <Speed targets, permissions, and what the user sees when each dependency breaks. The failure table is the part reviewers actually use.>

</details>

- **Security:** <auth, no secrets in client, ...>
- **Rate limiting / cost:** <per-user limit>
- **Performance:** <target latency; measure first>
- **Observability:** <logs / metrics>
- **Scale:** <stateless? replicas?>
- **Failure mode:** <what degrades when a dependency is down — the fallback / graceful-degrade path, not just "it errors">.

## 6. Alternatives Considered

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What you did not choose and why. Each rejected option needs a reopen condition, or it will be re-argued every review.>

</details>

<Option A vs B vs C — and *why* you chose one. These feed your ADRs.>

## 7. Risks & Dependencies

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What could go wrong, grouped by who can fix it — external blockers vs our own code vs operations.>

</details>

<What could bite; what you depend on.>

## 8. Verification

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <How anyone else can tell this works — as checks they can run, not as a claim you make.>
>
> <The distinction this section exists to hold: an endpoint returning `200` is not the same as
> working. The user-visible result, the durable state, and the failure behaviour all have to match.>

</details>

<Planned behaviour after implementation, not behaviour already shipped. §9's go-live bar cites the
checks below by number, so write them before you write the bar.>

### 8.1 What "working" means

<One row per situation a reviewer would ask about — the happy path, the duplicate, the out-of-order
arrival, the message that changes nothing, the dependency that dies mid-request.>

| Situation | What the user should see | What must be verified |
|---|---|---|
| <a normal update arrives> | <what changes on screen, without a refresh> | <the durable effect + the latency budget + what stays traceable> |
| <the same input arrives twice> | <one visible change — no duplicate rows, no second effect> | <reprocessing yields one durable effect and the same final state> |
| <a dependency is down> | <last-good view with an honest stale/degraded label; unsafe actions disabled, not silently failing> | <the §5 failure mode actually happens under fault injection> |

**Never let "absent" and "not applicable" render the same way.** If a value can be missing for two
different reasons, the screen has to say which — otherwise a whole class of failures under-reports
itself forever and nobody notices. Write that as a check, not as a hope.

### 8.2 Checks

<Numbered, each runnable by hand — a command or a click, with a visible pass/fail. "The code looks
right" is not a check. Keep them in the order a reviewer would run them.>

1. <command or click → observable result>
2. <...>

### 8.3 Test environment / stand-in

<Delete if you can test against the real thing. Otherwise: how do you exercise this before the real
dependency exists?>

⚠️ **A stand-in that does not run the production code path certifies a code path that will not
ship.** Fixture files, mocked clients and hand-written stubs usually skip the very steps that break
in production. Build the *other side of the boundary* instead — real schema, real rows, our code
reading it unmodified — so that **the only difference from production is who wrote the data**.

- **What it covers:** <the cases only this reaches — and, honestly, the ones it still cannot>
- **Fixture:** <one real, representative record traced end to end; say why it is representative>

### 8.4 Invariants

<Properties that must hold on every run, not one happy path. The usual load-bearing set:>

- <Replaying the same input twice yields the same final state.>
- <A duplicate has exactly one effect.>
- <An input that changes nothing produces no event, no write, and no re-render — and a *real* change
  immediately after one is still delivered.>
- <A late/older input is retained but cannot roll state backward.>
- <Crash injected at each commit point recovers with no loss and no double application.>

### 8.5 Bars

| Bar | Passing means |
|---|---|
| <name> | <the observable condition — a number, a diff, a screen state> |

<Report the measurement even when it misses the budget. A bar you only quote when it passes is not a
bar.>

## 9. Rollout / Phasing

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <Two clocks, kept separate: how long the build takes, and how long the evidence takes before anyone trusts it. Compressing a schedule compresses implementation, never proof.>

</details>

<Week-by-week: what lands when. shadow → advisory → live, if applicable.>
- **Go-live bar:** <the measurable gate each stage must pass before it advances — cite §8 by number,
  e.g. "advisory only after checks 1–10 pass on N consecutive service days". A stage without a bar is
  a hope, not a rollout; a bar that does not name its evidence is a slogan.>
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 뼈대 잡기
"이 PRD 기반으로 Design Doc(TRD) 뼈대 잡아줘 — 아키텍처·스택 / 데이터·API / 나중에 갈아끼울 the seam."
🎯 대안 비교
"선택지가 있으면 A vs B 비교하고, 왜 이걸 골라야 하는지 트레이드오프를 설명해줘."
⭐ 오늘은 뼈대만 — 깊이는 W3~4. (RFC = Design Doc = Design Proposal = TRD, 같은 문서)
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# Design Doc (TRD) — Mini Chatbot

> **Sample Design Doc / RFC / TRD for the 10-week dev class.** Same document, different names
> across companies: **Design Doc** (Google), **RFC** (Request for Comments), **Design Proposal**
> (ATI Labs), **TRD** (Technical Requirements Document, Meta). It answers **"how"** (the PRD
> answers "what/why"); decisions extracted from it become **ADRs**.
>
> **Write in English** (better LLM output). **Format = synthesis:** big-tech Design Doc standard ·
> ATI Labs house format (`passenger-flow-gen/docs/facility-config-ui-design.md`) · the Meta-engineer
> TRD prompt (Mermaid architecture, NFR quality specs, deployment strategy).

**Status:** Draft · **Author (owner):** <you> · **Reviewers:** <senior/staff> ·
**Related:** `PRD-mini-chatbot.md` · `ADR-001-reply-seam.md` · **Last updated:** 2026-08-06

---

## 1. Context & Summary
Implements `PRD-mini-chatbot.md`. A single HTML page renders a chat log and a form; a **single
function `generateReply(message)`** produces the answer. Today it returns a stub string; the
whole point of isolating it is that later weeks swap the stub for a real LLM call **without
touching the UI**. Keep the core simple; absorb change behind one seam.

## 2. Goals & Non-Goals
- **Goals:** working single-turn UI; one clearly-named seam for the "answer"; zero build tooling (plain HTML/JS) at W1.
- **Non-Goals:** framework/bundler (W1), backend (until W2), streaming (later). See PRD §5.

## 3. Architecture
Client-only at W1; a backend seam appears at W2.

```mermaid
flowchart LR
  U[User] -->|type + send| F["Chat UI (index.html)"]
  F -->|message| G["generateReply(message) ← the seam"]
  G -->|W1: stub string| F
  G -.->|W6: HTTP| API["backend · POST /chat"]
  API -.-> LLM[("LLM serving · vLLM")]
```

- **`generateReply()` is the seam (W3 concept).** W6 changes only this function to call the
  backend; W5 adds a cache here; the UI never changes.
- **Components:** `index.html` (markup + styles + the log/form), one `<script>` (append bubbles,
  call the seam). No global state beyond the DOM at W1.

### 3.1 Production target (where this is headed) ★

Don't build this today — **build the 🟢 slice and leave the seam where production attaches.**
This picture is the *reason* W1 stays thin: every later concern lands at a **known joint**, so no
week forces a UI rewrite. The rollout in §9 walks this diagram left-to-right, one PR per week.

```mermaid
flowchart TB
  subgraph TODAY["🟢 W1 — what you actually build today"]
    direction LR
    UI["Chat UI (index.html)"] --> SEAM["generateReply(message)<br/>stub string · the seam"]
  end

  subgraph TARGET["Production target — same UI + same seam, internals grow W2 → W9"]
    direction TB
    LB["Load balancer"] --> APP
    subgraph APP["Stateless app · N replicas — W3 / W4"]
      AUTH["Auth guard — W2"] --> LIM["Rate limit — W2 / W9"] --> S2["generateReply()<br/>HTTP call — W6"]
    end
    S2 --> CACHE["Reply cache — W5"] --> LLM["LLM serving · vLLM — W6"]
    APP --> DB[("messages / users — W2 ~ W4")]
    APP --> OBS["Metrics · structured logs — W2 / W9"]
  end

  UI -. "same front-end, unchanged" .-> LB
```

- **The seam is the only joint that moves.** Today it returns a stub; W6 makes it an HTTP call;
  W5 slots a cache in front of it. The UI never changes — that is the whole payoff of ADR-001.
- **Every production concern has a home already:** auth + rate-limit wrap the request path (§5),
  the app is designed **stateless** so W3 can run N replicas behind the load balancer (§5 Scale),
  persistence and metrics attach at W2–W4/W9. You are pre-structuring, not pre-building.
- **What "production" adds beyond W1** = the boxes outside the 🟢 slice. Read §5 (NFRs) for *what*
  each is and §9 (rollout) for *when* it lands. If a box isn't needed for your scale yet, it stays
  on the diagram as a labeled target — **scale-correct now, tune later** (don't build it early).

## 4. Data & API Design
- **W1:** no data model; messages live in the DOM only.
- **W2+ (planned):** `POST /chat { message } → { reply }` (REST, JSON, status codes 200/400/401/429).
- **W2+ persistence:** `messages(id, user_id, role, text, created_at)` — added with auth, not before.

## 5. Non-Functional Requirements (the "quality" specs) — grown over the class
- **Security (W2):** auth guard on `/chat`; never ship an API key in client JS (see the disaster cases — Cara/Lovable). Secrets server-side only.
- **Rate limiting (W2/W9):** per-user key + limit so one caller can't drain the token budget.
- **Performance (W5):** target p95 reply latency; measure before optimizing.
- **Observability (W2/W9):** structured request logs; GPU/latency metrics once a model is live.
- **Scale (W3):** stateless request path so we can run N replicas behind a load balancer.

## 6. Alternatives Considered
- **Inline the answer logic in the submit handler** — rejected: no seam, so W6 would rewrite the UI.
- **Start with a framework (React) at W1** — rejected: build tooling overhead buys nothing for a one-page MVP; add when state/reuse justify it.
- **Multi-turn from day 1** — rejected: violates PRD Non-Goals; single-turn first.

## 7. Risks & Dependencies
- **Risk:** the stub hides real latency/error behavior → surfaces at W6. Mitigate by keeping the seam's signature `(message) → replyString` stable.
- **Dependency:** W6 needs a serving endpoint (hosted API or on-prem vLLM) — decided in a W6 ADR.

## 8. Verification

How someone else can tell this works — as checks they can run, not as a claim you make.
**An endpoint returning `200` is not the same as working**, and here the same trap wears a
different hat: *a stub that always answers instantly and never throws proves nothing about the
UI you will actually ship.* See 8.3.

### 8.1 What "working" means

| Situation | What the user should see | What must be verified |
|---|---|---|
| Types a message and sends | Their bubble appears, then the reply bubble; the input clears and refocuses | The seam is called exactly once with the typed text; the log order is send-then-reply |
| Sends empty or whitespace-only input | Nothing is sent, nothing appears, no error | The submit handler returns early — the seam is never called |
| The reply is slow (W6) | A pending indicator; the send button is disabled while in flight | No second request can be started; the pending state clears on both success **and** failure |
| The reply fails (W6) | An error bubble that says something went wrong; the earlier log is still there and the UI still usable | The failure is caught at the seam; the UI does not blank, freeze, or lose history |
| A reply contains markup | It renders **as text** — `<script>alert(1)</script>` shows those characters, no popup | Insertion is `textContent`, never `innerHTML` (this is the W2 XSS lesson, checked from W1) |

**"No reply yet" and "the reply failed" must not look the same.** If both render as an empty or
frozen bubble, every backend failure at W6 reads as slowness and you will debug the wrong thing.

### 8.2 Checks

Each is a click or a command with a visible pass/fail. *"The code looks right" is not a check.*

1. Type `hello` → Enter. Two bubbles appear, in order, and the input is empty and focused.
2. Press Enter on an empty box, then on spaces only → nothing happens, no console error.
3. Paste `<script>alert(1)</script>` as the message → it shows as those characters. **No alert box.**
4. Temporarily make the stub wait 3 s (8.3) → the send button is disabled for those 3 s, and
   hammering Enter produces exactly one reply.
5. Temporarily make the stub throw (8.3) → an error bubble appears, the earlier bubbles are still
   there, and you can send a normal message afterwards.
6. **The seam check (this is the one that matters).** Swap the stub for the real HTTP call, then
   `git diff --stat index.html` → **0 changed lines** if the markup and script are separate files,
   or a diff touching only `generateReply()` if they are not. A UI diff here means ADR-001 did not
   hold and W6 just cost you a rewrite.
7. From W2 on: open devtools → Sources and search the client bundle for the API key → **no hits**.

### 8.3 Stand-in — make the stub lie less

The stub *is* our stand-in for the LLM, and by default it is the dishonest kind: instant, always
successful, always well-formed. A UI verified only against that has **never seen latency, an error,
or a slow user double-clicking** — exactly the risk §7 says surfaces at W6.

So give the stub the three behaviours the real thing has, behind a flag you can flip by hand:

| Mode | Stub does | Catches |
|---|---|---|
| `normal` | returns the canned string | the happy path |
| `slow` | waits ~3 s first | missing pending state, double-send, disabled-button bugs |
| `fail` | throws | missing error handling, a UI that blanks or freezes on failure |

Keep the signature `(message) → replyString` (a Promise from W6 on) identical in all three — the
point is that the UI cannot tell which mode it is in. That is what makes it a stand-in rather than
a test double the real code path will never resemble.

### 8.4 Invariants

- The seam's signature stays `(message) → replyString`; the UI reads **only** its return value.
- The seam is called exactly once per send — never zero times, never twice.
- Bubbles never appear out of order, even if two sends overlap.
- No secret ever reaches client-side code (W2 on).
- Swapping what is *inside* the seam changes no UI code.

### 8.5 Bars

| Bar | Passing means |
|---|---|
| **W1 done** | Checks 1–3 pass, and 4–5 pass with the stub in `slow` and `fail` mode |
| **Seam held (W6)** | Check 6 shows no UI diff when the stub becomes a real HTTP call |
| **Secrets clean (W2)** | Check 7 finds nothing, on every build that ships |
| **Latency (W5)** | p95 measured under `slow`-mode-like conditions and **written down even if it misses target** — a number you only quote when it flatters you is not a bar |

## 9. Rollout / Phasing
W1 stub UI → W2 auth + backend seam → W3 stateless/scale → W5 perf/cache → **W6 real LLM** →
W10 deploy. Each week is one PR against a living version of this doc.
- **Go-live bar:** the §8.5 bar for that week passes. W6 does not start until check 6 is green on
  the stub swap — a rollout stage without its bar is a hope, not a rollout.

3UI — 화면을 글로 묘사

① 빈 템플릿 — 복사해서 채우기
# UI — <Project Name> (text mockup)

> **Describe your screen(s) to Claude Code in words** — no design tool needed. Fill this in and
> paste it together with your PRD + Design Doc. Write in **English** for best results.
>
> **Exception — UI copy.** The *doc* is English; the *words your users actually read* (button
> labels, hints, error messages) stay in the product's own language. Quote them verbatim
> (`"보내기"`, not `"Send"`) — otherwise Claude Code ships an English UI to Korean users.
> Full example: `../filled/UI-mini-chatbot.md`.

## Screen: <name>
<Overall layout in one line: what sits where — e.g. "a centered card, header on top, list in the
middle, input bar at the bottom".>

- **<region 1>** (position): <what it contains>
- **<region 2>** (position): <what it contains>
- **<region 3>** (position): <what it contains>
- **Empty / loading / error state**: <what shows when there's nothing / it's loading / it fails>

## Behavior
- <user action, e.g. click / type / submit> → <what happens>
- <edge case, e.g. empty input, long text> → <what happens>

## Style
- Font · accent color · spacing feel (minimal? dense?). Keep it small for v0 — one screen.

## Prompt to give Claude Code
> "Build this as <a single HTML file / a React page>. [paste the sections above]. <note any stub /
> seam, e.g. 'the data comes from one function getX() — return sample data for now'>. Match the style."

*(Tip: ask for 2–3 variations and pick the best; or add "make it look like <a reference site>".)*
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 화면 만들기
"이런 화면이야: [헤더·목록·입력바 등 글로 묘사]. 자체 완결 HTML(index.html)로 만들어줘."
🔀 여러 개 뽑기
"버전 2~3개 만들어줘 — 제일 좋은 걸 고를게."
🔗 레퍼런스
"○○사이트처럼 만들어줘." (참고 화면을 같이 주기)
⭐ 완벽한 디자인 말고 — 텍스트로 그려도 화면이 나옴. 디자인 툴(Stitch·v0)은 선택.
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# UI — Mini Chatbot (text mockup)

> **Describe the screen to Claude Code — in words.** No design tool needed. This is the "UI mockup"
> you feed alongside the PRD + Design Doc. (Write in **English** for best results.)
>
> **Two different languages, on purpose.** The *doc* is English (better LLM output); the *UI copy
> your users read* stays in the product's own language. So the button below is **"보내기"** and the
> empty-state hint is Korean — quote those strings verbatim, don't translate them into the doc's
> language, or Claude Code will ship an English UI to Korean users.

## Screen: chat
One centered card (~520px wide, ~680px tall) on a light-gray page background.

- **Header** (top of card): app name **"Mini Chatbot"** in bold + one line of small gray subtext
  ("ask me anything").
- **Message log** (fills the middle, scrolls vertically):
  - **User messages** — right-aligned, purple bubble, white text, rounded (bottom-right corner squared).
  - **Bot messages** — left-aligned, light-gray bubble, dark text (bottom-left corner squared).
  - **Empty state** (before the first message): a small centered hint, "아무거나 물어보세요 👇".
- **Input bar** (fixed at the bottom): a text input that grows to fill + a **"보내기"** button (purple).

## Behavior
- Press Enter or click **보내기** → append the user's bubble, then after ~0.4s append the bot reply.
- **Empty input** → do nothing (no empty bubble).
- Auto-scroll to the newest message.

## Style
- System sans-serif. Accent **purple `#6C5CF0`**. Card: white, 16px radius, soft shadow.
- Minimal — one screen, no nav, no settings, no avatars.

## Prompt to give Claude Code
> "Build this as a **single self-contained `index.html`** (inline CSS + JS, no libraries). [paste the
> sections above]. The reply is produced by one function **`generateReply(message)`** — return a stub
> string for now (real LLM comes later). Match the style notes."

*(Tip: ask for 2–3 variations and pick the best. Or add "make it look like <a site you like>".)*

4ADR — 왜 이 결정을 했나 (불변 기록)

① 빈 템플릿 — 복사해서 채우기
# ADR-<NNN> — <short decision title>

> One decision: what you chose, why, what you rejected. Short and **immutable** — a later
> change gets a *new* ADR that supersedes this one (don't edit history). Write in **English**.
> Full filled example: `../filled/ADR-001-reply-seam.md`.

- **Status:** proposed | accepted | superseded-by-ADR-NNN _(YYYY-MM-DD)_
- **Date:** <YYYY-MM-DD>
- **Deciders:** <you / reviewers>
- **Context source:** <PRD / Design Doc links>
- **Related:** <other ADRs>

## Context
<The forces at play. What makes this a real decision (a fork with trade-offs)?>

## Decision
<What you chose, stated plainly. If it's a rule/gate, write the rule up front.>

## Options considered
- **<Option A — chosen>** — <why it wins>.
- **<Option B>** — rejected: <why>.
- **<Option C>** — rejected: <why (e.g. over-engineered / YAGNI)>.

## Consequences
- **Positive:** <what gets easier>.
- **Negative:** <what you accept / how you mitigate>.
- **Reversible?** <blast radius if you change your mind>.

## Verdict
_(Optional — for measured decisions. Fill after you test: PASS/FAIL + one line.
Commit the rule before measuring so you can't rationalize the result afterward.)_
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 결정 남기기
"이 결정(A vs B)을 ADR로 남겨줘 — 고른 것·이유·버린 대안·결과. 짧게."
⭐ 결정은 불변 — 바뀌면 새 ADR로 supersede (기존 걸 고치지 않음).
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# ADR-001 — Reply generation: isolate behind one `generateReply()` seam

> **Sample ADR (Architecture Decision Record) for the 10-week dev class.** An ADR captures
> **one decision**: what was chosen, why, and what was rejected — short and **immutable**
> (a later change supersedes it with a new ADR; you don't edit history). **Write in English.**
> **Format = ATI Labs house ADR** (`onprem-llm-wiki/decisions/ADR-001-serving-engine.md`),
> which is the big-tech ADR standard (Nygard) plus a **Verdict** section for measured decisions.

- **Status:** accepted _(2026-07-16)_
- **Date:** 2026-07-16
- **Deciders:** <you> (owner)
- **Context source:** `PRD-mini-chatbot.md` · `design-doc-mini-chatbot.md` §3
- **Related:** a future **ADR-002** (W6) will decide *which* LLM backs the seam (hosted vs on-prem).

## Context
The mini chatbot must produce a reply for each message. At W1 there is no real model — the reply
is a stub. But we know a real LLM arrives at W6, caching at W5, and possibly a backend at W2.
Where the "make an answer" logic lives now determines how much we rewrite later. Two forces:
**(1) ship a visible MVP today** (a stub is fine), and **(2) don't repaint the UI every time the
answer source changes.** A beginner working with AI can build either shape; the decision is which
one to *ask the AI to build*.

## Decision
**All "produce an answer" logic lives in one function, `generateReply(message) → string`,** called
by the submit handler. W1 returns a stub. Later weeks change **only this function**: W6 makes it an
HTTP call to the backend; W5 adds a cache in front of it. The UI (append user bubble, append bot
bubble) never changes. This is the "seam" concept the class teaches at W3.

## Options considered
- **Seam function (chosen)** — one named function isolates the volatile part. _Cost:_ one tiny
  indirection now. _Benefit:_ W6 is a 5-line change, not a UI rewrite.
- **Inline in the submit handler** — rejected: fastest to type, but W6 forces editing the UI code
  and its tests; the volatile and stable parts get tangled.
- **Premature abstraction (provider interface, DI container)** — rejected: over-engineered for one
  call site (YAGNI). A single function is the right altitude; promote to an interface only if a
  second answer source actually appears.

## Consequences
- **Positive:** swapping stub → real LLM (W6) touches one function; UI, tests, and styling are
  untouched. Caching (W5) and backend routing (W2) also attach at the same seam.
- **Negative:** the stub hides real latency/error/streaming behavior, which only surfaces at W6.
  Mitigation: keep the seam's signature `(message) → replyString` stable, and add async handling
  when the real call lands (revisit in the W6 ADR).
- **Reversible:** the seam is a local refactor; if a different shape is needed later, supersede
  this ADR — no wide blast radius.

## Verdict
_(Fill after the W6 swap: did changing only `generateReply()` deliver a working real-LLM reply
without UI edits? PASS/FAIL + one line. This mirrors the house style — commit the rule up front,
record the measured outcome later, no post-hoc rationalization.)_

5Roadmap — 지금 어디까지 왔나 (세션 간 기억 앵커)

① 빈 템플릿 — 복사해서 채우기
# Roadmap — <Project Name> (living doc)

> **Cross-session memory anchor.** Paste this at the start of each Claude Code session so the AI
> knows where the project is. Keep it at your repo root; update it every session. Write in **English**.
> Full filled example: `../filled/roadmap-mini-chatbot.md`.

## Status board (update every session)
- **Current week:** W<N> · **Repo:** `<name>/`
- **Last done:** <one line>
- **Next step:** <one line>
- **Blockers:** <none / ...>

## 10-week plan (goal per week · status)
| Week | Goal | Status |
|---|---|---|
| W1 | <...> | ⬜ |
| W2 | <...> | ⬜ |
| … | | |
| W10 | Present · deploy · run | ⬜ |

## Documents (this project)
- PRD → `docs/prd.md`  · Design Doc → `docs/design-doc.md`  · Decisions → `docs/decisions/`

## Decisions log (one line each → link the ADR)
- **ADR-001** — <decision> → <consequence>

## Open questions
- <...>

## Session log (append, newest last)
- **<YYYY-MM-DD>** — <what you did this session>
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 로드맵 만들기
"이 프로젝트 10주 roadmap.md 만들어줘 — 상태보드 · 주별 목표 · 결정 로그 · 세션 로그."
🔁 매 세션 갱신
"매 세션 시작 때 이거 붙여넣을게 — 오늘 한 것 반영해서 최신 상태로 갱신해줘."
⭐ AI에게 '지금 어디까지 왔는지' 알려주는 기억 앵커 — 매 세션 붙여넣고 갱신.
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# Roadmap — Mini Chatbot (living doc)

> **What this file is.** Claude Code resets its memory every session. This `roadmap.md` is the
> **cross-session memory anchor** — paste it (or have it read) at the start of each session so the
> AI knows *where the project is* before you ask for anything. Keep it at your repo root and update
> it every session: what's done, what's next, one decision line. **Write in English.**
>
> Maintaining this file *is itself* the skill the class teaches: giving an LLM durable context.

## Status board (update every session)
- **Current week:** W1 · **Repo:** `mini-chatbot/`
- **Last done:** W1 stub UI renders; send → user bubble → stub bot reply (verified in browser).
- **Next step:** W2 — add a backend seam + auth guard on `/chat`.
- **Blockers:** none.

## 10-week plan (goal per week · status)
| Week | Goal | Status |
|---|---|---|
| **W1** | Whole loop + PRD; stub chat UI | ✅ done |
| W2 | Auth · secrets · logging (guard `/chat`) | ⬜ next |
| W3 | Scale · the seam · stateless request path | ⬜ |
| W4 | Architecture · data model (messages table) | ⬜ |
| W5 | Performance · measure p95 · cache at the seam | ⬜ |
| W6 | Real LLM behind `generateReply()` | ⬜ |
| W7 | Faster (streaming / batching) | ⬜ |
| W8 | Cheaper · bigger | ⬜ |
| W9 | Ops · rate limit · monitoring | ⬜ |
| W10 | Present · deploy · run | ⬜ |

## Documents (this project)
- PRD → `docs/prd.md` (what/why) · seeded from the W1 5-box.
- Design Doc → `docs/design-doc.md` (how) · living, grows each week.
- Decisions → `docs/decisions/ADR-001-reply-seam.md` (immutable).

## Decisions log (one line each → link the ADR)
- **ADR-001** (W1) — reply logic isolated behind `generateReply()` seam. → swap at W6.

## Open questions
- W6: which model backs the seam — hosted API vs on-prem? (decide with a W6 ADR)
- W2: history before or after auth?

## Session log (append, newest last)
- **2026-07-16** — W1: PRD written, stub UI built with Claude Code, verified reply renders.
2주차
보안 — 로그인(JWT) · 권한 · 비번 해시 · XSS/CSRF · rate limit
문서 2종

1🔒 보안 Design Doc (W2) — 내 앱에 로그인·보안 붙이기 — 영어 원문 그대로 복붙

① 빈 템플릿 — 복사해서 채우기
# Design Doc (TRD) — <Project Name>

> The "how" doc. Same thing as **RFC · Design Proposal · TRD** — pick your team's word.
> Write in **English**. Decisions you make here become **ADRs**.
> **At W1, fill §1–§3 only** (context · scope · one diagram with the seam). §4–§9 grow W2–W5 —
> the filled example looks complete because it is the *finished* shape, not day-one homework.
> Full filled example: `../filled/design-doc-mini-chatbot.md`.

**Status:** Draft → In Review → Approved → Implemented  ·  **Author (owner):** <you> · **Reviewers:** <senior/staff> ·
**Related:** <PRD link> · <ADR links> · **Last updated:** <YYYY-MM-DD>
<!-- Status lifecycle = ATI Labs team working agreement (owner drives it Draft→…→Implemented). At W1 you'll only be in Draft. -->


---

> **Every `##` section below opens with a collapsed plain-words summary.** Keep them — a reader
> grasps a section without reading it, and skips it outright when they already know the material.
> Write what the section *decides and why*, not what it contains. Three short paragraphs max.
> `>` on every line including blank ones, or the callout splits into two boxes.
> Delete this note; keep the `<details>` blocks.

---

## 0. Baseline — what already exists (not under review)

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What already works today, and what this design assumes from it. State plainly that none of it
> needs approval — it is here so later sections can say "we reuse this" in one line.>

</details>

<Table: capability | where it lives | what this design assumes from it. Delete this section only
if the work is genuinely greenfield. Re-verify it against `origin/main` immediately before a
review — a draft date is not evidence, and this section goes stale within days.>

## 1. Context & Summary

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <The problem in plain language, and the one-sentence version of what is missing.>

</details>

<Which PRD does this implement? One-paragraph shape of the solution + the key seam.>

## 2. Goals & Non-Goals

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What is in scope and — more importantly — what is deliberately out. Non-Goals are the highest-signal part of this doc and the first thing juniors drop.>

</details>

<Scope boundary for the design (distinct from the PRD's product scope).>

## 3. Architecture

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <The shape of the solution in one breath, and where the seam is — the one place you can swap later without touching the rest.>

</details>

<Components + data flow. A diagram beats prose:>

```mermaid
flowchart LR
  User --> UI --> Seam[the seam function] --> Backend --> Store[(DB)]
```

<Call out the **seam**: the one place you'll swap later without touching the rest.>

## 4. Data & API Design

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What gets stored and what the API exposes. Say why this shape rather than the obvious simpler one.>

</details>

- **Data model:** <tables / schema, or "none yet">.
- **API:** <`METHOD /path { in } → { out }`, status codes>.

## 5. Non-Functional Requirements

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <Speed targets, permissions, and what the user sees when each dependency breaks. The failure table is the part reviewers actually use.>

</details>

- **Security:** <auth, no secrets in client, ...>
- **Rate limiting / cost:** <per-user limit>
- **Performance:** <target latency; measure first>
- **Observability:** <logs / metrics>
- **Scale:** <stateless? replicas?>
- **Failure mode:** <what degrades when a dependency is down — the fallback / graceful-degrade path, not just "it errors">.

## 6. Alternatives Considered

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What you did not choose and why. Each rejected option needs a reopen condition, or it will be re-argued every review.>

</details>

<Option A vs B vs C — and *why* you chose one. These feed your ADRs.>

## 7. Risks & Dependencies

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <What could go wrong, grouped by who can fix it — external blockers vs our own code vs operations.>

</details>

<What could bite; what you depend on.>

## 8. Verification

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <How anyone else can tell this works — as checks they can run, not as a claim you make.>
>
> <The distinction this section exists to hold: an endpoint returning `200` is not the same as
> working. The user-visible result, the durable state, and the failure behaviour all have to match.>

</details>

<Planned behaviour after implementation, not behaviour already shipped. §9's go-live bar cites the
checks below by number, so write them before you write the bar.>

### 8.1 What "working" means

<One row per situation a reviewer would ask about — the happy path, the duplicate, the out-of-order
arrival, the message that changes nothing, the dependency that dies mid-request.>

| Situation | What the user should see | What must be verified |
|---|---|---|
| <a normal update arrives> | <what changes on screen, without a refresh> | <the durable effect + the latency budget + what stays traceable> |
| <the same input arrives twice> | <one visible change — no duplicate rows, no second effect> | <reprocessing yields one durable effect and the same final state> |
| <a dependency is down> | <last-good view with an honest stale/degraded label; unsafe actions disabled, not silently failing> | <the §5 failure mode actually happens under fault injection> |

**Never let "absent" and "not applicable" render the same way.** If a value can be missing for two
different reasons, the screen has to say which — otherwise a whole class of failures under-reports
itself forever and nobody notices. Write that as a check, not as a hope.

### 8.2 Checks

<Numbered, each runnable by hand — a command or a click, with a visible pass/fail. "The code looks
right" is not a check. Keep them in the order a reviewer would run them.>

1. <command or click → observable result>
2. <...>

### 8.3 Test environment / stand-in

<Delete if you can test against the real thing. Otherwise: how do you exercise this before the real
dependency exists?>

⚠️ **A stand-in that does not run the production code path certifies a code path that will not
ship.** Fixture files, mocked clients and hand-written stubs usually skip the very steps that break
in production. Build the *other side of the boundary* instead — real schema, real rows, our code
reading it unmodified — so that **the only difference from production is who wrote the data**.

- **What it covers:** <the cases only this reaches — and, honestly, the ones it still cannot>
- **Fixture:** <one real, representative record traced end to end; say why it is representative>

### 8.4 Invariants

<Properties that must hold on every run, not one happy path. The usual load-bearing set:>

- <Replaying the same input twice yields the same final state.>
- <A duplicate has exactly one effect.>
- <An input that changes nothing produces no event, no write, and no re-render — and a *real* change
  immediately after one is still delivered.>
- <A late/older input is retained but cannot roll state backward.>
- <Crash injected at each commit point recovers with no loss and no double application.>

### 8.5 Bars

| Bar | Passing means |
|---|---|
| <name> | <the observable condition — a number, a diff, a screen state> |

<Report the measurement even when it misses the budget. A bar you only quote when it passes is not a
bar.>

## 9. Rollout / Phasing

<details>
<summary><b>In plain words</b> — click to expand</summary>

> [!NOTE]
> <Two clocks, kept separate: how long the build takes, and how long the evidence takes before anyone trusts it. Compressing a schedule compresses implementation, never proof.>

</details>

<Week-by-week: what lands when. shadow → advisory → live, if applicable.>
- **Go-live bar:** <the measurable gate each stage must pass before it advances — cite §8 by number,
  e.g. "advisory only after checks 1–10 pass on N consecutive service days". A stage without a bar is
  a hope, not a rollout; a bar that does not name its evidence is a slogan.>
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 설계부터 시키기 ⭐
"지금 열려 있는 이 폴더가 내 앱이야. 여기에 로그인·보안을 붙일 건데, 사람이 하나씩 찔러보는 공격만이 아니라 AI 에이전트가 자동으로 훑는 대규모 공격까지 버텨야 해. 이 앱에 데이터베이스가 없으면 SQLite로 먼저 만들고 시작해줘. 오늘 배운 개념 — 비번 해시(bcrypt) · 인증과 인가는 다름(매 요청 소유자 확인) · 입력을 명령으로 착각(SQL·프롬프트 인젝션) · XSS/CSRF와 토큰을 어디 저장하느냐 · 최소 권한 · 자격증명 위생 · rate limit — 을 기준으로 선택지를 비교분석한 다음, 그 결론으로 Design Doc을 작성해줘. 다만 혼자 다 쓰지 말고, 입력 → 전송로 → 유입 → 신분 → 권한 → 저장 → 유출 7단계를 한 칸씩 나한테 물어보면서 정해줘. 결정한 항목은 각각 ADR로 따로 남겨줘."
🎯 Grill me (공격자 입장)
"이 보안 설계를 공격자 입장에서 캐물어봐 — 토큰이 털리면 어디까지 뚫리나, 로그아웃이 즉시 되나, 로그나 에러 메시지로 비밀이 새나, 필요 이상 권한으로 도는 프로세스는 없나. 빈틈마다 어떻게 고칠지도 같이."
💬 구현 + 검증까지
"이 design doc대로 내 앱에 구현하고, §7 검증 10개를 하나씩 내가 직접 해볼 수 있게 알려줘 — 뭘 누르거나 치면 되는지, 통과면 화면에 뭐가 보이는지. 코드를 읽어서 판단하지 말고 실제로 돌려서 확인할 수 있는 방법으로."
⭐ 영어 원문 그대로 Claude Code에 붙여넣기 → 수업에서 본 '방어 앱'이 나옴. 검증 10개를 내 눈으로 하나씩 → /10w/w2/practice
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# Design Doc (TRD) — Add Authentication & Security to <Your App>

> W2 filled example. **Copy this whole doc, fill the `<...>`, and paste it to Claude Code**
> (prompt at the bottom). It produces the "hardened app" you saw attacked in class —
> brute-force, SQL injection, IDOR, no-auth, flood, XSS and CSRF all bounce off.
> Write in English (better LLM output). Decisions here are recorded as ADRs.

**Status:** Draft → In Review → Approved → Implemented  ·  **Author (owner):** <you> ·
**Reviewers:** <mentor> · **Related:** <PRD link> · ADR-002 (auth approach) · **Last updated:** <YYYY-MM-DD>

---

## 1. Context & Summary
The app from W1 (`<e.g. a notes app / mini chatbot with saved history>`) currently has **no login
and no access control** — anyone can read anyone's data and the server trusts all input.
This doc adds **authentication (who are you) and authorization (are you allowed)** plus the baseline
hardening that stops the attacks that actually breach small apps.

**Threat model note (why "we're too small to be a target" is not true anymore):** most traffic that
finds a new public endpoint is automated. Scanners sweep continuously, and in 2026 a frontier AI model
autonomously escaped its own sandbox and breached a production service by chaining ordinary flaws —
**command injection into a proxy, an over-privileged process, and credentials left lying on a server**.
The defenses below are exactly the ones that would have stopped it. Design for a tireless attacker,
not an occasional one.

## 2. Goals & Non-Goals
- **Goals:** users log in; each user sees only their own data; passwords are stored safely;
  brute-force, injection, cross-site attacks and request floods are blocked; **a single compromise
  stays contained** (least privilege, credential hygiene, audit trail).
- **Non-Goals (defer):** social login / OAuth, MFA, SSO, a CDN/WAF edge layer (that's the *scale*
  week). Ship the app layer first.

## 3. Architecture (auth flow)
```mermaid
flowchart LR
  User --> Login[POST /login]
  Login -->|verify bcrypt hash| DB[(users)]
  Login -->|access token, short TTL| Mem[held in memory by the client]
  Login -->|refresh token, HttpOnly cookie| Cookie[(browser cookie)]
  Mem -->|Authorization: Bearer| API[protected endpoints]
  API -->|verify signature + exp| API
  API -->|owner_id == me ?| DB
  Cookie --> Refresh[POST /refresh -> rotate] --> Mem
```
- **Token-based auth (JWT).** The access token is **short-lived (~15 min)** and never written to
  `localStorage`; the **refresh token lives in an `HttpOnly` cookie** the browser's JavaScript cannot read.
- The **seam**: one `require_login()` / `current_user()` helper used by every protected endpoint —
  the only place auth is read, so the scheme can be swapped without touching the rest of the app.

## 4. Data & API
- **Data model — `users`:** `id`, `email` (unique), `password_hash` (bcrypt, never plaintext),
  `token_version` (bump to invalidate all of a user's tokens), `created_at`.
  Every user-owned row (notes, messages, …) has an `owner_id` → `users.id`.
- **Data model — `refresh_tokens`:** `jti` (unique id), `user_id`, `expires_at`, `revoked_at`.
  This is the small piece of server state that makes logout and revocation actually work.
- **API:**
  - `POST /login    { email, password } → 200 { access_token } + refresh cookie | 401`
  - `POST /refresh  (refresh cookie) → 200 { access_token } + NEW refresh cookie | 401`  (rotate on every use)
  - `POST /logout   → 200`  (revoke the refresh token; bump `token_version`)
  - `GET  /me/items → 200 [ my items ]`  (login required)
  - `GET  /items/{id} → 200 | 401 (no valid token) | 403 (not my item)`

## 5. Security Requirements  ★ (the core — implement all)
Organized by the **seven points a request passes through**: input → transport → inflow → identity →
authorization → storage → output. Each point gets its own "don't trust this".

### 5.1 Input — never let input become a command
- **Parameterized queries only.** Never build SQL by string concatenation/format; use bound
  parameters (`WHERE email = ?`). *(SQL injection.)*
- **Validate and normalize at the boundary** — types, lengths, formats, allow-lists.
- **Same rule for AI features.** If the app calls an LLM, user text is **data, not instructions**:
  keep system instructions separate, and never let model output trigger a privileged action without
  a server-side check. *(Prompt injection is the same root cause as SQL injection.)*

### 5.2 Transport — the road is not safe
- **HTTPS everywhere**, plus `Strict-Transport-Security`. No credentials over plain HTTP.

### 5.3 Inflow — don't accept unlimited requests
- **Login lockout** — after **5** failed logins for an account (or IP), lock for a cooldown → **429**.
- **Rate limiting** — per-IP sliding window on all endpoints; over the limit → **429**.

### 5.4 Identity — a token is a claim, not a fact
- **Sign with a strong secret/key**, pin the algorithm server-side, and **reject `alg: none`**.
- **Short expiry (~15 min)** on access tokens; verify `exp` on every request.
- **A JWT is signed, not encrypted** — anyone can read its contents. **Never put secrets,
  passwords, or personal data in the payload.**
- **Storage:** access token in memory; **refresh token in an `HttpOnly` + `Secure` + `SameSite`
  cookie**. Do **not** put tokens in `localStorage` — any XSS reads them instantly and the attacker
  can then use them from their own machine. *(See ADR-002.)*
- **Revocation:** rotate the refresh token on every use, store its `jti`, and reject reuse.
  Logout revokes the refresh token and bumps `token_version`.

### 5.5 Authorization — logging in is not permission
- For any resource with an owner, check `resource.owner_id == current_user.id` on **every request**
  → **403** otherwise. A hidden button in the frontend is **not** a control. *(IDOR / BOLA.)*
- **Role checks (RBAC)** for admin-only actions, and **least privilege everywhere**: the app's DB user,
  workers, and any background/agent process get only the permissions they actually need — **never root**.

### 5.6 Storage — assume it will be read some day
- **Passwords: bcrypt (or argon2) with a per-user salt.** Never plaintext, never plain SHA-256.
- **Secrets from the environment** — DB URL, signing key, API keys from env vars / a secrets manager.
  Never commit them; `.env` in `.gitignore`. **Rotate immediately if one is ever exposed.**

### 5.7 Output — be careful on the way out too
- **Escape on render.** User text goes into the page as **text, not markup** — use the framework's
  auto-escaping and never build HTML with raw `innerHTML`. *(XSS.)*
- **CSRF:** because the refresh cookie is sent automatically, set `SameSite=Lax|Strict` and require a
  CSRF token on cookie-authenticated state-changing routes. (Bearer-header routes are not CSRF-able.)
- **Don't leak in errors** — generic messages to the client, details to the log.

### 5.8 Containment — assume one thing will fail
- **Structured audit log:** `{time, user, action, result}`. **Never log passwords, tokens, or secrets.**
- **Watch outbound traffic**, not just inbound — stolen data has to leave somehow.
- **Use battle-tested libraries.** Do not hand-roll crypto, JWT verification, or auth.

## 6. Alternatives Considered
- **Session vs JWT vs OAuth** — chose JWT; see **ADR-002** for why and what was rejected.
- **Token storage: `localStorage` vs `HttpOnly` cookie vs memory + cookie** — chose memory + cookie
  (ADR-002). `localStorage` is the common tutorial default and the weakest against XSS.
- **SHA-256 vs bcrypt for passwords** — bcrypt (slow + salted); SHA-256 is too fast and unsalted.

## 7. Verification  ★ (how we prove it — run these after building)
1. Open the DB: the password column shows `$2b$...` (a bcrypt hash), **not** `1234`.
2. Log in as user A, request user B's item by id → **403**.
3. Call a protected endpoint with no token → **401**.
4. Send `' OR '1'='1` as the password → login **fails** (not bypassed).
5. Hit an endpoint 100× fast → starts returning **429**.
6. Post `<script>alert(1)</script>` as content → it shows up **as text**, no popup.
7. In devtools, `document.cookie` does **not** show the refresh token (it's `HttpOnly`),
   and no token is in `localStorage`.
8. Log out, then reuse the old refresh token → **401**, and it cannot be rotated back in.
9. `grep` the logs for a known password and for `Bearer` → **no hits**.
10. Enter the wrong password 5× for one account → the 6th attempt is **locked out (429)**,
    and even the *correct* password is refused until the cooldown ends.

Each check must be runnable by hand — a command or a click, with a visible pass/fail.
"The code looks right" is not a check.

## 8. Rollout / Phasing
- Ship §5.1–5.7 together (they're one layer); §5.8 lands with it — logging is not optional.
- **Go-live bar:** all ten checks in §7 pass. A check that "should pass" has not passed.
- Later (scale week): edge rate-limit / WAF / CDN in front of the app.

---

## Prompt to give Claude Code
> Read this design doc, then implement it on my app.
> [paste this whole document, with the `<...>` filled in]
> Use my stack's standard libraries for password hashing (bcrypt/argon2), JWT verification and the DB
> layer — do not hand-roll crypto. Parameterized queries everywhere. Access token in memory, refresh
> token in an HttpOnly + Secure + SameSite cookie, rotated on every use. If my app has no database
> yet, create one (SQLite is fine) as part of this. Then walk me through the ten checks in §7 one by
> one — for each, the exact thing I click or run, and what I should see on screen if it passed.

2🔒 보안 ADR (W2) — 로그인 방식 결정 (JWT · 토큰 저장 위치 · 매 요청 권한 확인)

① 빈 템플릿 — 복사해서 채우기
# ADR-<NNN> — <short decision title>

> One decision: what you chose, why, what you rejected. Short and **immutable** — a later
> change gets a *new* ADR that supersedes this one (don't edit history). Write in **English**.
> Full filled example: `../filled/ADR-001-reply-seam.md`.

- **Status:** proposed | accepted | superseded-by-ADR-NNN _(YYYY-MM-DD)_
- **Date:** <YYYY-MM-DD>
- **Deciders:** <you / reviewers>
- **Context source:** <PRD / Design Doc links>
- **Related:** <other ADRs>

## Context
<The forces at play. What makes this a real decision (a fork with trade-offs)?>

## Decision
<What you chose, stated plainly. If it's a rule/gate, write the rule up front.>

## Options considered
- **<Option A — chosen>** — <why it wins>.
- **<Option B>** — rejected: <why>.
- **<Option C>** — rejected: <why (e.g. over-engineered / YAGNI)>.

## Consequences
- **Positive:** <what gets easier>.
- **Negative:** <what you accept / how you mitigate>.
- **Reversible?** <blast radius if you change your mind>.

## Verdict
_(Optional — for measured decisions. Fill after you test: PASS/FAIL + one line.
Commit the rule before measuring so you can't rationalize the result afterward.)_
💬 이렇게 물어보세요 — 좋은 프롬프트
💬 대안 비교부터 ⭐
"로그인 방식을 정해야 해. 세션 / JWT를 HttpOnly 쿠키에 / JWT를 localStorage에 / OAuth 를 오늘 배운 기준으로 표로 비교해줘 — XSS로 훔칠 수 있나, CSRF로 시킬 수 있나, 즉시 로그아웃이 되나, 서버를 늘릴 때 어떤가. 그다음 고른 것과 버린 이유까지 ADR 한 장으로 써줘."
🔁 내 상황이면 달라지나
"내 앱은 [모바일 앱도 붙는다 / 서버 한 대다 / 사내 전용이다] — 이 조건이면 결정이 바뀌는지 다시 판단해줘. 바뀌면 기존 ADR을 고치지 말고 supersede 하는 새 ADR로."
⭐ 핵심은 JWT냐 세션이냐가 아니라 — 그 증표를 어디에 저장하느냐. 결정이 바뀌면 새 ADR로 supersede (기존 걸 안 고침).
▸ 채워진 예시 보기 (미니 챗봇) — 직접 해보고 나서 펼쳐 비교하세요
# ADR-002 — Authentication approach: JWT (short access token + rotating refresh cookie) + bcrypt + per-request authorization

> W2 filled example. One decision, short and **immutable** — if you change it later, write a *new*
> ADR that supersedes this one (don't edit history). Write in English.
> Copy this, adjust to your app, and keep it next to your design doc.

- **Status:** accepted _(YYYY-MM-DD)_
- **Date:** <YYYY-MM-DD>
- **Deciders:** <you> (+ mentor review)
- **Context source:** PRD <link> · Design Doc — Add Authentication & Security
- **Related:** ADR-001 (reply seam)

## Context
The app needs login and per-user data isolation, and `<a mobile client / a second service>` will call
the same API, so login state has to travel outside one browser session. We must pick **how login state
is carried**, **where the client keeps it**, and **how passwords are stored** — without over-building.

The forces: ship fast, but don't create the breaches we studied — plaintext passwords, "logged in =
can see everything", input executed as a command, and tokens that an attacker can lift and reuse.
Two facts from class shape this decision:

- **What decides XSS/CSRF exposure is not "session vs JWT" — it is *where the credential is stored*.**
  Anything JavaScript can read is gone the moment there is one XSS; anything the browser attaches
  automatically can be triggered by another site.
- **A token that cannot be revoked is a liability.** JWT's usual weakness is that it stays valid until
  it expires, so the design has to buy revocation back explicitly.

## Decision
- **Login state = JWT.** A **short-lived access token (~15 min)** sent as `Authorization: Bearer`,
  **held in memory** by the client — never in `localStorage`.
- **Refresh token in an `HttpOnly` + `Secure` + `SameSite` cookie**, **rotated on every use**, with its
  `jti` stored server-side so reuse is detected and logout actually revokes. `token_version` on the user
  row kills every outstanding token at once.
- **Passwords = bcrypt hash + per-user salt** (never plaintext, never plain SHA-256).
- **Authorization = checked on every request:** for any owned resource, `resource.owner_id ==
  current_user.id` or **403**. The frontend hiding a field is **not** a control.
- **Queries parameterized**; **secrets from env**; **login = 5-fail lockout + rate limit**;
  **least privilege** for every process (no root workers).

## Options considered
- **JWT, access in memory + refresh in HttpOnly cookie (chosen)** — works for browser *and* non-browser
  clients; nothing readable by JavaScript; rotation + `jti` restores the revocation that plain JWT lacks.
  Cost: more moving parts than a session, and we keep a small amount of server state after all.
- **JWT in `localStorage`** — rejected. It is the common tutorial default and the weakest option: one XSS
  reads the token in a single line, and the attacker then replays it **from their own machine, for as long
  as it is valid**. Convenience is not worth an exfiltratable credential.
- **Server-side session + opaque cookie** — rejected *for this app*, not on principle. It is genuinely
  simpler and revokes instantly, and it would be the right pick for a browser-only single-server app.
  We need a token our non-browser client can carry, and we don't want shared session storage when we
  add a second server.
- **OAuth / social login ("Log in with Google")** — deferred: extra setup and a third-party dependency
  the MVP doesn't need. Add later behind the same seam if users ask for it.

## Consequences
- **Positive:** one auth scheme for every client; no credential is reachable from JavaScript; automatic
  cookie attachment is contained by `SameSite` + CSRF token; the four classic breaches are closed by construction.
- **Negative / accepted:** an access token stays valid until it expires (≤15 min) even after logout —
  we accept that window and cut it with short TTL + refresh revocation. Rotation adds a table and a
  refresh endpoint to maintain. Signing-key leakage forges any user, so the key is env-only and rotatable.
- **Reversible?** Yes — auth is read only through `current_user()` / `require_login()`, so moving to
  sessions or OAuth touches one place, not the whole app.

## Verdict
_(Fill after you test.)_ PASS/FAIL — DB shows `$2b$…` (not plaintext) · other user's id → 403 ·
no token → 401 · `' OR '1'='1` login → fails · 100 fast requests → 429 ·
`<script>` renders as text · refresh token invisible to `document.cookie`, nothing in `localStorage` ·
reused refresh token after logout → 401 · 5 wrong passwords → locked out.
양식 = 빅테크 표준 + ATI Labs 실제 문서 + Meta PRD/TRD 종합  ·  ← 전체 위키로  ·  터미널로 내 위키 만들기 →