AI용 분할본 · 지도 2단계(코드 받기) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, PART 1 계정 준비, PART 4 결정서·마스터 스펙 · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 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개" 출력을 그대로 보인다.
당신은 시니어 풀스택 개발자입니다. 아래 스펙대로 Next.js 웹앱을 처음부터 끝까지 만들어주세요. 스펙에 없는 것을 임의로 추가하지 말고, 모호하면 스펙의 완료 기준을 우선하세요.
[제품 결정서 — 4-0의 단일 [제작 결정서] 중 기술 항목. 비어 있으면 코드를 만들지 말고 4-0의 질문부터 하세요]
{제품 결정서}
결정서 적용 규칙:
- 코드는 쓰지 않고 받는다: 새 프로젝트 폴더(PROGRESS.md 만 있어도 됨)에서 curl -sO https://console.hrum.ai/hostingEbook/get-book-code.mjs && node get-book-code.mjs && rm get-book-code.mjs (부록 A-2) → 바로 git init -b main. 이 책의 모든 책 코드 파일이 한 번에 생긴다
- 로그인은 항상 있다: 아이디+비밀번호 가입(복구 코드 1회 표시)·로그인·로그아웃·복구 화면, 서버 세션, 비로그인 401. 코드는 PART 5 의 책 코드 그대로(lib/db.ts, lib/auth.ts, lib/get-session-user.ts, lib/failure-limit.ts, app/api/auth/*)
- 메모는 PART 6 의 책 코드 그대로(lib/notes.ts, app/api/notes/*, components/Notes.tsx). 첫 화면 app/page.tsx 는 components/Notes 를 불러 쓴다
- 화면 배치 "한 칸형"이면 components/Notes.module.css 의 .layout 을 한 열로(편집 위, 목록 아래). "두 칸형"이면 책 그대로
- 관리자 페이지 "켬": 코드는 이미 받아져 있다. 뼈대를 배포한 뒤 지도 6단계에서 B-7 절차로 켠다(환경변수 두 개). "끔"이면 아무것도 하지 않는다 — 환경변수가 없으면 꺼져 있다
- 유료화 "나중에 유료": 결제 코드는 만들지 않는다(9-2 — 결제는 완성 뒤 PG사와 직접 계약해 따로 붙인다). 유료가 될 기능만 화면에서 분리
[앱 개요]
- 앱 이름: {앱이름}
- 컨셉: {한 줄 컨셉}
- 화면 문구: {화면 문구} — app/page.tsx(머리 문구)·app/layout.tsx(탭 제목·설명)에만
- 디자인 방향: {디자인 방향} · 모바일 우선 — app/globals.css 의 디자인 토큰과 components/Notes.module.css 에만
[기술 스택 — 변경 금지]
- Next.js 15 (App Router, output: 'standalone') + TypeScript(strict, any 금지). 스타일은 app/globals.css 의 CSS 변수(디자인 토큰)
- 런타임 의존성은 next, react, react-dom, pg 뿐 (8-0 package.json). 외부 서비스 SDK(인증·DB·캐시·결제·분석·AI)를 설치하지 않는다
- 배포: RIDM Apps (PART 8). 저장소 루트 Dockerfile(8-0), 포트 3000, 사용자 1000, 읽기 전용 파일시스템(쓰기는 /tmp·/app/.next/cache 만), 시작 후 10초 안에 / 응답
- DB: RIDM Apps 앱 전용 PostgreSQL. process.env.DATABASE_URL(플랫폼이 주입, 코드에 SSL 옵션 불필요), pg Pool max 2, 테이블은 lib/db.ts 가 첫 요청 때 CREATE TABLE IF NOT EXISTS. 파일시스템·브라우저 저장소에 데이터 저장 금지
- HRUM 은 쓰지 않는다 — API 키·크레딧 없음
[보안 규칙 — 절대 위반 금지]
1. 로그인이 필요한 모든 라우트는 getSessionUser(req) 로 회원을 확인하고 비로그인은 401
2. 회원 데이터의 모든 SELECT/UPDATE/DELETE 에 WHERE user_id = 세션 회원 id. 회원 id 를 브라우저에서 받지 않는다. 남의 것은 없는 것과 똑같이 404
3. 바꾸는 요청(POST·PATCH·DELETE)은 lib/same-origin.ts 로 Origin 이 이 앱 주소와 같을 때만 — 아니면 403. 가입·로그인·로그아웃·복구·탈퇴는 거기에 JSON 본문까지(jsonFromHere — 로그인 CSRF 차단)
4. 세션 쿠키는 서버 응답의 Set-Cookie 로만(평범한 이름 session, HttpOnly·Secure·SameSite=Lax·Path=/). RIDM Apps 관문이 브라우저에는 __Host-session 으로 저장하고 앱에는 session 으로 돌려준다. document.cookie·localStorage 로 로그인이나 데이터를 다루지 않는다
5. 비밀번호·복구 코드는 scrypt(무작위 salt) 해시만 저장, 세션 토큰은 sha256 만 저장. 로그인·복구 실패는 lib/failure-limit.ts 로 제한
6. 요청 제한은 lib/rate-limit.ts 하나로(메모리 — 앱은 컨테이너 1개): 가입 같은 IP 1시간 5회, 새 메모 회원당 1분 30개, 메모 고치기·지우기 합쳐서 회원당 1분 60회, 메모 읽기 회원당 1분 120회. 쌓이는 양은 DB 에서 센다: 메모 회원당 합계 200개·하루 50개(lib/notes.ts — 한 회원 최대 약 3.5MB)
7. SQL 값은 언제나 $1·$2 자리로 넘긴다. 문장에 값을 이어 붙이지 않는다
8. .gitignore 에 .env*와 그 다음 줄에 !.env.example(예외) 포함 + 값을 비운 .env.example 커밋. NEXT_PUBLIC_ 변수는 하나도 없다
9. 관리자 주소(ADMIN_PATH)·비밀번호(ADMIN_PASSWORD)는 콘솔 환경변수에만 — 코드·커밋·로그·PROGRESS.md 에 적지 않는다(B-7)
10. npm audit 결과(RIDM Apps 빌드 검사가 같은 것을 운영자에게 보여준다): package.json 은 고치지 않는다 — audit 결과를 완료 보고에 그대로 적고 책 개정(get-book-code --force)을 기다린다(8-0)
11. 브라우저 본문은 통째로 쓰지 않는다 — 가입·로그인·복구·탈퇴는 readStrings(4KB), 메모는 readJsonCapped(64KB)로 상한에서 읽기를 멈추고 parseNoteInput 이 고른 칸만 쓴다
12. 새 라우트를 만들거나 고치면 배포 전에 7-3-1 점검표와 7-3-2 검수 체크리스트를 다시 통과시킨다. 서버 코드는 7-3-2 보안 지시문을 지켜 쓴다
[서버 라우트 — 책 코드에 이미 있다]
POST /api/auth/signup { username, password } → { user, recoveryCode } (복구 코드는 이 응답에서 한 번만)
POST /api/auth/login { username, password } → { user } · 5회 실패 뒤 429 · 정지 회원 403
POST /api/auth/logout · POST /api/auth/recover { username, code, newPassword } → { recoveryCode }(새 코드)
POST /api/auth/delete-account { password } → { ok } · 틀린 비밀번호 403 · 회원·세션·메모 삭제(되돌릴 수 없음)
(auth 전부: 다른 주소·JSON 아닌 본문은 403)
GET /api/me → { user } 또는 401 · GET /api/health → { ok, commit }
GET /api/notes?offset=0 → { notes, hasMore } (50개씩, 최근 수정순)
POST /api/notes { title, body } → 201 { note }
GET · PATCH { title?, body? } · DELETE /api/notes/:id → 남의 것·없는 것·UUID 아닌 id 는 404
한도: 제목 1~100자(앞뒤 공백 제외, 필수) · 내용 5,000자 · 본문 64KB(413) · 새 메모 1분 30개·하루 50개·합계 200개 · 고치기·지우기 합쳐 1분 60회(429) · 세션 회원당 최근 10개
[화면 명세]
1. 가입(/signup)·로그인(/login)·복구(/recover): PART 5 책 코드 그대로에 디자인만 입힌다. 가입 직후 복구 코드를 한 번만 보여주고 "적어뒀어요" 체크 뒤 진행
2. 첫 화면(/): 로그인 안 됨이면 /login 으로. 로그인됨이면 머리 문구 + 로그아웃 + components/Notes(목록·새 메모·편집·삭제) + 맨 아래 components/DeleteAccount(회원 탈퇴, 5-4). 부록 A-2 가 아니라 PART 6 의 app/page.tsx 에서 출발한다
3. 공통: 결정서 화면 배치대로, 라이트·다크 모두 글자가 잘 보일 것, 390px 에서 가로 넘침 0, 오류는 사람 말로("로그인이 필요해요" 등 — 책 코드가 이미 한다)
[완료 기준 — 스스로 실행해 증명할 것]
- 계정 게이트: HRUM 콘솔 가입·이메일 인증 완료, (결정서에서 GitHub 를 고른 경우) GitHub Private 저장소 준비(제 컴퓨터 git 만이면 8-1 에서). 비밀값 자체는 출력하지 않는다
- 책 코드 받기: 첫 실행 출력 "[완전 코드] N개 — 새로 만듦 N · 책 원문으로 되돌림 0 · 책과 다름 0" (N 은 책 코드 파일 수 — 모든 책 코드 파일을 손으로 옮기지 않고 이 스크립트로 만든다)
- npm run check → 끝까지 통과(책 코드 대조 0 · 빌드 에러 0 · 테스트 전부 통과 · npm audit high 이상 0건 — 나오면 package.json 은 고치지 않고 결과를 보고해 책 개정을 기다린다)
- 책 코드 대조: node scripts/verify-book-code.mjs → "[완전 코드] N개 확인 — 불일치 0개" 출력(종료 코드 0). 이 스크립트도 책 코드(부록 A-2)다. 불일치가 나오면 그 파일을 책 원문으로 되돌린다 — 타입 에러가 난다고 책 코드를 고치지 말고 tsconfig.json·package.json 이 책과 같은지부터 본다
- git check-ignore .env.local → 경로 출력 / grep -rn "NEXT_PUBLIC_" app lib components → 0건 / grep -rn "localStorage\|document.cookie" app lib components → 0건
- DB 테스트 한 줄(7-5): TEST_DATABASE_URL 로 npx vitest run → skipped 0, 테스트 DB 가 없으면 "DB 테스트: 미실행(테스트 DB 없음)"
- 로컬: 5-3 의 로컬 DB + npm run build && npm run start 뒤 node scripts/smoke-live.mjs http://localhost:3000 --quick → 실패 0개 (로그인 실패 제한 429·복구 포함. 로컬 DB 가 없으면 "미실행"으로 적고 실서버로 확인)
- RIDM Apps 에 올려(PART 8) 첫 배포가 뜬 뒤 node scripts/smoke-live.mjs https://이름.ridm.ai → 실패 0개 (첫 줄의 실행 중 커밋 = 마지막 커밋이어야 배포된 것 · 다른 회원 격리 404 포함)
- 390px·1280px 스크린샷, 라이트·다크 — 메모 화면 가로 넘침 0 (npm run start 로 띄운 앱에서 — npm run dev 는 개발 배지가 목록을 가린다)
마스터 스펙이 만드는 뼈대의 나머지입니다. app/layout.tsx·app/globals.css 는 결정서대로 고쳐도 되는 파일이고(12·13번), tsconfig.json 은 책 그대로 둡니다.
app/layout.tsx — 레이아웃 (수정 허용 — 탭 제목·설명)코드 자리: cat app/layout.tsx · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서대로 탭 제목·설명만 · 원문(사람용 — AI 는 열지 않는다)
app/globals.css — 디자인 토큰 (결정서 13번으로 교체)코드 자리: cat app/globals.css · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서 13번 디자인 토큰으로 · 원문(사람용 — AI 는 열지 않는다)
tsconfig.json — TypeScript 설정코드 자리: cat tsconfig.json · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
도구 스크립트 3개 — 코드 받기·대조·실서버 점검. 앱 기능이 아니라 AI 가 실수 없이 만들고 스스로 확인하게 해 주는 장치입니다. get-book-code.mjs 는 콘솔에도 같은 파일이 있어 새 프로젝트 폴더(1단계의 PROGRESS.md 만 있어도 됩니다 — 책 파일이 아니라 건드리지 않습니다)에서 curl -sO https://console.hrum.ai/hostingEbook/get-book-code.mjs && node get-book-code.mjs && rm get-book-code.mjs 로 시작하고, 끝나면 바로 git init -b main 을 합니다 — 4-3·7-3 점검과 개정 맞추기(git status)가 git 을 쓰고, 커밋·push 는 8-1 에서 합니다. 첫 실행이 잘 되면 [완전 코드] N개 — 새로 만듦 N · 책 원문으로 되돌림 0 · 책과 다름 0, 이어서 node scripts/verify-book-code.mjs 가 [완전 코드] N개 확인 — 불일치 0개 를 찍습니다(N 은 같은 수)(책 파일을 첨부한 보조 경로라면 node get-book-code.mjs hostingEbook.html 뒤 rm get-book-code.mjs). 첫 실행 뒤 루트의 사본은 지웁니다 — 그 뒤로는 scripts/get-book-code.mjs 만 쓰고(스스로 최신판으로 바뀝니다), 루트 사본이 남아 있으면 실행을 거부하고 지우는 명령을 알려 줍니다. 두 스크립트는 실행할 때마다 지금 공개된 이 책(https://console.hrum.ai/hostingEbook.html)을 기준으로 삼습니다 — 책이 개정돼 대조에 "다름"이 뜨면 세 줄로 최신 책 코드에 맞춥니다(수정 허용 파일은 건드리지 않고, package.json 은 name 만 남기고 책판으로): ① git status 가 깨끗한지 확인(아니면 먼저 커밋) ② curl -s https://console.hrum.ai/hostingEbook/get-book-code.mjs -o scripts/get-book-code.mjs ③ node scripts/get-book-code.mjs --force. 되돌린 파일은 "되돌림"으로 한 줄씩 찍히고 전 내용은 옆의 <파일>.orig 에 남으니(이미 있으면 <파일>.1.orig… — 옛 사본을 덮지 않음, 커밋되지 않음), 일부러 고쳤던 파일이 있으면 그 사본이나 git diff 로 사라진 내용을 확인합니다. app/api/notes/[id]/route.ts 처럼 대괄호가 든 경로는 zsh 에서 따옴표로 감쌉니다(스크립트 출력도 따옴표로 찍습니다). smoke-live.mjs 는 앱의 가입 제한(같은 IP 1시간 5회) 중 2회를 쓰니 한 시간에 두 번 정도까지만 돌립니다.
scripts/get-book-code.mjs — 책 코드 받기 — 모든 책 코드 파일 생성코드 자리: cat scripts/get-book-code.mjs · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/get-book-code.test.ts — 코드 받기·대조 스크립트 테스트 — 자가 갱신·되돌림 목록·수정 허용 파일 보호·package.json name·프로젝트 밖 금지·화면 검사·관리자 주소 검사코드 자리: cat tests/get-book-code.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
scripts/verify-book-code.mjs — 책 코드 대조 — 완료 기준·push 전마다 실행코드 자리: cat scripts/verify-book-code.mjs · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
scripts/smoke-live.mjs — 실서버(또는 로컬) 점검 — 8-3코드 자리: cat scripts/smoke-live.mjs · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
AI용 분할본 · 지도 2단계(코드 받기) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, PART 1 계정 준비, PART 4 결정서·마스터 스펙 · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 A-2 의 curl 한 줄부터)