AI용 분할본 · 지도 4단계(보안·테스트) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, 부록 A 스펙·코드 받기(A-2) · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 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개" 출력을 그대로 보인다.회원제 웹서비스에서 가장 자주 터지는 사고는 화려한 해킹이 아닙니다. 내 글 주소가 /notes/1043 이라는 걸 본 사람이 1042 로 바꿔 보니 남의 글이 열리는 것 — 서버가 "로그인했는가"만 보고 "이 글이 이 사람의 것인가"를 안 본 것입니다. 보안 점검 목록마다 맨 위에 있을 만큼 흔합니다. 두 번째는 다른 사이트가 여러분 회원의 브라우저를 빌려 글을 지우거나 쓰게 만드는 요청 위조(CSRF), 세 번째는 스크립트가 가입·글쓰기를 무한히 반복해 DB 를 채우는 것입니다.
다행히 이 책의 구조를 그대로 따랐다면 셋 다 이미 막혀 있습니다. 이 장은 "왜 안전한지"를 이해하고, 스스로 점검하고, 자동 테스트로 증명하는 장입니다.
방어선 1 — 누구인지는 서버만 안다 (세션)
로그인 상태는 서버가 만든 무작위 토큰 하나입니다(5-1). DB 에는 그 해시만, 브라우저에는 HttpOnly·Secure·SameSite=Lax 쿠키로만 있어 화면의 스크립트가 읽을 수도 없습니다. 로그인이 필요한 모든 라우트는 getSessionUser(req) 로 회원을 찾고, 없으면 401 입니다. 정지된 회원은 세션이 남아 있어도 여기서 걸립니다(B-7). 비밀번호·복구 코드는 scrypt 해시로만 저장돼, DB 가 통째로 새어도 원래 값을 알 수 없습니다.
방어선 2 — 모든 쿼리에 회원 조건 (격리)
메모를 읽고·고치고·지우는 모든 SQL 에 user_id = 세션 회원 id 가 들어 있고(6-1), 남의 메모와 없는 메모는 똑같이 404 입니다. 메모 id 는 UUID 형식이 아니면 SQL 에 닿기도 전에 404 이고, 값은 언제나 $1 자리로 넘겨 문장에 이어 붙이지 않습니다(SQL 주입 없음). 관리자 화면도 메모는 개수만 세고 내용은 읽지 않습니다(B-7). 이 DB 는 브라우저가 직접 접속하지 못하고(DATABASE_URL 은 서버에만) 오직 여러분의 API 라우트만 읽고 쓰므로, 남의 데이터를 막는 것은 라우트의 WHERE 절입니다.
방어선 3 — 바꾸는 요청은 같은 주소에서만 (Origin 검사)
다른 사이트에 숨겨 둔 폼이 여러분 회원의 브라우저로 POST /api/notes 를 보내게 만들 수 있습니다(쿠키가 함께 갈 수 있으니까). 그래서 메모와 관리자의 모든 POST·PATCH·DELETE 는 요청의 Origin 이 이 앱 주소와 정확히 같을 때만 통과하고, 없거나 다르면 DB 에 닿기 전에 403 입니다. 여러분의 화면은 같은 주소에서 부르므로 브라우저가 알아서 맞는 Origin 을 붙입니다.
로그인·가입도 같은 규칙입니다: 로그인은 아무것도 "바꾸지" 않는 것 같지만, 다른 사이트가 숨긴 폼으로 공격자의 아이디로 회원의 브라우저를 로그인시키면(로그인 CSRF) 회원은 모르고 공격자 계정에 메모를 적게 됩니다. 그래서 가입·로그인·로그아웃·복구·탈퇴는 Origin 이 같고 본문이 JSON(content-type: application/json)일 때만 받습니다(jsonFromHere). 다른 사이트의 폼은 JSON 을 보낼 수 없으니 두 겹입니다. 추가로 모든 페이지는 다른 사이트의 프레임 안에 넣을 수 없습니다(next.config.ts — 클릭 가로채기 방지).
lib/same-origin.ts — 바꾸는 요청은 같은 주소에서만 (CSRF 방어)코드 자리: cat lib/same-origin.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
왜 브라우저가 보낸 본문을 그대로 쓰면 안 되나요? 받은 JSON 을 통째로 DB 에 넣거나 고치면, 보낸 사람이user_id(남의 번호)나id처럼 여러분이 정하려던 값을 끼워 넣을 수 있습니다. 그래서 이 앱은parseNoteInput이title·body두 칸만 골라 타입과 길이를 확인하고, 번호는 언제나 세션에서 가져옵니다. 본문 크기도 읽는 도중 끊습니다(메모 64KB, 가입·로그인 4KB). 또 로그인 확인이 없는 라우트는 "누구나 쓰는 문"이 됩니다 — 모든 메모 라우트가 맨 먼저getSessionUser를 부르는 이유입니다.
나중에 HRUM 같은 외부 API 를 붙인다면: 서버 키를 들고 대신 호출해 주는 라우트는 문이 활짝 열려 있는 순간 남이 내 요금으로 호출하는 통로가 됩니다. 반드시 ①로그인 확인 ②같은 주소 확인 ③회원별 속도와 하루 한도 ④허용한 필드만 골라 다시 조립 — 이 순서를 외부 호출 전에 모두 통과시키세요(AI 앱 책의 채팅 라우트가 그 예시입니다).
방어선 4 — 반복을 막는다 (요청 제한) + 비밀값은 Git에 없다
가입은 같은 IP 에서 1시간 5회, 새 메모는 회원당 1분 30개, 메모 고치기·지우기는 합쳐서 회원당 1분 60회, 메모 읽기(목록·한 개)는 회원당 1분 120회, 로그인·복구·탈퇴 실패는 아이디당 5회·IP 당 30회 / 15분입니다. 속도 제한은 늦출 뿐 쌓이는 양을 막지는 못하므로, 메모는 회원당 합계 200개·하루 50개를 DB 에서 세어 한 회원이 DB 를 채우지 못하게 합니다(6-2 의 계산 — 한 회원 최대 약 3.5MB). 세션도 같은 이유로 회원당 최근 10개만 남깁니다 — 로그인을 계속 되풀이해도 세션 표가 끝없이 늘지 않도록, 새 세션을 넣는 SQL 한 문장이 그 회원의 만료된 세션과 11번째부터의 옛 세션을 함께 지웁니다(lib/auth.ts MAX_SESSIONS). 기기 11대째에서 로그인하면 가장 오래된 기기가 로그아웃됩니다. 세는 곳은 메모리(Map)입니다 — RIDM Apps 의 앱은 컨테이너 하나(프로세스 하나)로 돌아서 이것이 곧 공유 저장소이고, 별도 캐시 서비스가 필요 없습니다. 재시작하면 숫자가 초기화되는 것은 이 용도에선 괜찮습니다. 오래 켜져 있는 유료 플랜에서도 쌓이지 않도록 오래된 항목을 주기적으로 지웁니다.
lib/rate-limit.ts — 가입·새 메모·메모 고치기 요청 제한 (메모리)코드 자리: cat lib/rate-limit.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
DATABASE_URL(플랫폼이 넣음 — 여러분은 로컬 개발용 주소만 .env.local 에), 그리고 관리자 페이지를 켜면 ADMIN_PATH·ADMIN_PASSWORD(콘솔 환경변수에만 — B-7). 어느 것도 코드·커밋·로그·채팅에 적지 않습니다.gitignore에 .env*가 있는지 확인 — 단 .env*만 넣으면 .env.example까지 통째로 무시돼 커밋 못 하니, 바로 아래 줄에 !.env.example(예외 처리)을 반드시 같이 넣는다.env.example만 커밋(아래 그대로). NEXT_PUBLIC_ 으로 시작하는 변수는 브라우저에 공개되는 값이라 이 앱에는 하나도 없습니다 — 보이면 잘못 만든 것입니다 .gitignore — 전체 (순서 중요: !.env.example 예외는 반드시 .env* 다음 줄)코드 자리: cat .gitignore · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
PROGRESS.md는 AI 작업 메모(PART 0 규칙 5-e)라 커밋하지 않습니다. *.tsbuildinfo는 TypeScript 빌드 캐시입니다 — 커밋하면 RIDM Apps 빌드 검사에 "위험 코드 패턴"으로 잡혀 운영자가 보는 기록에 남습니다.
.env.example — 이 내용만 커밋(값은 전부 비움), .env.local 은 실제 값으로 채우되 절대 커밋 금지코드 자리: cat .env.example · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
[프롬프트]
보안 점검을 실행하고 각 항목의 실제 명령 결과를 보여 주세요:
1. git ls-files | grep -E "(^|/)\.env" → .env.example 만 나와야 함(아직 한 번도 커밋하지 않았다면 빈 출력도 정상 — 8-1 에서 스테이징한 뒤 다시 확인) (next-env.d.ts 는 Next 가 만드는 타입 파일이라 대상 아님)
2. git check-ignore .env.local → 경로가 출력되면 정상
3. grep -nE "^[^#]*=.+" .env.example → 0건이어야 함 (# 로 시작하는 설명 줄은 제외) (예시 파일은 값을 전부 비운다 — postgresql://user:password@… 같은 예시 주소도 RIDM Apps 빌드 검사가 비밀값으로 보고 빌드를 막는다)
4. grep -rn "NEXT_PUBLIC_" app lib components → 0건이어야 함
5. grep -rn "localStorage\|document.cookie" app lib components → 0건이어야 함 (세션·메모는 서버·DB 에만)
6. grep -rln "FROM notes\|UPDATE notes\|INTO notes" app lib → lib/notes.ts 와 관리자 개수 세기(lib/admin.ts)만 나와야 함 — 새 라우트가 notes 를 직접 건드리면 그 쿼리에 user_id 조건이 있는지 보여 주세요
7. git grep -nE "admin[0-9]{24}" → 0건이어야 함 (관리자 주소는 콘솔에만 — B-7)
8. npm audit --audit-level=high → high 이상 0건 (RIDM Apps 빌드 검사도 같은 것을 본다. 무엇이 나오든 package.json 은 고치지 않는다 — audit 결과를 완료 보고에 그대로 적고 책 개정(get-book-code --force)을 기다린다 — 8-0)
9. npx vitest run → 전부 통과 (7-5)
10. 7-3-1 점검표의 명령을 전부 실행해 결과를 보여 주세요 (입력·한도·격리)
7-3 이 "비밀값이 새지 않았나"를 본다면, 이 표는 "내 서버의 문이 열려 있지 않나"를 봅니다. 책 코드를 그대로 쓰면 전부 통과합니다. AI 가 라우트를 새로 만들었거나 고쳤다면 배포 전에 반드시 다시 돌립니다 — 새 라우트가 로그인 확인이나 입력 검사를 빠뜨리는 것이 가장 흔한 사고입니다.
| 점검 | 왜 필요한가 |
|---|---|
| 1. 로그인 확인 없는 라우트가 없다 | 확인이 빠진 라우트는 누구나 쓰는 문입니다. 가입·로그인처럼 원래 열려 있어야 하는 것만 예외 |
| 2. 브라우저 본문을 통째로 쓰지 않는다 | 통째로 쓰면 user_id 같은 값을 끼워 넣을 수 있습니다. 칸을 골라 검사한 값으로 다시 만들어야 합니다 |
| 3. 본문 크기·글자 길이·타입 상한이 있다 | 큰 본문·이상한 타입이 서버와 DB 를 붙잡습니다 |
| 4. 모든 메모 SQL 에 user_id 조건이 있다 | 이 조건 하나가 회원 사이의 벽입니다 |
| 5. 호출 횟수 제한이 있다 | 읽기·쓰기 모두 분당 제한과, 쌓이는 양(회원당 200개·하루 50개) 상한이 요금·용량 폭탄을 막습니다 |
| 6. 바꾸는 요청은 같은 사이트에서만 | 다른 사이트에 숨겨 둔 폼이 로그인한 회원의 브라우저로 요청을 보내는 것을 막습니다 |
| 7. 오류 응답에 안쪽 정보가 없다 | 오류 문구·스택이 그대로 나가면 서버 구조가 공개됩니다 |
| 8. CORS 를 열지 않았고, 쿠키는 HttpOnly·Secure·SameSite | 다른 사이트가 응답을 읽거나 세션 쿠키를 화면 스크립트가 읽는 것을 막습니다 |
| 9. 화면에 HTML 을 그대로 꽂는 코드와 SQL 이어 붙이기가 없다 | 메모 내용이 화면 스크립트나 SQL 로 실행되는 것을 막습니다 |
[프롬프트]
7-3-1 점검표를 실행하고 각 항목의 실제 명령 결과를 보여 주세요. 기대와 다르면 고치기 전에 먼저 알려 주세요:
1. find app/api -name route.ts -exec grep -L getSessionUser {} + → 가입·로그인·로그아웃·복구·health 5개만 나와야 함 (그 밖의 파일이 나오면 그 라우트가 비로그인에도 열려 있다는 뜻)
2. grep -rn "req.json()\|request.json()" app lib → 0건이어야 함 (본문은 readJsonCapped·readStrings 로만). 이어서 grep -n "parseNoteInput" app/api/notes/route.ts "app/api/notes/[id]/route.ts" → 새 메모·고치기 모두 거쳐야 함
3. grep -rn "readJsonCapped\|readStrings" app lib → 메모 만들기·고치기와 가입·로그인·복구·탈퇴 라우트가 나와야 함
4. grep -n "user_id" lib/notes.ts → 메모를 읽고·고치고·지우는 문장마다 user_id = $ 조건이 보여야 함 (하나라도 빠졌으면 그 문장이 남의 메모에 닿는다)
5. grep -rn "allow(" app/api → 메모 읽기·만들기·고치기·지우기와 가입의 제한이 보여야 함
6. grep -rln "sameOrigin\|jsonFromHere" app/api → 메모 라우트 2개와 가입·로그인·로그아웃·복구·탈퇴가 나와야 함
7. grep -rnE "\b(e|err|error)\.(message|stack)" app lib → 0건이어야 함
8. grep -rn "Access-Control" app lib next.config.ts → 0건이어야 함. 이어서 grep -n "httpOnly" lib/get-session-user.ts → secure: true, sameSite: 'lax' 와 함께 나와야 함
9. grep -rn "dangerouslySetInnerHTML=\|innerHTML *=" app components → 0건이어야 함. 이어서 grep -rn '${' lib | grep -E "SELECT|INSERT|UPDATE|DELETE" → ${COLUMNS} 처럼 책이 정한 상수만 보이고 회원이 보낸 값은 없어야 함
결제·웹훅을 붙일 때: 이 책의 코드에는 결제가 없어서 웹훅 검사도 없습니다(9-2). PG의 웹훅을 받는 라우트를 만든다면 위 표에 한 줄을 더하세요 — 요청마다 PG 의 서명을 검증하고(실패하면 처리 없이 거절), 같은 알림이 두 번 와도 한 번만 처리합니다. 서명 검증이 없는 웹훅은 "결제 완료"를 누구나 흉내 낼 수 있는 문입니다.
이 책의 코드도 처음에는 값싼 AI 가 데모를 만들다 문이 열린 채로 나온 적이 있습니다. AI 는 "돌아가는 코드"를 먼저 만들고 문단속은 빠뜨리기 쉽습니다 — Claude·GPT·Gemini·Cursor·저가 모델 어느 것이든 마찬가지입니다. 그래서 서버 코드를 새로 만들거나 고치게 할 때는 요청 끝에 아래 지시문을 같이 붙여 넣습니다. 어느 모델에서도 통하도록 짧은 규칙만 적었습니다.
[보안 지시문 — 서버 코드를 만들거나 고칠 때 반드시 지킨다] 아래 규칙을 어기는 코드는 쓰지 마세요. 지킬 수 없는 규칙이 있으면 코드를 쓰기 전에 먼저 말해 주세요. 1. 로그인 먼저: 서버 라우트(app/api 아래)는 맨 처음에 로그인 세션을 확인하고, 없으면 바로 401 로 끝낸다. 예외는 가입·로그인·로그아웃·비밀번호 복구·health 뿐이다. 2. 바디를 그대로 넘기지 않는다: 요청 바디(또는 그 안의 객체)를 외부 API·DB 에 통째로 넘기지 않는다. 허용한 필드만 하나씩 골라 새 객체를 만든다. 모델·지시문·회원 id·가격처럼 서버가 정할 값은 브라우저가 보내도 쓰지 않는다. 3. 입력 검사: 바디는 크기 상한을 두고 읽는다. 필드마다 타입(문자열·숫자·배열)과 길이·범위를 서버에서 검사하고, 어긋나면 400. 배열은 항목 수와 항목 하나하나도 검사한다. 4. 비밀 키: API 키·비밀번호·DB 주소는 서버 환경변수에만 둔다. 이름에 NEXT_PUBLIC_ 금지, 'use client' 파일에서 process.env 금지, 코드·커밋·로그·응답에 실제 값 금지. 5. 호출 제한: 돈이 들거나 DB 에 쓰는 라우트는 회원별로 분당 제한과 하루 한도를 둔다. 넘으면 429. 한도 확인은 외부 API 를 부르기 전에 한다. 6. 웹훅: 결제 알림 같은 웹훅은 요청마다 보낸 곳의 서명을 검증하고, 틀리면 아무것도 처리하지 않고 거절한다. 같은 알림이 두 번 와도 한 번만 처리한다. 7. 오류 응답: 사용자에게는 짧은 문구만 보낸다. 에러 메시지·스택·SQL·외부 API 응답 원문은 응답에 넣지 않는다(서버 로그에만). 8. CORS 와 요청 출처: Access-Control-Allow-Origin: * 를 쓰지 않는다(CORS 헤더를 아예 넣지 않는다). 무언가를 바꾸는 요청(POST·PATCH·DELETE)은 같은 사이트에서 온 JSON 만 받는다. 세션 쿠키는 HttpOnly·Secure·SameSite. 9. SQL 에는 자리표시자($1, $2)로만 값을 넣고, 화면에 HTML 을 직접 꽂지 않는다(dangerouslySetInnerHTML·innerHTML 금지). 10. 결과 재사용: 계산형·풀이형 API 결과(만세력·자미두수·궁합·네이탈·꿈해몽·오늘의 사주·평생 사주 등)는 DB 에 저장하고 재사용한다. 같은 입력으로 반복 호출하지 않는다 — 다시 부르는 때는 출생 정보나 lang 이 바뀌었을 때뿐이다. 대화형(상담·채팅)은 매번 새로 생성되니 저장 대상이 아니다. 끝나면 무엇을 바꿨는지와 함께 7-3-2 검수 체크리스트의 실제 명령 결과를 보여 주세요.
AI가 만든 코드 검수 체크리스트 — 7-3-1 은 책 코드 전체를 정해진 기대값과 맞춰 봅니다. 이 체크리스트는 AI 가 이번에 새로 만들거나 고친 파일만 지시문대로 됐는지 봅니다. 배포 전에 7-3-1 과 함께 돌립니다.
[프롬프트] 7-3-2 검수 체크리스트를 실행해 주세요. 먼저 git status --short 로 이번에 새로 만들거나 고친 파일을 보여주고, 아래 명령의 <파일> 자리에 그 파일들을 넣어 하나씩 돌린 실제 출력을 그대로 보여 주세요. 정상과 다른 줄이 있으면 고치기 전에 먼저 알려주고, 고친 뒤 같은 명령을 다시 돌려 보여 주세요: 1. 로그인 먼저 — grep -n "getSessionUser" <바뀐 route.ts> → 라우트 함수마다 맨 앞에 있고 없으면 401 (가입·로그인·로그아웃·복구·health 만 예외) 2. 바디 그대로 넘기기 — grep -nE "req(uest)?\.json\(\)|\.\.\.(raw|body)" <파일> → 0건. 나오면 그 값이 fetch·sql 로 그대로 가는지 보고, 허용 필드로 새 객체를 만들게 고친다 3. 길이·타입 — grep -nE "typeof|\.length|Number\.is" <바뀐 route.ts> → 바디·쿼리에서 꺼낸 필드마다 검사가 보여야 함. 본문은 readJsonCapped (lib/notes.ts)·readStrings (lib/body.ts) 로만 읽는다 4. 비밀 키 — grep -n "NEXT_PUBLIC_\|process\.env" <파일> → NEXT_PUBLIC_ 은 0건, process.env 는 서버 파일(app/api·lib)에만. 'use client' 파일·components 에서는 0건 5. 호출 제한 — grep -nE "allow\(|PER_DAY|_MAX" <바뀐 route.ts> → 돈이 들거나 DB 에 쓰는 라우트마다 회원 id 로 센 제한이 보여야 함 (이 책: allow(분당)과 NOTES_PER_DAY·NOTES_MAX(하루·합계)) 6. 웹훅 서명 — grep -rniE "webhook|signature" app/api → 웹훅 라우트가 없으면 0건이 정상. 있으면 서명 검증 줄이 처리보다 먼저 와야 함 (9-2) 7. 오류·CORS — grep -nE "\b(e|err|error)\.(message|stack)|String\(e\)|Access-Control" <파일> → 0건 (console.error 로그 안은 괜찮고, 응답 json 안은 안 됨) "확인했다"는 말만으로는 통과가 아니다 — 명령과 출력을 같이 보여 주세요.
ADMIN_PASSWORD 를 덮어씁니다 — 저장하면 앱이 다시 시작되고 기존 관리자 로그인이 전부 풀립니다(B-7)node scripts/make-admin-path.mjs 로 새 값을 만들어 ADMIN_PATH 에 저장합니다(비밀번호도 같이 바꾸면 기존 관리자 로그인도 풀립니다)과거 커밋에 비밀값이 박혔다면 저장소를 지우고 새로 만드는 게 히스토리 세탁보다 빠릅니다(값은 이미 바꿨으니 급하진 않음). 새 저장소는 콘솔의 앱 상세에서 "변경"으로 다시 연결합니다.
두 브라우저를 번갈아 띄워 손으로 확인하는 방식은 재현이 안 되고 AI가 "확인했다"고 거짓 보고하기 쉽습니다. 이 절의 책 코드 파일(get-book-code 가 만든다)을 그대로 두고 npx vitest run으로 증명합니다. tests/auth.test.ts·tests/notes.test.ts·tests/db.test.ts·tests/admin.test.ts의 DB 부분은 진짜 PostgreSQL에 대고 확인하므로 TEST_DATABASE_URL을 줄 때만 돌고, 없으면 건너뜁니다(테스트용 빈 DB를 쓰세요 — 운영 DB 금지). 없으면 npm run check는 멈추지 않고 DB 테스트 미실행 경고를 찍고 건너뜁니다 — 그때 로그인·메모·내 테이블(lib/schema-extra.ts)은 실서버 smoke 로만 확인된 것이니 완료 보고에 "DB 테스트: 미실행(테스트 DB 없음)", 돌렸으면 "DB 테스트: 실행(skipped 0)"이라고 적습니다(PART 0 5-f).
테스트 DB: 5-3 에서 띄운 로컬 DB 를 그대로 씁니다(두 용도로 같이 써도 됩니다 — 테스트는 자기 스키마를 따로 만들고 지웁니다). 아직 없으면 5-3 의 한 줄 — docker run -d --rm --name notes-testdb -e POSTGRES_HOST_AUTH_METHOD=trust -p 127.0.0.1:54329:5432 postgres:17 (이름·앞쪽 포트는 예시, 바꿨으면 주소도 같이). docker ps 가 오류를 내면 Docker Desktop(또는 colima)을 먼저 켭니다 — 설치돼 있어도 꺼져 있으면 실패합니다. 테스트는 TEST_DATABASE_URL=postgresql://postgres@localhost:54329/postgres npm run check, 로컬 실행은 5-3 대로 .env.local + npm run build && npm run start 뒤 node scripts/smoke-live.mjs http://localhost:3000 --quick — 로그인 실패 제한(6회째 429)과 복구 코드 재설정도 이 스크립트가 확인하니 손으로 하지 않습니다. 끝나면 docker stop notes-testdb(컨테이너째 지워짐). Docker 가 없으면 새 계정·외부 DB 서비스를 만들지 말고 "미실행"으로 보고합니다.
vitest.config.ts — 프로젝트 루트코드 자리: cat vitest.config.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/notes.test.ts — 메모 API — 격리 404·Origin 403·비로그인 401·한도 400·50개씩·1분 30개·하루 50개·합계 200개·고치기 1분 60회 429·읽기 1분 120회 429·정지 회원 (실제 PostgreSQL)코드 자리: cat tests/notes.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/screens.test.tsx — 메모·탈퇴 화면이 부르는 곳 — 목록·POST·PATCH·DELETE·401 넘기기·탈퇴 확인코드 자리: cat tests/screens.test.tsx · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/auth.test.ts — 가입·로그인·세션·복구·같은 주소 JSON 만(403)·본문 4KB 상한·회원 탈퇴 (실제 PostgreSQL)코드 자리: cat tests/auth.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/failure-limit.test.ts — 실패 제한코드 자리: cat tests/failure-limit.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/rate-limit.test.ts — 요청 제한코드 자리: cat tests/rate-limit.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/db.test.ts — 내 테이블 실행·분리 (실제 PostgreSQL)코드 자리: cat tests/db.test.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
tests/test-db.ts — 테스트 DB 주소에 설정 더하기 (테스트 도우미)코드 자리: cat tests/test-db.ts · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
부록 A-2 의 tests/get-book-code.test.ts 와 B-7 의 tests/admin.test.ts 까지 합쳐 전부 통과해야 다음으로 갑니다(기준 앱: 테스트 DB 없이는 DB 가 필요한 테스트만 건너뛰고 나머지 전부 통과, 테스트 DB 를 붙이면 건너뜀 0 으로 전부 통과. vitest 가 찍는 "ESM syntax in a file loaded as CommonJS" 경고는 결과에 영향이 없으니 무시한다).
origin은 push 할 때 생깁니다). 8-1 의 push 뒤 git remote -v로 origin이 내 GitHub 저장소(https://github.com/{내계정}/my-notes-app.git)인지 확인하고, 그 URL을 결정서 맨 아래 "저장소:" 줄에 적습니다AI용 분할본 · 지도 4단계(보안·테스트) · 이 페이지 전에 읽을 페이지: PART 0 사용법·AI 시작 지침, 부록 A 스펙·코드 받기(A-2) · 코드 파일은 여기 없다 — 코드 자리의 줄을 따른다(폴더에 책 파일이 하나도 없는 처음이면 부록 A-2 의 curl 한 줄부터)