AI용 분할본 · 지도 3단계(회원·메모 화면) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, PART 5 회원 기능 · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 A-2 의 curl 한 줄부터)
핵심 규칙 — AI 는 어느 페이지에서든 이것을 지킨다
node scripts/get-book-code.mjs 가 책 그대로 만든다(첫 실행은 부록 A-2 의 curl 한 줄). AI용 분할본의 코드 자리는 " 경로" 한 줄이다 — 그 파일을 cat 으로 읽는다. 고쳐도 되는 파일은 PART 0 [AI 시작 지침] 5-b 의 목록뿐이다.node scripts/verify-book-code.mjs 를 실행해 "불일치 0개" 출력을 그대로 보인다.회원제 앱의 두 번째 벽은 "이 데이터가 이 회원의 것인가"입니다. 메모장은 그 벽을 연습하기에 가장 작은 앱입니다 — 여기서 익힌 구조(모든 쿼리에 회원 조건, 남의 것은 "없음", 변경은 같은 주소에서만, 속도 제한)가 게시판·일기·할 일 목록에 그대로 쓰입니다.
메모는 앱 전용 DB의 notes 테이블 한 곳에 모든 회원 것이 같이 들어 있습니다. 회원을 가르는 것은 딱 하나 — 모든 쿼리의 WHERE user_id = $… 조건이고, 그 값은 브라우저가 아니라 세션 쿠키에서 서버가 꺼낸 회원 id
| 요청 | 하는 일 | 성공 | 실패 |
|---|---|---|---|
GET /api/notes?offset=0 | 내 메모 목록 — 최근 수정순 50개씩, { notes, hasMore } | 200 | 비로그인 401 · 잘못된 offset 400 |
POST /api/notes | 새 메모 { title, body } | 201 { note } | Origin 다름·없음 403 · 비로그인 401 · 형식 400 · 64KB 초과 413 · 회원당 1분 30개·하루 50개·합계 200개 초과 429 |
GET /api/notes/:id | 내 메모 하나 | 200 { note } | 비로그인 401 · 남의 메모·없는 메모·UUID 아닌 id 모두 404 |
PATCH /api/notes/:id | 제목·내용 중 준 것만 바꿈, 수정 시각 갱신 | 200 { note } | 403 · 401 · 400 · 413 · 404(위와 같음) · 회원당 1분 60회 초과 429 |
DELETE /api/notes/:id | 내 메모 지우기(되돌릴 수 없음) | 200 { ok: true } | 403 · 401 · 404 · 회원당 1분 60회(고치기와 합산) 초과 429 |
남의 메모가 403 이 아니라 404 인 이유: 403("권한 없음")은 "그 메모가 있기는 하다"는 사실을 알려 줍니다. 이 책의 라우트는 남의 메모를 없는 메모와 똑같이 404 로 답해서, 메모 id 를 이것저것 넣어 보는 사람이 아무것도 알아낼 수 없게 합니다. 쿼리도 "메모를 찾은 뒤 주인인지 비교"가 아니라 WHERE id = $1 AND user_id = $2 한 번이라, 비교를 빠뜨릴 틈이 없습니다.
| 항목 | 한도 | 넘으면 |
|---|---|---|
| 제목 | 앞뒤 공백을 뺀 1~100자, 필수 | 400 "제목은 1~100자예요" |
| 내용 | 5,000자까지(비어도 됨) | 400 "내용은 5000자까지예요" |
| 요청 본문 | 64KB 까지 — Content-Length 를 속여도 읽는 도중 끊음 | 413 |
| 새 메모 속도 | 회원당 1분에 30개 | 429 "잠시 후 다시 시도해주세요" |
| 메모 고치기 속도 | 회원당 1분에 60회 | 429 "잠시 후 다시 시도해주세요" |
| 새 메모 하루 | 회원당 최근 24시간에 50개 | 429 "새 메모는 하루(24시간) 50개까지예요…" |
| 메모 합계 | 회원당 200개 — 지우면 그만큼 다시 만들 수 있음 | 429 "메모는 한 사람당 200개까지예요…" |
| 목록 | 한 번에 50개, "더 보기"로 이어서 | — |
화면(components/Notes.tsx)도 같은 글자 수를 미리 막고 남은 글자 수를 보여주지만, 그건 친절일 뿐 방어가 아닙니다 — 화면을 거치지 않고 요청을 보내는 사람이 있으니 진짜 검사는 서버의 lib/notes.ts 가 합니다. 속도 제한(lib/rate-limit.ts)과 Origin 검사(lib/same-origin.ts)는 7-2 에서 설명합니다.
한 회원이 DB 를 얼마나 채울 수 있나 — 계산: 1분 30개 제한만으로는 부족합니다. 꽉 찬 메모(제목 100자·내용 5,000자, 한글 한 글자 3바이트)는 DB 에서 약 17KB(인덱스 포함 실측)라, 스크립트가 1분에 30개씩 쉬지 않고 쓰면 1분에 약 0.5MB — 무료 플랜 DB 100MB 를 3시간 남짓에 넘깁니다. 넘긴 채 하루가 지나거나 150%(5시간 남짓)를 넘으면 DB 가 읽기 전용이 되어(9-1) 모든 회원의 저장은 물론, 로그인할 때 세션 한 줄을 쓰는 것까지 실패해 아무도 로그인하지 못합니다. 그래서 개수를 DB 에서 세는 두 한도를 더 둡니다(lib/notes.ts 의 NOTES_MAX·NOTES_PER_DAY) — 회원 한 명의 최대치는 200개 × 17KB ≈ 3.5MB, 무료 DB 의 3.5%(베이직 1GB 의 0.35%)이고, 하루 50개면 하루 최대 약 0.9MB 라 그 3.5MB 에 닿는 데도 나흘이 걸립니다. 동시에 보낸 요청도 한 회원 안에서는 하나씩 세므로 한도를 넘지 못합니다. 막지 못하는 것: 여러 IP 에서 계정을 많이 만들면(가입은 IP 당 1시간 5회) 합계는 계정 수만큼 늘어납니다 — 앱 카드의 DB 용량 80% 알림 메일(8-4)이 오면 관리자 페이지(B-7)의 메모 수로 이상한 계정을 찾아 정지하고, 필요 없는 행을 정리합니다(부록 E-2).
lib/notes.ts — 메모 읽기·쓰기 — 모든 쿼리에 user_id, 입력 검사, 64KB 제한코드 자리: cat lib/notes.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
app/api/notes/route.ts — GET 목록 · POST 새 메모코드 자리: cat app/api/notes/route.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
app/api/notes/[id]/route.ts — GET · PATCH · DELETE 메모 하나 (남의 메모는 404)코드 자리: cat app/api/notes/[id]/route.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
첫 화면(/)은 로그인했는지 먼저 묻고(/api/me), 아니면 로그인 화면으로 보냅니다. 로그인했으면 "○○님의 메모" 머리와 로그아웃 버튼, 그 아래 메모 목록·편집(components/Notes.tsx), 맨 아래 접힌 "회원 탈퇴"(components/DeleteAccount.tsx, 5-4)가 나옵니다. 넓은 화면에서는 왼쪽 목록 · 오른쪽 편집, 폰(640px 이하)에서는 편집이 위로 오고 목록이 아래로 갑니다.
바꿔도 되는 것: app/page.tsx(머리 문구·배치 — 결정서 12번), app/layout.tsx(탭 제목·설명), app/globals.css·components/Notes.module.css(색·글꼴·간격·두 칸/한 칸 — 결정서 1·13번). 바꾸지 않는 것: components/Notes.tsx·components/DeleteAccount.tsx — 첫 화면은 반드시 두 컴포넌트를 불러 씁니다(verify-book-code 가 "화면이 책 컴포넌트를 안 씀"으로 잡습니다). 메모 화면을 새로 짜면 글자 수·401 처리·저장 순서를 다시 틀리게 됩니다.
app/page.tsx — 첫 화면 — 로그인 확인 후 메모 (수정 허용 — 결정서대로 배치·문구)코드 자리: cat app/page.tsx · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서대로 배치·문구만(components/Notes·components/DeleteAccount 는 그대로 불러 쓴다) · 원문(사람용 — AI 는 열지 않는다)
components/Notes.tsx — 메모 목록 + 편집 (책 그대로)코드 자리: cat components/Notes.tsx · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
components/Notes.module.css — 메모 화면 모양 (수정 허용 — 결정서 1·13번)코드 자리: cat components/Notes.module.css · 고쳐도 되는 파일(5-b) — 메모 화면 모양 — 결정서 1·13번으로 · 원문(사람용 — AI 는 열지 않는다)
손으로 두 계정을 만들어 확인하는 대신 scripts/smoke-live.mjs(부록 A-2)가 브라우저처럼 접속해 한 번에 확인합니다 — 가입 → 메모 만들기 → 목록 맨 위 → 고치기 → Origin 없는 요청 403 → 로그아웃 뒤 목록 401 → 재로그인 뒤 메모 그대로 → 두 번째 회원이 앞 회원의 메모를 목록에서 못 보고, 그 id 로 GET·PATCH·DELETE 하면 모두 404 → 지운 뒤 404. 버리는 테스트 회원(smoke_…) 2명이 생기고, 끝에 둘 다 탈퇴해 메모와 함께 지워집니다(5-4).
메모 기능을 직접 실행해서 증명해 주세요: 1. node scripts/verify-book-code.mjs → "불일치 0개" (Notes.tsx 를 안 고쳤고 첫 화면이 그것을 쓴다는 증거) 2. npm run build → 에러 0 3. 5-3 의 로컬 DB·로컬 실행(npm run build && npm run start)으로 띄운 앱에 node scripts/smoke-live.mjs http://localhost:3000 --quick → 실패 0개 (출력 원문 그대로) 4. 같은 실행(npm run dev 아님 — 개발 배지가 목록을 가린다)에서 390px·1280px 폭 스크린샷 각 1장: 메모 2개 이상 있는 상태, 가로 넘침 없음(document.documentElement.scrollWidth === clientWidth), 라이트·다크 모두 글자가 잘 보임 로컬 DB 가 없으면 3번은 "미실행(로컬 DB 없음)"으로 적고 8-3 실서버 스모크로 확인한다 — 4번은 로그인 화면 스크린샷으로 대신하고 메모 화면은 8-3 뒤 실서버에서 찍는다.
userId 로 조회하거나, WHERE id = $1 만 쓰고 AND user_id = $2 를 빠뜨린 것. 회원 id 는 getSessionUser(req) 로만 얻는다lib/client.ts 의 sendJson 으로 같은 주소의 /api/… 만 부른다(브라우저가 Origin 을 붙인다). 명령줄 도구로 시험하면 Origin 이 없어 403 이 정상hasMore)AI용 분할본 · 지도 3단계(회원·메모 화면) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, PART 5 회원 기능 · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 A-2 의 curl 한 줄부터)