Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ
11 KiB
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/*.png78장 +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.tsx의isServiceVisible('saju')+notFound()제거.lib/service-visibility.ts의HideableService에서'saju'제거.app/api/admin/services/route.tsDEFAULT_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 해석 보유" — 하위 호환)
- 비로그인: 기본 해석(사주팔자·오행·대운 등 계산 기반)은 그대로 표시 + "AI 상세 해석(12항목)은 로그인하면 무료" CTA(
- 일일 제한(서버측 강제):
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/*.png78장 +card_back.png→public/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_readingsinsert(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 = 3kstDayStart(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 + 안내 문구(예시 해석 미제공, 데이터 오염 방지)