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의 질문부터 하세요]
{제품 결정서}
결정서 적용 규칙:
- 로그인은 항상 만든다: 아이디+비밀번호 가입(복구 코드 1회 표시)·로그인·로그아웃·복구 화면, 서버 세션, 비로그인 401, endUserId = 세션의 회원 id(브라우저 값 무시). 코드는 이 책 7-2의 [완전 코드] 그대로(lib/db.ts, lib/auth.ts, lib/get-session-user.ts, lib/failure-limit.ts, app/api/auth/*)
- 캐릭터: 결정서의 캐릭터를 lib/persona-list.ts 에 적는다(key·name·tone·intro·persona). 하나면 고정, 둘 이상이면 상담 화면에 고르기 버튼(components/Chat.tsx 가 이미 처리)
- 기억 "불필요": 상담 화면을 <Chat memory={false} /> 로 쓴다(/reading, 기억 배지·지난 대화 불러오기 없음). components/Chat.tsx·app/api/history/route.ts 는 책 그대로 둔다 — history 는 불리지 않을 뿐
- 화면 구조 "한 페이지형"이면 하단 탭 대신 섹션 스크롤, "단계형"이면 온보딩→운세 화면→상담 순 이동만 허용
- 출생 정보 처리(고정 — 4-0 표 7): 시간 모름이면 hour·minute 를 보내지 않고, 응답이 주지 않은 값(시주·상승궁·명반)을 짐작해 채우지 않는다 — 화면은 이 판 PART 5 화면 장의 안내 문구대로(만세력: 시주 자리는 안내 카드, 나머지는 응답의 년·월·일 3주 기준 값 그대로, birthHour.known 으로 판별). 자유채팅 systemPrompt 에는 "출생 시각: 미상, 추정하지 마라" 블록. HRUM API가 근사치를 내려주지 않으므로 "참고용 근사치" 옵션은 만들지 않는다. 타로만·꿈해몽만 판은 출생 정보를 받지 않는다
- 브라우저는 personaKey 만 보낸다. 프록시가 findPersona 로 말투·페르소나를 붙이고 목록에 없는 key 는 400
- 유료화 "나중에 유료": 결제 코드는 만들지 않는다(9-4 — 결제는 완성 뒤 PG사와 직접 계약해 따로 붙인다). 유료가 될 기능만 화면에서 분리
[앱 개요]
- 앱 이름: {앱이름}
- 컨셉: {한 줄 컨셉}
- 페르소나(캐릭터): {페르소나 블록 — 4-0 결정서의 페르소나 항목, 3-2 공식 기반}
- 디자인 방향: {디자인 방향} · 모바일 우선 · 한글 폰트 Pretendard (CDN)
[기술 스택 — 변경 금지]
- Next.js 15 (App Router, output: 'standalone') + TypeScript(strict, any 금지). 스타일은 app/globals.css 의 CSS 변수(디자인 토큰) — Tailwind 를 써도 되지만 토큰은 한 곳에
- 런타임 의존성은 next, react, react-dom, pg, react-markdown, remark-gfm 뿐 (8-0 package.json). 외부 서비스 SDK(인증·DB·캐시·결제·분석)를 설치하지 않는다
- 배포: 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. 파일시스템에 데이터 저장 금지
- 상담 기억·대화 이력: FLOAT 서버(/reading/memory, /memory/{endUserId}/messages). 대화 기록 테이블을 만들지 않는다
[보안 규칙 — 절대 위반 금지]
1. FLOAT_API_KEY 는 서버 환경변수로만 사용. 클라이언트 코드·NEXT_PUBLIC_ 접두사 금지. 이 앱에는 NEXT_PUBLIC_ 변수가 하나도 없다
2. HRUM API 호출은 브라우저에서 직접 하지 않고 서버 라우트만 거친다: 계산형·상담 경로(manseryeok·compat·personality-type·ziwei-chart·astrology·tarot·dream·reading·reading/memory 중 이 판 lib/app-topics.ts 의 주제에 속한 것만 — 5-0)는 app/api/float/[...path]/route.ts 프록시, 대화 이력은 app/api/history/route.ts, chat/completions 는 app/api/chat/route.ts(6-3), reading/report 는 부록 B-6 리포트를 붙일 때만 만드는 app/api/report/route.ts(기본 뼈대에는 없음 — 프록시 allowlist 에도 넣지 않는다). 모두 lib/float-client.ts 로만 상류를 부른다
3. .gitignore 에 .env*와 그 다음 줄에 !.env.example(예외) 포함 + 키 값을 비운 .env.example 커밋
4. 요청 제한과 중복 합치기는 lib/dedup.ts 하나로(메모리 — 앱은 컨테이너 1개). 과금 라우트는 requestId(UUID) 없으면 400, 결과 불명은 409 OUTCOME_UNKNOWN 으로 자동 재호출 금지
5. 모든 FLOAT 라우트와 원국·이력 라우트는 getSessionUser(req) 로 로그인을 확인하고 비로그인은 401
6. 세션 쿠키는 서버 응답의 Set-Cookie 로만(평범한 이름 session, HttpOnly·Secure·SameSite=Lax·Path=/). RIDM Apps 관문이 브라우저에는 __Host-session 으로 저장하고 앱에는 session 으로 돌려준다. document.cookie 로 세션을 다루지 않는다
7. 비밀번호·복구 코드는 scrypt(무작위 salt) 해시만 저장, 세션 토큰은 sha256 만 저장. 로그인·복구 실패는 lib/failure-limit.ts 로 제한
8. 프록시 입력 검증: 허용 경로 allowlist, JSON 객체만, 경로별 허용 필드만 골라 타입(숫자·글자)과 길이를 확인한 뒤 새 객체로 다시 조립해서 전달(브라우저 본문을 통째로 넘기지 않는다 — answers 도 항목별로 재조립), 본문 16KB 상한, 상류 fetch 15초 timeout → 504. 시간 모름은 hour·minute 키를 생략한 채 그대로 전달하고 12:00 을 채우지 않는다
9. 기억의 열쇠(reading/memory 의 endUserId, chat 의 user, /memory/{id}/messages 의 id)는 브라우저 값을 절대 쓰지 않는다 — 서버가 세션의 회원 id 로 강제
10. 원국 소유권: 원국은 birth_profiles 에 user_id 로 저장하고 모든 쿼리는 WHERE user_id = 세션 회원 id. 다른 회원의 id 를 받아 원국을 조회/수정하는 API를 만들지 않는다
11. 크레딧이 드는 모든 POST 는 클라이언트가 requestId 를 보내고(없거나 형식이 틀리면 400), 서버는 lib/dedup.ts 로 회원+경로+requestId 를 상류 호출 전에 선점해 동시·중복 수신을 한 번의 호출로 합친다. FLOAT 는 X-Idempotency-Key 를 감사 로그용으로만 저장할 뿐 중복을 걸러주지 않으므로 이 서버측 선점이 이중 과금을 막는 유일한 방어선이다
12. 캐릭터는 personaKey 로만 고른다 — 페르소나 문장은 서버의 lib/persona-list.ts 에서만 붙이고, 브라우저가 보낸 tone·persona 문장은 버린다(7-2 프록시)
13. 자유 채팅의 messages 길이 제한은 role별로 분리 — role:user 500자 이하, role:assistant(과거 이력) 4,000자까지
14. npm audit 결과(RIDM Apps 빌드 검사가 같은 것을 운영자에게 보여준다): package.json 은 고치지 않는다 — audit 결과를 완료 보고에 그대로 적고 책 개정(get-book-code --force)을 기다린다(8-0)
15. 크레딧이 드는 프록시 POST 와 원국 저장 POST 는 lib/body.ts 의 jsonFromHere 로 이 앱 화면에서 온 JSON 요청만(아니면 403). 가입·로그인·복구 본문은 readStrings 로만 읽는다(4KB 상한)
16. 회원당 하루 한도(DAILY_LIMIT)는 usage_log 에서 한국 날짜로 센다 — 모든 과금 경로를 usage_log 에 기록하고, 한도 확인은 상류를 부르기 전에 한다
17. 새 라우트를 만들거나 고치면 배포 전에 7-3-1 점검표와 7-3-2 검수 체크리스트를 다시 통과시킨다. 서버 코드는 7-3-2 보안 지시문을 지켜 쓴다
[HRUM API 계약 — https://api.hrum.ai]
인증 헤더: X-Partner-Api-Key (프록시에서만 부착)
응답 공통: **chat/completions 를 제외한 모든 엔드포인트**가 `{ success, data }` 래핑 — 실데이터는 항상 `data` 안. chat/completions 만 OpenAI 형식 raw (`choices[0].message.content`)
1) 화면 API — 이 판 PART 5 의 화면 장(요청 body·응답 필드)과 이 상자 아래 "화면 API 계약" 상자에 있다. 만세력·자미두수 명반·네이탈·타로·꿈해몽 중 이 앱의 주제(lib/app-topics.ts)만 쓴다
2) AI 사주 리딩 POST /api/v1/partners/v1/reading
req { year, month, day, hour?, minute?, gender, question, tone?: "warm"|"casual"|"formal", only?: "saju"|"ziwei"|"astro"|"tarot"|"dream", tarot?: { spread, seed }, dream? }
only = AI 하나만(6-2 topic). tarot·dream 은 생년월일 없이 그 입력만, ziwei 는 시각 있는 원국 필수. 1회 = 토큰 값 + 그 주제 1회 값(saju 는 토큰 값만)
res.data { interpretation, usage:{inputTokens,outputTokens}, costPoints } (+ only·modulesUsed·moduleFees, tarot 이면 뽑힌 카드 tarot)
3) 기억형 상담 POST /api/v1/partners/v1/reading/memory
req = reading + endUserId (필수, 같은 값 = 기억 유지)
res.data = reading + memory:{ used, summaryInjected, notesInjected, tags[] }
4) 자유 채팅 POST /api/v1/partners/v1/chat/completions ※ OpenAI 호환 raw 응답
req { model: "ridm/gpt-5.6-terra", user: endUserId(서버가 세션에서 정한 값), systemPrompt, messages:[{role,content}] }
res { choices:[{message:{content}}], usage, costWon }
5) 대화 이력 GET /api/v1/partners/v1/memory/{endUserId}/messages?limit=20&offset=0 ※ 크레딧 차감 없음(요청 제한은 소모)
res.data { endUserId, messages:[{id, role, content, tags[], createdAt}] (한 페이지 안에서는 오래된 순), offset, limit, total }
아직 질문한 적 없는 회원은 404 END_USER_NOT_FOUND — 빈 대화로 처리한다 (app/api/history/route.ts)
[화면 명세]
1. 가입(/signup)·로그인(/login)·복구(/recover): 7-2 [완전 코드] 그대로에 디자인만 입힌다. 가입 직후 복구 코드를 한 번만 보여주고 "적어뒀어요" 체크 뒤 진행. 로그아웃 버튼은 헤더 또는 설정 안
2. 온보딩(원국 입력): 생년월일·태어난 시간(모름 체크 가능)·성별 → POST /api/profile (components/ProfileForm.tsx). 이미 입력한 회원은 건너뛰고, "생년월일 수정"으로 다시 열 수 있다. 타로만·꿈해몽만 판은 온보딩이 없다(가입하면 바로 화면)
3. 운세 화면: 이 판 PART 5 의 화면 장 그대로 — 응답을 그 장의 정규화 함수(lib/*.ts)로 바꾼 뒤 그 장의 컴포넌트로만 렌더한다(만세력 ManseryeokDetail · 자미두수 ZiweiChart · 별자리 NatalChart · 타로 TarotDraw · 꿈해몽 DreamSearch 중 이 판의 것). 응답에 있는 만큼만, 없는 값은 지어내지 않는다. 만세력은 결정서 "화면 표시"에 따라 펼쳐보기, 시간 모름이면 시주 카드 대신 "출생시간을 알려주면 시주까지 볼 수 있어요" 안내
4. 상담: components/Chat.tsx(6-2) — 화면마다 topic(그 주제의 AI, 6-2)으로. 기억 "필요"면 reading/memory + 끝없는 대화(20개씩) + memory.used 배지, "불필요"면 <Chat memory={false} />(reading). 말풍선, 로딩 중 입력·칩 비활성화, 추천 질문 칩 3개, 대화 밖 오류 표시, 결과 불명 시 같은 requestId 로 재확인, 입력 500자·aria-label, AI 답변은 components/AssistantMessage.tsx 로만
5. 공통: 결정서 화면 구조대로(탭형이면 하단 탭 화면|상담), 라이트·다크 모두 글자가 잘 보일 것, 각 API 에러는 사람 말로("크레딧이 부족해요" 등)
6. 참고 구성(한 페이지형 예시): 아래 부록 A-2 의 app/page.tsx — 로그인 안 됨이면 소개+가입/로그인, 로그인됨이면 원국 입력(받는 판만) 또는 이 판의 화면+상담
[완료 기준 — 스스로 실행해 증명할 것]
- 계정 게이트: FLOAT 로그인·API 키 발급 완료, (결정서에서 GitHub 를 고른 경우) GitHub 가입·Private 저장소 준비(제 컴퓨터 git 만이면 8-1 에서). 비밀값 자체는 출력하지 않고 사용자의 "저장 완료" 신호와 환경변수 존재 여부만 확인
- 책 코드 받기: node scripts/get-book-code.mjs 출력 "책과 다름 0" (모든 [완전 코드] 파일을 손으로 옮기지 않고 이 스크립트로 만든다) → 바로 git init -b main(커밋·push 는 8-1)
- npm run check → 끝까지 통과(책 코드 대조 0 · 빌드 에러 0 · 테스트 전부 통과 · npm audit high 이상 0건 — 나오면 package.json 은 고치지 않고 결과를 보고해 책 개정을 기다린다)
- [완전 코드] 대조: node scripts/verify-book-code.mjs → "불일치 0개" 출력(종료 코드 0). 이 스크립트도 책의 [완전 코드](부록 A-2)다. 불일치가 나오면 그 파일을 책 원문으로 되돌린다 — 타입 에러가 난다고 책 코드를 고치지 말고 tsconfig.json·package.json 이 책과 같은지부터 본다
- git check-ignore .env.local → 경로 출력 / grep -r "float_live_" app/ components/ lib/ → 0건 / grep -r "NEXT_PUBLIC_" app lib components → 0건
- 로컬: 가입 → 복구 코드 표시 → 로그아웃 → 로그인 → 틀린 비밀번호 6회째 429 → 복구 코드로 재설정 → 옛 비밀번호 401·새 비밀번호 200 (DATABASE_URL 이 있는 로컬 DB 로)
- 이 판 PART 5 화면 장의 완료 기준(실호출 결과가 응답 JSON 과 같음 · 시각 모름 처리 · 390px 가로 넘침 0)을 전부 통과
- 기억 "필요": 같은 회원으로 5번 넘게 질문 → 5번째부터 memory.used true / 로그아웃 → 재로그인 뒤 대화 이력 그대로 / 다른 회원은 앞 회원의 대화가 안 보임 / 브라우저가 임의 endUserId 를 보내도 서버가 회원 id 로 바꿔 보냄(7-5 테스트)
- 시간 모름 원국으로 상담 → 답변이 출생 시각을 모른다고 밝히고 정오 등으로 단정하지 않음 (타로만·꿈해몽만 판은 해당 없음)
- AI 답변에 **굵게**·목록·빈 줄이 있는 실제 응답 1건이 말풍선에서 <strong>·<ol>/<ul>·문단으로 보이고 별표가 남지 않음 + 390px 가로 넘침 0
- 프록시에 null·배열·허용 외 경로·requestId 누락을 보내면 400/404, 비로그인은 401
- 같은 requestId로 두 번 요청 → FLOAT는 한 번만 불림(7-5). 모든 프록시 호출은 lib/api.ts callProxy 로만
- 목록에 없는 personaKey 로 호출 시 400 (tests/proxy.test.ts) · 캐릭터가 둘 이상이면 상담 화면에 고르기 버튼이 보이고 고른 캐릭터 말투로 답이 옴
- 로컬: node scripts/smoke-live.mjs http://localhost:3000 --quick → 실패 0개
- RIDM Apps 에 올려(PART 8) 첫 배포 승인 뒤 node scripts/smoke-live.mjs https://이름.ridm.ai → 실패 0개 (첫 줄의 실행 중 커밋 = 마지막 커밋이어야 배포된 것)
- README.md 에 RIDM Apps 배포 순서(환경변수 FLOAT_API_KEY, DATABASE_URL 은 플랫폼 주입)와 로컬 실행 방법 기재
[HRUM API 계약 — 만세력 · 사주 리포트]
1) 만세력 POST /api/v1/partners/v1/manseryeok
req { year, month, day, hour?, minute?, timezone: "Asia/Seoul", gender: "male"|"female" }
**wonkuk 4기둥 공통 구조**: year·month·day·hour 네 기둥이 전부 같은 필드 세트를 가짐 — { stemHanja(천간 한자), branchHanja(지지 한자), ohaeng, stemSipsin, branchSipsin, jijanggan[](지장간), ungSung(운성), napEum(납음), majorSinsal[], minorSinsal[] }. **일주(day)도 십성 필드가 있으며** day.stemHanja+branchHanja 가 일간·일지. **hour 를 생략하면 API는 정오로 가정하지 않고 시주 자체를 제외한다** — wonkuk.hour: null, trueSolarTime: null, ohaengBalance·branchRelations·cheonEulGwiIn 은 년·월·일 3주 기준. yongshin·strength·gyeokguk·ilganSummary·seun 은 제거되지 않고 년·월·일 3주(6글자) 기준 값으로 온다. 최상위에 birthHour: { known:false, hourPillar:null, basis:"three_pillars", excludedFields:[](항상 빈 배열 — 판별에 쓰지 말 것), note } 가 추가되므로, **앱은 birthHour.known 값 하나로 시간 미상 여부를 판단**하면 되고 별도 클라이언트 상태로 정오 기본값과 실제 입력을 구분할 필요가 없다. hour 를 보낸 요청의 응답은 이전과 완전히 동일하고 birthHour 필드가 없다.
실제 응답 예시 (요약 — 1999-07-21 10:00 여성):
{ "success": true, "data": {
"wonkuk": { "day": { "stemHanja": "甲", "branchHanja": "戌", "ohaeng": "목",
"stemSipsin": "비견", "branchSipsin": "편재", "jijanggan": ["辛","丁","戊"], ... },
"year": {...}, "month": {...}, "hour": {...} },
"ohaengBalance": { "목": 2, "화": 1, "토": 3, "금": 1, "수": 1 },
"ilganSummary": "...", "strength": { "result": "중화", "score": ... },
"gyeokguk": { "name": "...", "summary": "..." },
"daeun": [ { "startAge": 4, "endAge": 13, "stemHanja": "...", "isCurrent": false }, ... ]
} }
res.data { wonkuk:{year,month,day,hour:{stemHanja,branchHanja,ohaeng,stemSipsin,branchSipsin,jijanggan[],ungSung,napEum,majorSinsal[],minorSinsal[]}}, ohaengBalance:{목,화,토,금,수}, ilganSummary, strength:{result,level,score}, gyeokguk:{name,summary}, daeun:[{startAge,endAge,stemHanja,branchHanja,ohaeng,isCurrent,stemSipsin,branchSipsin,sinsal[]}], seun:[{year,stemHanja,branchHanja,grade,isCurrent}], yongshin, branchRelations[], gongmangDetail, cheonganDetail }
3.5) 사주 리포트 POST /api/v1/partners/v1/reading/report
req { year, month, day, hour?, minute?, gender, tone?, persona?(최대2000자), sections?(최대12개) }
res.data { report(섹션별 마크다운 전문), sections[], sajuSource, usage, costPoints, model }
※ 사주 계산·주입은 서버가 자동 수행 — 만세력을 따로 부를 필요 없음
[HRUM API 계약 — 자미두수 명반]
POST /api/v1/partners/v1/ziwei-chart
req { year, month, day, hour(필수), minute?, gender, referenceDate?: "YYYY-MM-DD"(대한·유년 기준일, 기본 오늘), lang?: ko|en|ja|th }
hour 가 없으면 400 birth_time_required(과금 없음) — 부르지 않는다
res.data { lang, referenceDate, clock, chart: { solarDate, lunarDate{year,month,day,isLeapMonth}, yearGanzhi, yearBasisNote, fiveElementsClass{key,label}, soulPalaceBranch, bodyPalaceBranch, palaces[12]{key,label,branch,stem,isBodyPalace,majorStars[],minorStars[],decadal{from,to,isCurrent}}, mutagens[]{type,label,starLabel,palaceLabel}, currentDecadal, yearly }, notes{lateZiNotice,approximateTimeNote} }
₩50/호출 · 같은 입력이면 같은 결과(저장·재사용)
[HRUM API 계약 — 점성술 토탈]
POST /api/v1/partners/v1/astrology
req { birthDate: "YYYY-MM-DD", birthTime?: "HH:mm", birthTimePrecision?: exact|approximate|unknown, birthLatitude?, birthLongitude?, birthTimezoneOffset?(분, 기본 540),
houseSystems?: [placidus|whole_sign|equal|porphyry](생략 = 4종 전부), minorAspects?(기본 false), orbs?: leo1928(기본)|heindel1919, lang?: ko|en|ja|th,
timing?: { orbDegrees(필수 0.5~10), at?(생략 = 지금), progressions?(기본 true), yearLength(진행 시 필수 tropical|julian) }, partner?: { 상대 출생 입력 } }
시각은 birthTimePrecision 을 함께 보내야 쓰인다(생략 = unknown, 정오 계산). 상승궁·중천·하우스 = 시각(exact|approximate) + 좌표가 다 있을 때만(없으면 angles null + anglesOmitted)
res.data { natal: { bodies[10]{ id, longitude, sign, signKey, signName, degreeInSign, speed, retrograde, dignities[], house{체계: 번호} }, nodes[], angles{ asc, mc, dsc, ic, ascSign, mcSign, dscSign, icSign, polar }|null,
anglesOmitted, houseSystems, houses{체계: { cusps[12], bodies }}, housesExcluded[], aspects[]{ a, b, type, key, orb, strength, applying, outOfSign }, dignities, features[], moonSignUncertain },
timing?(timing 을 보냈을 때), synastry?(partner 를 보냈을 때), readings{ dignities[], aspects[]: { key, text, lang, deviation? }, lang }, readingsUnavailable, costPoints }
₩50/호출(블록·풀이 문단을 붙여도 같음) · 같은 입력이면 같은 결과(저장·재사용 — timing 은 at 을 넣었을 때만 같은 결과)
[HRUM API 계약 — 타로]
POST /api/v1/partners/v1/tarot
req { count?: 1~10 | spread?: single|past-present-future|choice|celtic-condensed|future-prediction (둘 중 하나만), reversed?: boolean(기본 true), seed?: 1~200자, question?: 500자까지, lang?: ko|en|ja|th }
res.data { lang, count, spread{type,name,description,positions[]}|null, reversedAllowed, seed, cards[]{position,positionLabel,id,name,nameEn,arcana,reversed,keywords[],meaning,imageUrl}, cardBackUrl, question, costPoints }
AI 해석 없음 · 출생 정보 불필요 · 호출당 고정 요금(장수 무관) · 같은 seed + 같은 spread/count + 같은 reversed = 같은 카드
[HRUM API 계약 — 꿈해몽]
POST /api/v1/partners/v1/dream
req { dream: 1~1,000자(필수), feeling?: 300자까지, lang?: ko(한국어만) }
res.data { lang, mode: glossary|none, items[]{keywords[](기본 낱말 하나), score 0~1}(최대 40개, 두드러진 순) }
자체 꿈 상징 어휘집(낱말만, 뜻·길흉 없음) · AI 해석·풀이 문장 없음(뜻은 상담 AI 가 풀이) · 출생 정보 불필요 · ₩30/호출, 찾은 항목이 없으면 ₩0
마스터 스펙이 만드는 뼈대의 나머지입니다. 화면 구조를 탭형·단계형으로 골랐다면 app/page.tsx만 그 구조로 나누고, 나머지 파일은 그대로 씁니다.
components/ProfileForm.tsx — 원국 입력코드 자리: cat components/ProfileForm.tsx · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
app/page.tsx — 첫 화면 (소개 / 원국 입력 / 사주·자미두수·별자리·타로·꿈해몽 탭 + 탭마다 상담)코드 자리: cat app/page.tsx · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서대로 배치·모양만 · 원문(사람용 — AI 는 열지 않는다)
app/layout.tsx — 레이아웃코드 자리: cat app/layout.tsx · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서대로 배치·모양만 · 원문(사람용 — AI 는 열지 않는다)
app/globals.css — 디자인 토큰 (결정서 13번으로 교체)코드 자리: cat app/globals.css · 고쳐도 되는 파일(5-b) — 화면 파일 — 결정서대로 배치·모양만 · 원문(사람용 — AI 는 열지 않는다)
tsconfig.json — TypeScript 설정코드 자리: cat tsconfig.json · 없거나 verify 가 "다름"이면 node scripts/get-book-code.mjs · 손으로 고치지 않는다 · 원문(사람용 — AI 는 열지 않는다)
도구 스크립트 3개 — 코드 받기·대조·실서버 점검. 앱 기능이 아니라 AI 가 실수 없이 만들고 스스로 확인하게 해 주는 장치입니다. get-book-code.mjs 는 콘솔에도 같은 파일이 있어 빈 폴더에서 curl -sO https://console.hrum.ai/get-book-code.mjs && node get-book-code.mjs && rm get-book-code.mjs 로 시작합니다(책 파일을 첨부한 보조 경로라면 node get-book-code.mjs sajuEbook.html 뒤 rm get-book-code.mjs). 끝나면 바로 git init -b main 을 합니다 — 7-3 점검과 개정 맞추기(git status)가 git 을 쓰고, 커밋·push 는 8-1 에서 합니다. 첫 실행 뒤 루트의 사본은 지웁니다 — 그 뒤로는 scripts/get-book-code.mjs 만 쓰고(스스로 최신판으로 바뀝니다), 루트 사본이 남아 있으면 실행을 거부하고 지우는 명령을 알려 줍니다. 두 스크립트는 실행할 때마다 지금 공개된 책을 기준으로 삼습니다 — 책이 개정돼 대조에 "다름"이 뜨면 세 줄로 최신 책 코드에 맞춥니다(수정 허용 파일은 건드리지 않고, package.json 은 name 만 남기고 책판으로): ① git status 가 깨끗한지 확인(아니면 먼저 커밋) ② curl -s https://console.hrum.ai/get-book-code.mjs -o scripts/get-book-code.mjs ③ node scripts/get-book-code.mjs --force. 되돌린 파일은 "되돌림"으로 한 줄씩 찍히고 전 내용은 옆의 <파일>.orig 에 남으니(이미 있으면 <파일>.1.orig… — 옛 사본을 덮지 않음, 커밋되지 않음), 일부러 고쳤던 파일이 있으면 그 사본이나 git diff 로 사라진 내용을 확인합니다. smoke-live.mjs 는 1회에 크레딧 약 ₩100~150(화면 API 각 1회 + 기억형 상담 2회, --compat이면 궁합 ₩20 추가)과 앱의 가입 제한(같은 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 한 줄부터)