Files
jaengseung-made/docs/superpowers/specs/2026-07-02-phase2-saju-tarot-design.md

11 KiB

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 실재 — 복사해 사용. <img onError> 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.tsxisServiceVisible('saju') + notFound() 제거. lib/service-visibility.tsHideableService에서 '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.pngpublic/images/tarot/ 복사. <img onError> 텍스트 폴백 구현(카드명+영문명)

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 양쪽)

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 전환) — <img> + 폴백으로 시작

리스크·주의

  • 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 + 안내 문구(예시 해석 미제공, 데이터 오염 방지)