From 878c0fbf490df3c0c5f1b8e7d78f945aced9663b Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 2 Jul 2026 20:22:27 +0900 Subject: [PATCH] =?UTF-8?q?docs(phase2):=20=EC=82=AC=EC=A3=BC=20=EC=9E=AC?= =?UTF-8?q?=ED=99=9C=EC=84=B1=20+=20=ED=83=80=EB=A1=9C=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20=EC=84=A4=EA=B3=84=20=E2=80=94=20=EA=B3=B5=EA=B0=9C?= =?UTF-8?q?=C2=B7=EB=AC=B4=EB=A3=8C=ED=99=94=C2=B7=EC=9D=BC=EC=9D=BC?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=C2=B7web-ui=20=ED=8F=AC=ED=8C=85=20(WS1~4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ --- .../2026-07-02-phase2-saju-tarot-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-02-phase2-saju-tarot-design.md diff --git a/docs/superpowers/specs/2026-07-02-phase2-saju-tarot-design.md b/docs/superpowers/specs/2026-07-02-phase2-saju-tarot-design.md new file mode 100644 index 0000000..27020b1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-phase2-saju-tarot-design.md @@ -0,0 +1,128 @@ +# Phase 2 별도 서비스 — 사주 재활성 + 타로 신규 설계 + +- 날짜: 2026-07-02 +- 선행: Phase 0(정리)·Phase 1(외주 코어) main 머지 완료 +- 배경: 운영 비전 2축(별도 서비스)의 첫 구현. 사주는 숨김 해제·무료화, 타로는 web-ui(개인 사이트) 구현 구조를 이 repo에 포팅. + +## 결정 사항 (CEO 확정, 2026-07-02) + +| 결정 | 내용 | +|------|------| +| 노출 정책 | **공개** — 누구나 체험, **결과 저장·재확인만 로그인** | +| 사주 AI 무료화 | 회원 무료 + **일일 횟수 제한**(서버측 강제). 비회원은 계산 기반 기본 해석까지 | +| 타로 스펙 | **3카드 스프레드, web-ui 타로 구조 그대로 포팅**(카드 데이터·셔플·선택 UX·해석 스키마) | +| 마이페이지 | **5번째 탭 'AI 기록'**. 사주 경로 `/work/saju` 유지, 타로는 `/tarot` 신규 | +| 일일 제한 기본값 | 사주 AI 해석 **1회/일**, 타로 AI 인사이트 **3회/일** (상수 분리, 조정 가능) | +| 타로 AI | **Gemini** (기존 GEMINI_API_KEY 재사용, 사주와 동일 폴백 체인 패턴). tarot-lab(Claude)의 프롬프트·스키마를 포팅 | + +## 포팅 원본 (web-ui 조사 결과 — 2026-07-02 감사) + +- 카드: `web-ui/src/pages/tarot/data/cards.js` — 78장(메이저 22 하드코딩 + 마이너 56 프로그램 생성), 필드 `{id, slug, name, nameEn, arcana, element, suit?, rank?, keywords[], reversedKeywords[], meaningUpright, meaningReversed, symbols[], image}` +- 이미지: `web-ui/public/images/tarot/cards/*.png` 78장 + `card_back.png` 실재 — 복사해 사용. `` CSS 텍스트 폴백 패턴 유지 +- 셔플: Fisher-Yates + 카드별 독립 50% 역방향. 리딩 시 덱에서 20장만 부채꼴로 펼쳐 사용자가 3장 직접 선택(과거/현재/미래 position 순) +- AI 계약: `cards_reference`(카드 의미 텍스트 블록)와 `context_meta`(메이저 비율·원소 분포·정역 흐름)를 **프론트가 로컬 카드 데이터로 조립**해 전송 — 서버는 카드 DB 없음 +- interpretation_json 스키마: `{summary, cards[{position, card, reversed, interpretation, evidence{card_meaning_used, position_logic, category_lens}, advice}], interactions[{type: synergy|conflict|transition, between[], explanation}], advice, warning|null, confidence: high|medium|low}` — three_card는 interactions ≥1 필수 +- 파이프라인 견고성: strict JSON 프롬프트 + 파싱 폴백(코드블록 스트립) + 검증 실패 시 사유 주입 reroll 1회 +- 카테고리: `연애 / 일·커리어 / 관계 / 재물 / 건강 / 일반` +- interpret ↔ save 분리: 저장 실패해도 해석은 유지(`save_failed` 상태) +- 고아 컴포넌트(CardGrid, SpreadSlots)와 원카드·히스토리 페이지는 포팅 제외(YAGNI — 히스토리는 마이페이지 AI 기록 탭이 대체) + +## 워크스트림 4개 + +### WS1. 사주 재활성 (공개 + 무료화) + +- **가드 제거**: `app/work/saju/layout.tsx`의 `isServiceVisible('saju')` + `notFound()` 제거. `lib/service-visibility.ts`의 `HideableService`에서 `'saju'` 제거. `app/api/admin/services/route.ts` DEFAULT_SERVICES에서 saju 행 제거. `service_settings`에서 saju 행 DELETE(마이그레이션) +- **무료화(SajuAISection)**: Phase 0에서 넣은 "개편 준비 중" 안내와 `hasPaid` 게이트를 **로그인 게이트**로 교체: + - 비로그인: 기본 해석(사주팔자·오행·대운 등 계산 기반)은 그대로 표시 + "AI 상세 해석(12항목)은 로그인하면 무료" CTA(`/login?next=` 현재 경로) + - 로그인: AI 상세 해석 무료 생성. 이미 저장된 해석 있으면 즉시 표시(무제한) + - `hasPaid` 데이터 소스(orders 'saju_detail')는 제거하고 `user` 세션 유무로 대체. `saju_records` 저장 시 `is_paid: true` 유지(필드 의미: "AI 해석 보유" — 하위 호환) +- **일일 제한(서버측 강제)**: `app/api/saju/analyze/route.ts`에 ① 세션 인증 확인(401) ② `ai_usage_log`에서 user_id+service='saju'+오늘(KST) 카운트 ≥ `SAJU_DAILY_LIMIT`(=1)이면 429 `{ error: '오늘 사용량을 모두 썼습니다. 내일 다시 시도해주세요.' }` ③ 성공 시 usage 기록. 제한 상수는 `lib/ai-usage.ts`에 정의 +- 로또 번호 섹션(SajuFortuneSection)·`hasLottoSubscription`은 현행 유지(추후 별도 정리) + +### WS2. 타로 신규 (`/tarot`) — web-ui 구조 포팅 + +**데이터·유틸 (lib/tarot/)** +- `lib/tarot/cards.ts`: 78장 데이터 TS 포팅(타입 `TarotCard` 정의, 메이저 하드코딩+마이너 생성 로직 그대로). `SPREADS`(three_card만), `CATEGORIES`, `findCard(slug)` +- `lib/tarot/shuffle.ts`: `fisherYates`, `buildShuffle(deck, 20)` — 순수 함수(테스트 가능) +- `lib/tarot/reference.ts`: `buildReferenceBlock(picks)`, `buildContextMeta(picks)` — 순수 함수(테스트 가능) +- 단위 테스트: 78장 무결성(slug 중복 없음·필드 채움), 셔플(길이·중복 없음·원본 불변), reference 블록 형식 + +**에셋** +- `web-ui/public/images/tarot/cards/*.png` 78장 + `card_back.png` → `public/images/tarot/` 복사. `` 텍스트 폴백 구현(카드명+영문명) + +**UI (`app/tarot/page.tsx` + 컴포넌트)** +- 3-step 클라이언트 플로우(web-ui Reading 구조): ①질문(선택)·카테고리 선택 ②20장 부채꼴 뒷면에서 3장 클릭 선택(과거/현재/미래) ③결과 +- 결과 2탭: **카드 해석**(로컬 데이터 — 키워드·의미·상징, 비회원 포함 모두) / **AI 인사이트**(로그인+일일 제한 — summary·카드별 해석+evidence·interactions·advice·warning·confidence) +- 비로그인이 AI 탭 클릭 시: "로그인하면 AI 해석 무료(일 3회)" CTA +- 셔플은 `'use client'` + `useEffect` 초기화(hydration mismatch 방지) +- 저장: AI 해석 성공 시 자동 저장 시도(로그인 상태). 저장 실패해도 해석 표시 유지 +- 디자인: `--jsm-*` 토큰 라이트 재작성. gradient/blur/보라/이모지 금지. 카드 이미지는 그대로(에셋은 가드레일 대상 아님) + +**API** +- `POST /api/tarot/interpret`: body는 web-ui 계약 그대로(`{spread_type:'three_card', category, question, cards[{position,card_id,reversed}], cards_reference, context_meta}`). 서버: ①세션 인증(401) ②일일 제한 `TAROT_DAILY_LIMIT`(=3) 체크(429) ③Gemini 호출(사주와 동일 모델 폴백 체인) — tarot-lab SYSTEM_PROMPT 포팅(RWS 전통·데이터 우선·strict JSON·confidence 기준) ④JSON 파싱 폴백+스키마 검증(위 interpretation_json 스키마, three_card interactions ≥1) ⑤실패 시 사유 주입 reroll 1회 ⑥성공 시 usage 기록. 응답 `{ interpretation_json, model }` +- `POST /api/tarot/readings`: 로그인 필수. `{spread_type, category, question, cards, interpretation_json}` → `tarot_readings` insert(user_id, summary는 interpretation_json.summary 추출). 응답 `{ id, created_at }` +- `GET /api/tarot/readings`: 로그인 필수, 본인 것만 최신순(마이페이지 소비) + +**DB (마이그레이션 1개, 클라우드+NAS 양쪽)** +```sql +CREATE TABLE IF NOT EXISTS tarot_readings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + spread_type text NOT NULL DEFAULT 'three_card', + category text, + question text, + cards jsonb NOT NULL, + interpretation jsonb NOT NULL, + summary text, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE tarot_readings ENABLE ROW LEVEL SECURITY; +CREATE POLICY tarot_select_own ON tarot_readings FOR SELECT USING (auth.uid() = user_id); + +CREATE TABLE IF NOT EXISTS ai_usage_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL, + service text NOT NULL CHECK (service IN ('saju','tarot')), + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE ai_usage_log ENABLE ROW LEVEL SECURITY; +CREATE INDEX IF NOT EXISTS idx_ai_usage_user_day ON ai_usage_log (user_id, service, created_at); + +DELETE FROM service_settings WHERE id = 'saju'; +``` +(insert는 service role로 수행 — policy 불요. tarot_readings insert도 서버 admin client 경유) + +**사용량 유틸 (`lib/ai-usage.ts`)** +- `SAJU_DAILY_LIMIT = 1`, `TAROT_DAILY_LIMIT = 3` +- `kstDayStart(now?)`: KST 자정 기준 오늘 시작 시각(UTC ISO) 계산 — 순수 함수, 단위 테스트(KST 경계) +- `getTodayUsage(admin, userId, service)`: `ai_usage_log`에서 오늘 카운트 조회 +- `recordUsage(admin, userId, service)`: 사용 1건 기록 +- 호출 순서(각 API): 인증 → `getTodayUsage ≥ limit`이면 429 → AI 호출 → **성공 시에만** `recordUsage` (실패한 생성은 카운트하지 않음 — 동시 요청 레이스는 개인 서비스 규모에서 허용) + +### WS3. 마이페이지 'AI 기록' 탭 + +- `app/mypage/page.tsx`: Tab 타입에 `'ai'` 추가, 라벨 `AI 기록` (5탭) +- 콘텐츠: 사주 기록(`saju_records` 본인 — 세션 클라이언트 RLS 조회)과 타로 기록(`GET /api/tarot/readings`) 통합 최신순 리스트 + - 사주 카드: 생년월일시·성별 요약 + 해석 보유 뱃지 + "결과 다시 보기" 링크(`/work/saju/result?` 저장된 birth 파라미터로 재구성) + - 타로 카드: 날짜·카테고리·질문·뽑은 카드 3장 이름 + summary + 접이식 상세(advice·warning) +- 빈 상태: 사주/타로 바로가기 CTA + +### WS4. 진입점 + 문서 + +- TopNav LINKS: `{ href: '/work/saju', label: '사주' }`, `{ href: '/tarot', label: '타로' }` 추가 (5링크, 모바일 드로어 자동) +- CLAUDE.md: 핵심 IA에 사주(공개 전환)·`/tarot` 반영, 숨김 서비스 표에서 saju 제거, 사주 시스템 섹션의 "현재 숨김" 문구 갱신, 파일 구조에 tarot 추가 +- 검증: `npm test`(신규 tarot·ai-usage 테스트 포함) + `npm run build` + 가드레일 grep(신규 공개 파일) + 수동 확인 안내(사주 무료 플로우·타로 3카드 플로우·AI 기록 탭) + +## 범위 밖 + +- 타로 원카드("오늘의 카드")·전용 히스토리 페이지 — AI 기록 탭이 대체, 필요 시 추후 +- 사주 로또 섹션 정리, music 고도화(Phase 3) +- 사주 결과 재확인용 딥링크 고도화(birth 파라미터 재구성으로 충분) +- 카드 이미지 최적화(next/image 전환) — `` + 폴백으로 시작 + +## 리스크·주의 + +- `saju_records` 테이블은 이 repo 마이그레이션에 없음(클라우드 직접 생성) — AI 기록 탭의 사주 조회는 기존 result 페이지와 동일한 세션 클라이언트 쿼리 패턴 재사용. 스키마 확인은 구현 시 result/SajuAISection 코드 기준 +- Gemini strict JSON: 사주 analyze의 기존 파싱 패턴 + tarot-lab의 폴백·reroll 포팅으로 이중 방어. 검증 실패 2회 시 사용자에게 재시도 안내(usage 카운트는 성공 시에만) +- 카드 이미지 78장(수 MB)이 repo에 들어감 — Vercel/NAS 정적 서빙 문제없음, git 용량만 인지 +- GEMINI_API_KEY 미설정 환경(로컬)에서는 사주와 동일하게 예시 폴백 또는 503 — 타로는 503 + 안내 문구(예시 해석 미제공, 데이터 오염 방지)