랜딩(/tarot) + 오늘의 카드 + 3장 스프레드 + 히스토리 4 페이지. agent-office 확장으로 tarot_readings 테이블 + interpret/save/list/patch/delete 5 endpoint. Claude Sonnet 4.6 + evidence·interactions 기반 근거 해석 프롬프트. 켈틱 10장·카드 이미지 정식 매핑·텔레그램 push는 v2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
555 lines
22 KiB
Markdown
555 lines
22 KiB
Markdown
# Tarot Lab v1 — Design Spec
|
||
|
||
**작성일:** 2026-05-23
|
||
**상태:** 디자인 승인 완료, 구현 계획 작성 대기
|
||
**관련 자산:**
|
||
- `source/images/tarot_page/tarot_main_landing_page.png` (랜딩 시안)
|
||
- `source/images/tarot_page/tarot_card_select_page.png` (카드 선택 시안)
|
||
- `source/images/tarot_page/tarot_background.png` (정적 배경 폴백)
|
||
- `source/images/tarot_page/tarot_cards.png` (카드 콜라주 참고)
|
||
- `source/videos/tarot_main_background.mp4` (히어로 영상)
|
||
|
||
---
|
||
|
||
## 1. 목표와 배경
|
||
|
||
개인 웹 플랫폼에 라이더-웨이트(RWS) 기반 타로 리딩 기능을 추가한다. v1은 **오늘의 카드 / 3장 스프레드 / 리딩 히스토리·마이페이지** 3개 핵심 흐름을 한 번에 배포하고, AI 해석은 Claude Sonnet 4.6을 통해 **근거 기반(evidence)** 으로 생성한다. 켈틱 크로스 10장 스프레드와 카드 78장 정식 이미지 자산은 v2 분리.
|
||
|
||
### 비목표 (v2 이후)
|
||
- 켈틱 크로스 10장 스프레드
|
||
- 사용자가 제공할 카드 78장 정식 이미지 자산의 정식 매핑 (v1은 placeholder/CSS)
|
||
- 78장 의미 텍스트 완성본 (v1은 메이저 22 + 마이너 키워드만)
|
||
- 텔레그램 자동 push ("매일 오늘의 카드")
|
||
- 카드 78장 도감 화면
|
||
- 즐겨찾기 메모 편집 UI (백엔드 endpoint는 v1에 포함, UI는 v2)
|
||
|
||
---
|
||
|
||
## 2. 아키텍처
|
||
|
||
```
|
||
web-ui (React + Vite)
|
||
/tarot 랜딩 (히어로 영상 + 3-tier)
|
||
/tarot/today 오늘의 카드 (원카드)
|
||
/tarot/reading 3장 스프레드 (메인 인터랙션)
|
||
/tarot/history 마이페이지 (리딩 이력)
|
||
│
|
||
│ /api/agent-office/tarot/*
|
||
▼
|
||
agent-office (FastAPI 확장)
|
||
app/routes/tarot.py 4 endpoint
|
||
app/agents/tarot.py TarotAgent (Claude Sonnet 호출 + 응답 검증)
|
||
app/db.py tarot_readings 테이블 추가
|
||
│
|
||
▼ Anthropic API
|
||
Claude Sonnet 4.6
|
||
```
|
||
|
||
### 경계 결정 이유
|
||
- **카드 78장 메타데이터는 프론트 정적 JSON** — 자주 안 변하고 셔플·선택에 백엔드 호출 불필요. 라운드트립 절약.
|
||
- **AI 해석만 백엔드** — API key 보호 + 호출 로깅·검증·reroll 가능.
|
||
- **히스토리도 백엔드** — localStorage는 기기 의존, 사용자가 영속화 요구.
|
||
- **신규 컨테이너 없음** — agent-office 확장. nginx·docker-compose 변경 0건.
|
||
|
||
### Why agent-office인가
|
||
1. `ANTHROPIC_API_KEY` 이미 환경변수로 연결됨
|
||
2. Claude SDK + httpx 클라이언트 set up 완료
|
||
3. Agent FSM 패턴(idle→working→reporting)에 자연스럽게 맞음 — TarotAgent도 "리딩 수행" 작업으로 모델링
|
||
4. 텔레그램 봇 연결되어 있어 v2에서 "매일 오늘의 카드" push 확장 여지
|
||
|
||
---
|
||
|
||
## 3. 프론트 데이터 모델
|
||
|
||
### 정적 카드 데이터 (`web-ui/src/pages/tarot/data/cards.js`)
|
||
|
||
```js
|
||
export const TAROT_DECK = [
|
||
// Major Arcana 22장
|
||
{
|
||
id: 0,
|
||
slug: "the-fool",
|
||
name: "바보",
|
||
nameEn: "The Fool",
|
||
arcana: "major",
|
||
element: "air",
|
||
keywords: ["새로운 시작", "도약", "순수", "자유"],
|
||
reversedKeywords: ["무모함", "경솔함", "위험", "방향 상실"],
|
||
meaningUpright: "미지의 세계로 내딛는 첫걸음. 계산보다 직관과 신뢰로 시작하는 시기.",
|
||
meaningReversed: "준비 없이 뛰어들어 위험을 자초하거나, 두려움으로 첫걸음을 미루는 상태.",
|
||
image: null, // 사용자가 /images/tarot/cards/the-fool.png 추가 시 자동 매핑
|
||
},
|
||
// ... Major 21장 더
|
||
|
||
// Minor Arcana 56장
|
||
{
|
||
id: 22,
|
||
slug: "ace-of-wands",
|
||
name: "지팡이 에이스",
|
||
arcana: "minor",
|
||
suit: "wands",
|
||
rank: 1,
|
||
element: "fire",
|
||
keywords: ["창조의 불씨", "영감", "새로운 시작"],
|
||
reversedKeywords: ["지연", "동기 부족", "방향 상실"],
|
||
meaningUpright: "...",
|
||
meaningReversed: "...",
|
||
image: null,
|
||
},
|
||
// ... Minor 55장 더
|
||
];
|
||
|
||
export const SPREADS = {
|
||
one_card: {
|
||
id: "one_card",
|
||
name: "오늘의 카드",
|
||
positions: [{ idx: 0, label: "오늘" }],
|
||
},
|
||
three_card: {
|
||
id: "three_card",
|
||
name: "3장 스프레드",
|
||
positions: [
|
||
{ idx: 0, label: "과거" },
|
||
{ idx: 1, label: "현재" },
|
||
{ idx: 2, label: "미래" },
|
||
],
|
||
},
|
||
};
|
||
|
||
export const CATEGORIES = ["연애", "일·커리어", "관계", "재물", "건강", "일반"];
|
||
```
|
||
|
||
**v1 시드 데이터 작업량:**
|
||
- 메이저 22장: 정·역 키워드 + 정·역 의미 텍스트 완성 (필수)
|
||
- 마이너 56장: 정·역 키워드만 (필수) + 의미 텍스트는 짧은 요약 1문장씩 (v2에서 보강)
|
||
|
||
### 카드 이미지 자동 매핑 규칙
|
||
- 사용자가 `web-ui/public/images/tarot/cards/<slug>.png` 추가 시 자동 표시
|
||
- `cards.js`에서 `image: \`/images/tarot/cards/${slug}.png\`` 일관 패턴
|
||
- `onError` → CSS 카드 디자인 폴백 (그라데이션 보더 + 카드명 + 심볼)
|
||
|
||
---
|
||
|
||
## 4. 백엔드 데이터 모델
|
||
|
||
### tarot_readings 테이블 (`agent_office.db`)
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS tarot_readings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
created_at TEXT NOT NULL, -- UTC ISO8601
|
||
spread_type TEXT NOT NULL, -- 'one_card' | 'three_card'
|
||
category TEXT, -- '연애' | '일·커리어' | …
|
||
question TEXT, -- 사용자 입력 (NULL 가능)
|
||
cards TEXT NOT NULL, -- JSON: [{position, card_id, reversed}]
|
||
interpretation_json TEXT, -- Claude 응답 파싱 결과 전체
|
||
summary TEXT, -- interpretation_json.summary 빠른 조회용
|
||
model TEXT, -- 'claude-sonnet-4-6'
|
||
tokens_in INTEGER,
|
||
tokens_out INTEGER,
|
||
cost_usd REAL,
|
||
confidence TEXT, -- 'high' | 'medium' | 'low'
|
||
favorite INTEGER DEFAULT 0,
|
||
note TEXT
|
||
);
|
||
CREATE INDEX idx_tarot_created ON tarot_readings(created_at DESC);
|
||
CREATE INDEX idx_tarot_favorite ON tarot_readings(favorite, created_at DESC);
|
||
```
|
||
|
||
**저장 정책:**
|
||
- 모든 리딩은 자동 저장 (사용자가 "저장" 누르지 않아도). 사용자가 별도 액션 없이도 히스토리에서 확인 가능.
|
||
- `favorite` 토글 + `note` 편집은 별도 PATCH 호출
|
||
- 카드는 `card_id`(slug)만 저장 — 실제 이름·의미는 항상 프론트 데이터에서 조회 → 카드 데이터 수정이 과거 이력에 자동 반영
|
||
|
||
### interpretation_json 구조
|
||
|
||
```json
|
||
{
|
||
"summary": "전체 흐름 한 단락 (3~4문장)",
|
||
"cards": [
|
||
{
|
||
"position": "과거",
|
||
"card": "the-fool",
|
||
"reversed": false,
|
||
"interpretation": "이 위치에서 이 카드가 의미하는 바 (3~4문장)",
|
||
"evidence": {
|
||
"card_meaning_used": "참고 카드 정보에서 인용한 키워드·상징",
|
||
"position_logic": "왜 이 의미가 이 위치에 그렇게 적용되는지 (1~2문장)",
|
||
"category_lens": "카테고리 관점에서 부각되는 면 (1문장)"
|
||
},
|
||
"advice": "이 카드가 주는 짧고 구체적인 조언 (1문장)"
|
||
}
|
||
],
|
||
"interactions": [
|
||
{
|
||
"type": "synergy" | "conflict" | "transition",
|
||
"between": ["the-fool", "the-lovers"],
|
||
"explanation": "두 카드의 슈트·원소·정역방향 흐름 근거 (1~2문장)"
|
||
}
|
||
],
|
||
"advice": "3장(또는 1장) 종합 조언 (2문장)",
|
||
"warning": null,
|
||
"confidence": "high" | "medium" | "low"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. API 명세
|
||
|
||
### 5.1 `POST /api/agent-office/tarot/interpret`
|
||
AI 해석만 수행 (저장과 분리). 응답 받은 후 사용자가 별도 액션 없으면 자동 저장 호출.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"spread_type": "three_card",
|
||
"category": "연애",
|
||
"question": "다음 달 그 사람과의 관계는?",
|
||
"cards": [
|
||
{ "position": "과거", "card_id": "the-fool", "reversed": false },
|
||
{ "position": "현재", "card_id": "the-lovers", "reversed": true },
|
||
{ "position": "미래", "card_id": "ten-of-cups", "reversed": false }
|
||
],
|
||
"cards_reference": "## 1. 위치: 과거 | 카드: The Fool ...",
|
||
"context_meta": {
|
||
"major_minor_ratio": "2:1",
|
||
"element_distribution": { "air": 2, "water": 1, "fire": 0, "earth": 0 },
|
||
"orientation_flow": "upright→reversed→upright"
|
||
}
|
||
}
|
||
```
|
||
|
||
`cards_reference`와 `context_meta`는 프론트가 `cards.js`를 기반으로 빌드해서 전송. 백엔드가 카드 데이터를 따로 가지고 있을 필요 없음 (DRY).
|
||
|
||
**Response:** `interpretation_json` 구조 + 호출 메타.
|
||
```json
|
||
{
|
||
"interpretation_json": { /* 위 4절 구조 */ },
|
||
"model": "claude-sonnet-4-6",
|
||
"tokens_in": 712,
|
||
"tokens_out": 942,
|
||
"cost_usd": 0.0163,
|
||
"latency_ms": 5240,
|
||
"reroll_count": 0
|
||
}
|
||
```
|
||
|
||
**에러:**
|
||
- 400 — spread_type 미지원 / cards 길이 불일치 / cards_reference 빈 문자열
|
||
- 429 — Anthropic API rate limit
|
||
- 500 — Claude 호출 실패 (Retry-After 헤더 포함) 또는 reroll 2회 모두 실패
|
||
|
||
### 5.2 `POST /api/agent-office/tarot/readings`
|
||
리딩 저장. interpret 결과를 그대로 + 사용자 컨텍스트.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"spread_type": "three_card",
|
||
"category": "연애",
|
||
"question": "...",
|
||
"cards": [...],
|
||
"interpretation_json": { ... },
|
||
"model": "claude-sonnet-4-6",
|
||
"tokens_in": 712, "tokens_out": 942, "cost_usd": 0.0163,
|
||
"confidence": "medium"
|
||
}
|
||
```
|
||
|
||
**Response:** `{ "id": 123, "created_at": "2026-05-23T07:42:11Z" }`
|
||
|
||
### 5.3 `GET /api/agent-office/tarot/readings`
|
||
페이지네이션 + 필터.
|
||
|
||
**Query:** `?page=1&size=20&favorite=true&spread_type=three_card&category=연애`
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"items": [
|
||
{ "id": 123, "created_at": "...", "spread_type": "three_card",
|
||
"category": "연애", "question": "...", "cards": [...],
|
||
"summary": "한 줄 요약", "confidence": "medium", "favorite": 1 }
|
||
],
|
||
"page": 1, "size": 20, "total": 47
|
||
}
|
||
```
|
||
|
||
### 5.4 `PATCH /api/agent-office/tarot/readings/{id}`
|
||
즐겨찾기 토글·메모.
|
||
|
||
**Request:** `{ "favorite": true }` 또는 `{ "note": "메모" }`
|
||
|
||
### 5.5 `DELETE /api/agent-office/tarot/readings/{id}`
|
||
이력 삭제.
|
||
|
||
### Nginx 라우팅
|
||
변경 없음. 기존 `/api/agent-office/` 매칭에 흡수됨.
|
||
|
||
---
|
||
|
||
## 6. AI 프롬프트 설계
|
||
|
||
### SYSTEM_PROMPT
|
||
|
||
```text
|
||
당신은 라이더-웨이트(RWS) 타로 덱의 전통 상징체계에 정통한 타로 리더입니다.
|
||
사용자의 질문, 카테고리, 뽑힌 카드 각각의 정·역방향과 위치를 받아 근거 기반으로 해석합니다.
|
||
|
||
# 해석 원칙
|
||
1. 데이터 우선: "참고 카드 정보" 블록의 키워드·기본의미·상징만을 1차 근거로 사용.
|
||
외부 변형 의미·다른 덱 해석은 사용하지 않음.
|
||
2. 위치 의미 결합: 카드의 의미와 위치(과거/현재/미래 또는 오늘)를 명시적으로 결합해서 해석. evidence에 근거 기록.
|
||
3. 카드 간 상호작용 분석 (3장 스프레드):
|
||
- 시너지: 같은 슈트, 같은 원소, 메이저 비율, 정·역 흐름
|
||
- 충돌·전환: 슈트 충돌(컵-소드, 완드-펜타클), 정→역 전환, 메이저↔마이너 전환
|
||
4. 자기 성찰 톤: 운명론 단정 금지. "…할 가능성이 있어 보입니다" 같은 표현.
|
||
5. 카테고리 컨텍스트: 동일 카드라도 카테고리에 따라 강조점이 달라야 함.
|
||
6. 질문 직접 응답: 사용자 질문을 evidence·advice에서 인용·반영.
|
||
|
||
# 응답 형식 (strict JSON only — 코드블록 없이 raw JSON)
|
||
{
|
||
"summary": "전체 흐름 한 단락 (3~4문장)",
|
||
"cards": [
|
||
{
|
||
"position": "<위치 라벨>",
|
||
"card": "<card_id>",
|
||
"reversed": <bool>,
|
||
"interpretation": "3~4문장",
|
||
"evidence": {
|
||
"card_meaning_used": "참고 카드 정보에서 인용한 키워드·상징",
|
||
"position_logic": "왜 이 위치에 이렇게 적용되는지 (1~2문장)",
|
||
"category_lens": "카테고리 관점에서 부각되는 면 (1문장)"
|
||
},
|
||
"advice": "1문장"
|
||
}
|
||
],
|
||
"interactions": [
|
||
{ "type": "synergy"|"conflict"|"transition",
|
||
"between": ["<card_id>", "<card_id>"],
|
||
"explanation": "1~2문장" }
|
||
],
|
||
"advice": "2문장. interactions를 1개 이상 참조할 것.",
|
||
"warning": "역방향·충돌 경계 (없으면 null)",
|
||
"confidence": "high"|"medium"|"low"
|
||
}
|
||
|
||
# confidence 판정 기준
|
||
- high: 3장 모두 한 방향 서사 또는 명확한 전환
|
||
- medium: 2장 일관, 1장 별도 신호
|
||
- low: 카드 간 의미 충돌이 커서 명확한 흐름 잡기 어려움
|
||
|
||
# 금지사항
|
||
- 참고 카드 정보에 없는 상징 도입 금지
|
||
- 역방향 카드를 정방향처럼 다루지 말 것
|
||
- "신비롭게 들리는" 문구로 채우지 말 것 — evidence에 인용·근거 명시
|
||
- JSON 외 텍스트 금지
|
||
```
|
||
|
||
### USER_PROMPT_TEMPLATE
|
||
|
||
```text
|
||
# 질문
|
||
{question}
|
||
|
||
# 카테고리
|
||
{category}
|
||
|
||
# 스프레드
|
||
{spread_name} ({spread_count}장)
|
||
|
||
# 뽑힌 카드와 참고 카드 정보
|
||
{cards_with_reference_block}
|
||
|
||
# 작업
|
||
위 정보만을 근거로 사용해, 시스템 지침의 JSON 형식으로 응답하세요.
|
||
- 각 카드의 evidence.card_meaning_used에는 위 "참고 카드 정보"에서 발췌한 키워드·의미를 그대로 인용.
|
||
- interactions는 3장 간 슈트·원소·정역방향 패턴을 분석해 최소 1개 이상 도출.
|
||
- confidence는 카드 흐름의 일관성에 따라 정직하게 판정.
|
||
```
|
||
|
||
### cards_with_reference_block 예시
|
||
|
||
```
|
||
## 1. 위치: 과거 | 카드: The Fool (정방향)
|
||
- 아르카나: Major (0)
|
||
- 원소: 공기 (Air)
|
||
- 정방향 키워드: 새로운 시작, 도약, 순수, 자유
|
||
- 정방향 의미: 미지의 세계로 내딛는 첫걸음. 계산보다 직관과 신뢰로 시작하는 시기.
|
||
|
||
## 2. 위치: 현재 | 카드: The Lovers (역방향)
|
||
- 아르카나: Major (6)
|
||
- 원소: 공기 (Air)
|
||
- 역방향 키워드: 관계 갈등, 선택의 어려움
|
||
- 역방향 의미: 두 길 사이에서 머뭇거리거나, 이미 내린 선택의 의구심이 커지는 시기.
|
||
|
||
## 3. 위치: 미래 | 카드: Ten of Cups (정방향)
|
||
- 아르카나: Minor (Cups, 10)
|
||
- 원소: 물 (Water)
|
||
- 정방향 키워드: 정서적 충만, 가족·공동체의 행복
|
||
- 정방향 의미: 컵 슈트의 완성 단계. 감정적 만족이 안정된 형태로 자리잡는 시기.
|
||
|
||
## 추가 컨텍스트
|
||
- 메이저:마이너 비율: 2:1 (메이저 우세 → 큰 인생 주제)
|
||
- 원소 분포: 공기 2, 물 1
|
||
- 정역 흐름: 정→역→정 (일시적 정체 후 회복 가능성)
|
||
```
|
||
|
||
### 응답 검증 (백엔드)
|
||
- `cards[].evidence.card_meaning_used`가 비어있으면 → reroll 1회 (max 1 retry, 총 2회 호출)
|
||
- `interactions`가 비어있고 spread_type == "three_card"이면 → reroll 1회
|
||
- reroll 2회 모두 실패 → 받은 응답 그대로 저장 + log warning + 500 응답
|
||
- JSON 파싱 실패 → codeblock 추출 시도 → raw 추출 시도 → 텍스트 그대로 summary에 박고 cards=[]
|
||
|
||
### 비용
|
||
- Sonnet 4.6 입력 $3/1M, 출력 $15/1M
|
||
- 회당 입력 ~700, 출력 ~900 토큰
|
||
- 회당 비용 ~$0.015~0.022
|
||
- 환경변수로 가격 오버라이드: `TAROT_COST_INPUT_PER_M`, `TAROT_COST_OUTPUT_PER_M`
|
||
|
||
---
|
||
|
||
## 7. UI 흐름
|
||
|
||
### 7.1 Route 구조
|
||
| Path | 화면 | 컴포넌트 |
|
||
|---|---|---|
|
||
| `/tarot` | 랜딩 | `Tarot.jsx` |
|
||
| `/tarot/today` | 오늘의 카드 | `TodayCard.jsx` |
|
||
| `/tarot/reading` | 3장 스프레드 메인 | `Reading.jsx` |
|
||
| `/tarot/history` | 마이페이지 | `History.jsx` |
|
||
|
||
### 7.2 랜딩 (`/tarot`)
|
||
- 영상 배경 (`tarot_main_background.mp4` autoplay muted loop, `prefers-reduced-motion` 시 정지 이미지)
|
||
- Overlay: `linear-gradient(rgba(15,4,40,.5) → rgba(15,4,40,.85))`
|
||
- 헤더 sticky nav: 오늘의 카드 / 타로 리딩 / 가이드 / 히스토리
|
||
- Hero: h1 "당신의 오늘을 비추는 타로" + sub + 2 CTA (지금 시작하기 / 오늘의 카드)
|
||
- 3-tier 카드: 🌙 오늘의 운세 / 🃏 3장 스프레드 / ✨ AI 해석 (hover lift)
|
||
|
||
### 7.3 3장 스프레드 (`/tarot/reading`)
|
||
3-step 진행, 한 화면 안에서 step 전환.
|
||
|
||
**Step 1 — 질문 입력 (좌측 panel)**
|
||
- 질문 textarea
|
||
- 카테고리 chip 선택 (`CATEGORIES` 중 1개)
|
||
- 스프레드 라디오 (3장 / 1장)
|
||
- [⊃ 카드 셔플하기] 버튼
|
||
|
||
**Step 2 — 카드 선택 (중앙)**
|
||
- 셔플된 카드 16장 그리드 (4×4, 카드 뒷면)
|
||
- 카드 hover 시 lift + glow
|
||
- 카드 click 시 자리(과거→현재→미래)로 날아가며 flip + 위치 라벨 표시
|
||
- 3장 모두 채워지면 [AI 해석 시작] 버튼 활성
|
||
|
||
**Step 3 — AI 해석 (우측 panel)**
|
||
- 좌측: 3장 카드 자리 (카드 click으로 우측 panel 전환)
|
||
- 우측 panel: 선택된 카드명 + 키워드 chip + 기본 의미 + AI interpretation + AI evidence(접을 수 있음) + advice
|
||
- 하단: 종합 summary + advice + warning(있을 때) + confidence 배지
|
||
- 액션: [⭐ 즐겨찾기 토글] / [다시 뽑기]
|
||
|
||
### 7.4 오늘의 카드 (`/tarot/today`)
|
||
- 단일 큰 카드 슬롯 + "운명을 묻다" 버튼
|
||
- 카테고리·질문 옵션 (default = "일반 / 없음")
|
||
- 클릭 → 1장 추출 + flip 애니메이션 + Claude 호출 → 우측 텍스트로 해석 표시
|
||
- 하루 1회 제한은 v1에 없음 (소비 자유)
|
||
|
||
### 7.5 히스토리 (`/tarot/history`)
|
||
- 카드 리스트형: 날짜 · 스프레드 종류 · 질문 · 카드 미니 · 요약 한 줄 · confidence 배지 · ⭐ 토글
|
||
- 클릭 → 디테일 모달 (원본 해석 전체)
|
||
- 필터: 즐겨찾기만 / 스프레드 종류 / 카테고리
|
||
- 페이지네이션 20개씩
|
||
|
||
### 7.6 공용 컴포넌트
|
||
- `TarotCard.jsx` — 단일 카드 (앞·뒷면 토글, props: cardId / reversed / size / clickable)
|
||
- `CardGrid.jsx` — 셔플 16장 그리드 (props: deckSlice / onPick)
|
||
- `SpreadSlots.jsx` — 위치별 슬롯 (props: spread / cards)
|
||
- `InterpretationPanel.jsx` — 우측 패널 (카드 의미 + AI 텍스트 + evidence 접기)
|
||
- `useTarotShuffle.js` — Fisher–Yates + 16장 슬라이스 hook
|
||
- `useTarotReading.js` — 카드 선택 상태 + reference 블록 빌더 + AI 호출 + 저장 hook
|
||
|
||
### 7.7 디자인 토큰
|
||
- 배경 그라데이션: `#0a0420 → #1a0d2e → #2a1648`
|
||
- 금색 액센트: `#d4af37`
|
||
- 카드 보더 글로우: `0 0 24px rgba(212, 175, 55, .35)`
|
||
- 폰트: 본문 기존 / 타이틀 세리프 (Cormorant Garamond + Noto Serif KR 폴백)
|
||
- 네임스페이스: `.tarot-*`
|
||
|
||
### 7.8 navLinks 추가
|
||
- id: `tarot`, label: `Tarot`, path: `/tarot`, subtitle: `ARCANA`,
|
||
description: "라이더-웨이트 카드로 오늘과 내일을 비추는 리딩 랩",
|
||
icon: sparkle 아이콘, accent: `#a78bfa`
|
||
|
||
---
|
||
|
||
## 8. 미디어 자산
|
||
|
||
### 히어로 영상
|
||
- 원본: `source/videos/tarot_main_background.mp4`
|
||
- 배포 위치: `web-ui/public/videos/tarot_hero.mp4` (Vite public/ 직접 서빙)
|
||
- 권장 압축: 1920×1080 H.264 ≤4Mbps, ≤15초 loop
|
||
- 폴백: `prefers-reduced-motion` 또는 `navigator.connection.saveData` 시 `tarot_background.png` 정지 이미지
|
||
|
||
### 배경 이미지
|
||
- 원본: `source/images/tarot_page/tarot_background.png`
|
||
- 배포 위치: `web-ui/public/images/tarot_background.png`
|
||
- 사용: 영상 fallback + 카드 선택 페이지 배경 layer
|
||
|
||
### 카드 자산
|
||
- v1: `web-ui/public/images/tarot/card_back.svg` — 단일 카드 뒷면 SVG (보라+금 + ARCANA TAROT 모노그램)
|
||
- v1 카드 앞면: 78장 모두 CSS 카드 디자인 (그라데이션 보더 + 카드명 세리프 + 심볼 이모지)
|
||
- 사용자 자산 추가 시: `web-ui/public/images/tarot/cards/<slug>.png` 자동 매핑, 누락 시 `onError` → CSS 폴백
|
||
- 정적 파일이므로 이미지 추가 후 별도 빌드 불필요. NAS의 `frontend/images/tarot/cards/`에 robocopy 또는 직접 업로드 → 페이지 reload만으로 즉시 반영
|
||
- 사용자가 78장을 한 번에 추가하지 않아도 됨 — 매핑된 것은 이미지로, 안 된 것은 CSS 폴백으로 자연스럽게 혼용
|
||
|
||
---
|
||
|
||
## 9. 테스트 전략
|
||
|
||
### 프론트 (Vitest)
|
||
- `data/cards.js` 검증: 78장 총수, slug 중복 없음, 메이저 22 + 마이너 56, 모든 카드 keywords·meaningUpright·meaningReversed 존재
|
||
- `useTarotShuffle.js`: Fisher–Yates 정확성 (중복 없음, 분포)
|
||
- `useTarotReading.js`: 카드 선택 상태 전환, reference 블록 빌더 단위 테스트
|
||
- `TarotCard.jsx`: 정·역 토글, flip 상태, 이미지 onError 폴백
|
||
- `Reading.jsx`: step 1→2→3 전환
|
||
|
||
### 백엔드 (pytest)
|
||
- `tarot.py::interpret`: 응답 파싱 (raw JSON / codeblock 감싸진 JSON / 깨진 JSON 폴백)
|
||
- `tarot.py::interpret`: evidence·interactions 누락 시 reroll 1회 → 실패 시 그대로 저장
|
||
- `db.py`: tarot_readings CRUD 정확성, favorite 필터, 페이지네이션
|
||
- Anthropic 호출은 mock — 실제 호출은 통합 테스트 1건만
|
||
|
||
### 제외
|
||
- AI 응답 품질 자체는 자동 테스트 불가 — manual QA로 검수
|
||
|
||
---
|
||
|
||
## 10. 배포
|
||
|
||
1. **백엔드 (agent-office 수정만)**: `git push` → Gitea Webhook → agent-office 재빌드 + 자동 마이그레이션 (`CREATE TABLE IF NOT EXISTS`)
|
||
2. **프론트**: 로컬 빌드 → `npm run release:nas` → robocopy (영상·이미지 포함)
|
||
3. **docker-compose 변경 없음**
|
||
4. **nginx 변경 없음**
|
||
5. **`scripts/deploy*.sh` 변경 없음** — 컨테이너 리스트 그대로
|
||
|
||
---
|
||
|
||
## 11. 위험·완화
|
||
|
||
| 위험 | 완화 |
|
||
|---|---|
|
||
| Claude 응답 JSON 깨짐 | 파싱 폴백 3단(codeblock→raw→텍스트) + reroll 1회 |
|
||
| 영상 파일 NAS 트래픽↑ | 압축 후 사이즈 체크 — 5MB 초과 시 사용자 노티 |
|
||
| 카드 이미지 미준비로 임팩트↓ | CSS 카드 디자인을 시안 톤(보라+금)에 맞춰 정교화 |
|
||
| AI 비용 폭주 | 회당 ~$0.02, 일 50회 가정 시 월 ~$30 — 개인 사용 OK |
|
||
| 78장 의미 텍스트 작성 부담 | v1 plan에 별도 "데이터 시드 task" 분리, 메이저 22 우선 + 마이너 키워드만 |
|
||
| reference 블록을 프론트가 빌드 → 백엔드 검증 누락 | reference 블록 빈 문자열·길이 단순 검증만 추가 (carot 검증은 v2) |
|
||
|
||
---
|
||
|
||
## 12. v1 작업량 추산
|
||
- 백엔드: agent-office 추가 ~300 LOC (`agents/tarot.py` + `routes/tarot.py` + `db.py` 마이그레이션 + 테스트)
|
||
- 프론트: ~1500~2000 LOC (4 페이지 + 5~7 컴포넌트 + 데이터 + CSS)
|
||
- 카드 시드 데이터: 메이저 22장 완성 + 마이너 56장 키워드만 + 짧은 의미 1문장
|
||
- 예상 plan task: 15~18개
|