Files
web-page-backend/docs/superpowers/specs/2026-05-25-saju-tarot-lab-migration-design.md
gahusb d87ad2421d docs(spec): saju-lab 신설 + tarot-lab 분리 마이그레이션 설계
- saju-web (Next.js+Supabase+OpenAI) → saju-lab (Python FastAPI+SQLite+Claude)
- agent-office 내 tarot 모듈 → 독립 tarot-lab 컨테이너 분리
- Phase 1 tarot 분리 (DB 마이그레이션 스크립트 + cutover)
- Phase 2 saju 신설 (TS→Python 계산엔진 포팅 + 사주/궁합 v1)
- 포트: tarot-lab 18250, saju-lab 18300
- API: /api/tarot/*, /api/saju/* 완전 이전

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:23:12 +09:00

29 KiB

saju-lab 신설 + tarot-lab 분리 — 마이그레이션 설계

작성일: 2026-05-25 상태: Spec (구현 plan 작성 전)


1. 목표

  1. saju-lab 신설: 별도 디렉토리에 있던 saju-web (Next.js + Supabase + OpenAI) 프로젝트를 web-backend 모노레포의 한 lab 서비스로 마이그레이션. Python FastAPI + Claude + SQLite 패턴으로 단순화.
  2. tarot-lab 분리: 현재 agent-office 컨테이너 내부 모듈로 들어 있는 tarot 기능을 독립 컨테이너로 분리. agent-office가 가벼워지고 tarot은 자체 라이프사이클을 가짐.

두 작업이 같은 패턴(독립 lab 컨테이너 신설)을 공유하므로 하나의 spec에 담아 순차 구현.


2. 배경

2-1. saju-web 현황

  • 위치: C:\Users\jaeoh\Desktop\workspace\saju-web
  • 스택: Next.js 16, TypeScript, Supabase(OAuth+DB), OpenAI gpt-4o, PortOne 결제, Kakao 공유
  • 기능 4종: 사주분석(10토큰), 궁합(15토큰), 토정비결(5토큰), 오늘의 운세
  • 핵심 자산: lib/saju-calculator.ts, lib/ai-interpretation.ts, lib/daeun-calculator.ts, lib/solar-terms.ts (계산 엔진 ~1500줄)
  • 현재 사용 중이 아님. 자산 보존 + 패턴 일치화를 위한 마이그레이션

2-2. tarot-lab 현황

  • 위치: agent-office/app/tarot/ (모듈), agent-office/app/routers/tarot.py
  • DB: agent_office.dbtarot_readings 테이블
  • API: /api/agent-office/tarot/* 6개 endpoint (interpret, save, list, get, patch, delete)
  • 21개 단위 테스트 존재
  • 문제: agent-office가 점점 비대해짐 (텔레그램·로또·주식·청약·유튜브·타로 모두 한 컨테이너에). tarot은 독립 도메인이라 분리가 자연스러움

2-3. 다른 lab 패턴 (참조 기준)

insta-lab, music-lab, realestate-lab은 모두 동일 패턴:

<lab>/
├── Dockerfile (python:3.12-slim)
├── requirements.txt
├── pytest.ini
├── tests/
└── app/
    ├── main.py (FastAPI)
    ├── config.py
    ├── db.py (SQLite)
    └── <도메인 모듈들>
  • 인증 없음 (개인 NAS 서비스)
  • nginx가 /api/<name>/로 라우팅
  • docker-compose의 한 항목으로 등록
  • Gitea Webhook → deployer가 rsync + docker compose up -d --build

3. 핵심 결정 사항

항목 결정
백엔드 언어 Python FastAPI (saju 계산 엔진은 TypeScript → Python 포팅)
AI 모델 Claude Sonnet 4.6 (claude-sonnet-4-6) + prompt-caching beta. tarot과 일관
DB SQLite 로컬 (saju-lab은 saju.db, tarot-lab은 tarot.db)
인증 없음 (다른 lab 패턴 일치). saju-web의 Supabase/PortOne/Kakao 제거
saju-lab v1 기능 사주 분석 + 궁합 + 사주 결과 내 세운(歲運) (오늘의 운세는 세운으로 통합). 토정비결은 v2
tarot DB 마이그레이션 1회성 복사 스크립트 (agent_office.db → tarot.db), cutover 후 agent-office tarot 모듈 완전 제거
saju-lab UI 시안 기반 신규 (시안 추후 제공, Phase 2 마지막 단계)
API prefix /api/saju/*, /api/tarot/* (완전 이전) — /api/agent-office/tarot/*는 제거
포트 (내부) tarot-lab 18250, saju-lab 18300
진행 순서 Phase 1 tarot 분리 → Phase 2 saju 신설

4. 디렉토리 구조

web-backend/
├── tarot-lab/                      # [신설]
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── pytest.ini
│   ├── tests/
│   │   ├── test_db.py              # agent-office/tests/test_tarot_db.py 이관
│   │   ├── test_schema.py
│   │   ├── test_pipeline.py
│   │   └── test_routes.py          # 6 endpoint (interpret + readings CRUD 5)
│   └── app/
│       ├── __init__.py
│       ├── main.py                 # FastAPI app + /api/tarot/* 라우터 6개
│       ├── config.py               # TAROT_MODEL, TAROT_COST_*, ANTHROPIC_API_KEY, TAROT_TIMEOUT_SEC
│       ├── db.py                   # tarot.db: 5 CRUD + _tarot_row_to_dict
│       ├── models.py               # Pydantic 모델 5개 (TarotCardDraw, TarotInterpretRequest, TarotInterpretResponse, TarotSaveRequest, TarotPatchRequest)
│       ├── pipeline.py             # Claude 호출 + reroll 1회
│       ├── prompt.py               # SYSTEM_PROMPT + build_user_message
│       └── schema.py               # validate_interpretation
│
├── saju-lab/                       # [신설]
│   ├── Dockerfile
│   ├── requirements.txt            # fastapi, httpx, anthropic, pydantic, sxtwl(절기/음력)
│   ├── pytest.ini
│   ├── tests/
│   │   ├── fixtures/
│   │   │   └── reference_saju.json # Node.js 원본에서 추출한 입력→출력 쌍
│   │   ├── test_core.py            # 천간/지지/십성/십이운성/calculate_saju
│   │   ├── test_solar_terms.py     # 24절기
│   │   ├── test_lunar.py           # 음력 변환
│   │   ├── test_analysis.py        # 오행/신강신약/용신/세운
│   │   ├── test_daeun.py
│   │   ├── test_shinsal.py         # 신살/공망/지장간
│   │   ├── test_compatibility.py   # 궁합 점수
│   │   ├── test_pipeline.py        # Claude mock + reroll
│   │   ├── test_compat_pipeline.py
│   │   ├── test_schema.py
│   │   ├── test_db.py
│   │   └── test_routes.py
│   └── app/
│       ├── __init__.py
│       ├── main.py                 # FastAPI app
│       ├── config.py               # SAJU_MODEL, SAJU_COST_*, ANTHROPIC_API_KEY, SAJU_TIMEOUT_SEC
│       ├── db.py                   # saju.db: saju_records, compat_records 테이블 + CRUD
│       ├── models.py               # SajuRequest, CompatRequest, etc.
│       ├── calculator/
│       │   ├── __init__.py
│       │   ├── constants.py        # HEAVENLY_STEMS, EARTHLY_BRANCHES, FIVE_ELEMENTS, HIDDEN_STEMS, TEN_GODS, TWELVE_FORTUNES
│       │   ├── core.py             # get_year_ganzi, get_month_ganzi, get_day_ganzi, get_hour_ganzi, get_ten_god, get_twelve_fortune, calculate_saju
│       │   ├── solar_terms.py      # get_solar_term_date, get_current_solar_term, get_solar_term_month_branch, get_days_to_next_solar_term — sxtwl 사용
│       │   ├── lunar.py            # solar_to_lunar, lunar_to_solar
│       │   ├── shinsal.py          # get_hidden_stems, get_all_hidden_stems, analyze_branch_interactions, calculate_shinsal, calculate_gongmang
│       │   ├── analysis.py         # calculate_detailed_element_balance, calculate_element_score, analyze_day_master_strength, estimate_yongshin, calculate_seun, perform_full_analysis
│       │   ├── daeun.py            # calculate_daeun, get_current_daeun, get_daeun_description
│       │   └── compatibility.py    # calculate_compatibility (오행 상생/상극 + 지지 합/충 점수화)
│       ├── interpret/
│       │   ├── __init__.py
│       │   ├── pipeline.py         # Claude 호출 + reroll (tarot 패턴)
│       │   ├── compat_pipeline.py
│       │   ├── prompt.py           # 사주 12항목 SYSTEM_PROMPT (Claude용 재작성, evidence-based)
│       │   ├── compat_prompt.py    # 궁합 SYSTEM_PROMPT
│       │   └── schema.py           # validate_saju_interpretation, validate_compat_interpretation
│       └── routers/
│           ├── __init__.py
│           ├── saju.py             # POST /api/saju/interpret, /readings CRUD, /current-fortune
│           └── compat.py           # POST /api/saju/compat/interpret, /readings CRUD
│
├── agent-office/                   # [수정]
│   ├── app/
│   │   ├── tarot/                  # [제거]
│   │   ├── routers/tarot.py        # [제거]
│   │   ├── models.py               # Tarot* 5개 제거
│   │   ├── db.py                   # tarot_readings 관련 CRUD 5개 + _tarot_row_to_dict + CREATE TABLE 제거
│   │   └── main.py                 # include_router(tarot_router.router) 줄 제거
│   ├── tests/                      # test_tarot_*.py 4개 제거
│   └── scripts/
│       └── migrate_tarot_to_lab.py # [신설] 1회성 마이그레이션
│
├── docker-compose.yml              # [수정] tarot-lab, saju-lab 추가
├── nginx/default.conf              # [수정] /api/tarot/ → tarot-lab, /api/saju/ → saju-lab, /api/agent-office/tarot/ 제거
├── scripts/
│   ├── deploy-nas.sh               # [수정] CONTAINERS 배열에 saju-lab, tarot-lab 추가
│   └── deploy.sh                   # [수정] 5위치 (CLAUDE.md memory의 "배포 스크립트 동기화" 항목 참조)
└── docs/superpowers/specs/
    └── 2026-05-25-saju-tarot-lab-migration-design.md  # 본 문서

프론트엔드 (web-ui/) — Phase 1·2 양쪽 변경:

web-ui/
├── src/
│   ├── api.js                      # [Phase 1 수정] tarot helpers 6개 URL prefix 변경 + [Phase 2 추가] saju/compat helpers
│   ├── routes.jsx                  # [Phase 2 수정] /saju, /saju/result, /saju/compatibility, /saju/compatibility/result 라우트
│   ├── components/Icons.jsx        # [Phase 2 수정] IconSaju 추가
│   └── pages/
│       ├── tarot/                  # [Phase 1] URL prefix만 변경, 그 외 변경 없음
│       └── saju/                   # [Phase 2 신설, 시안 받은 후]
│           ├── Saju.jsx
│           ├── SajuForm.jsx
│           ├── SajuResult.jsx
│           ├── Compatibility.jsx
│           ├── CompatibilityForm.jsx
│           ├── CompatibilityResult.jsx
│           ├── data/
│           │   ├── constants.js    # 천간/지지/오행 상수 (UI 표시용)
│           │   └── interpretations.js
│           ├── hooks/
│           │   ├── useSajuForm.js
│           │   └── useSajuInterpretation.js
│           └── components/
│               ├── SajuBoard.jsx   # 4기둥 시각화
│               ├── ElementChart.jsx# 오행 차트
│               ├── DaeunTimeline.jsx
│               └── InterpretationPanel.jsx

5. Phase 1: tarot-lab 분리

5-1. 신규 tarot-lab 컨테이너 생성

파일 단순 복사 + 모듈 평탄화:

  • agent-office/app/tarot/__init__.pytarot-lab/app/__init__.py (간단화)
  • agent-office/app/tarot/prompt.pytarot-lab/app/prompt.py
  • agent-office/app/tarot/pipeline.pytarot-lab/app/pipeline.py (import 경로 수정: ..config.config, ..models.models)
  • agent-office/app/tarot/schema.pytarot-lab/app/schema.py

추출 파일:

  • tarot-lab/app/config.py: agent-office의 config.py에서 TAROT_* 환경변수만 추출
    import os
    ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
    TAROT_MODEL = os.getenv("TAROT_MODEL", "claude-sonnet-4-6")
    TAROT_COST_INPUT_PER_M = float(os.getenv("TAROT_COST_INPUT_PER_M", "3.0"))
    TAROT_COST_OUTPUT_PER_M = float(os.getenv("TAROT_COST_OUTPUT_PER_M", "15.0"))
    TAROT_TIMEOUT_SEC = int(os.getenv("TAROT_TIMEOUT_SEC", "180"))
    TAROT_DATA_PATH = os.getenv("TAROT_DATA_PATH", "/app/data")
    TAROT_DB_PATH = os.path.join(TAROT_DATA_PATH, "tarot.db")
    
  • tarot-lab/app/models.py: agent-office models.py에서 Tarot* 5개만 추출
  • tarot-lab/app/db.py:
    • tarot_readings CREATE TABLE + WAL 활성화
    • 5 CRUD (save/get/list/update/delete) + _tarot_row_to_dict
    • DB 경로는 TAROT_DB_PATH (volume mount된 /app/data/tarot.db)
  • tarot-lab/app/main.py:
    from fastapi import FastAPI, HTTPException
    from .models import (...)
    from . import pipeline, db as db_module
    
    app = FastAPI(title="tarot-lab")
    
    @app.on_event("startup")
    def _init_db():
        db_module.init_db()
    
    # /api/tarot/* 5 endpoints (routers/tarot.py 코드 그대로)
    

Dockerfile (insta-lab 패턴):

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt:

fastapi==0.115.0
uvicorn[standard]==0.32.0
httpx==0.27.2
pydantic==2.9.2

5-2. 테스트 이관

  • agent-office/tests/test_tarot_*.py 4개 파일 → tarot-lab/tests/test_*.py
  • import 경로 수정 (from app.tarot.pipelinefrom app.pipeline)
  • pytest.ini 추가 (testpaths = tests, pythonpath = .)
  • 모두 통과 확인 (21 tests)

5-3. DB 마이그레이션 스크립트

agent-office/scripts/migrate_tarot_to_lab.py:

"""1회성 — agent_office.db의 tarot_readings를 tarot.db로 복사.
멱등성: 이미 존재하는 id는 SKIP.
실행: docker exec agent-office python /app/scripts/migrate_tarot_to_lab.py
"""
import sqlite3
import os

SRC = os.getenv("AGENT_OFFICE_DB", "/app/data/agent_office.db")
DST = os.getenv("TAROT_DB", "/app/data/tarot.db")

def migrate():
    src = sqlite3.connect(SRC)
    dst = sqlite3.connect(DST)
    dst.execute("""
      CREATE TABLE IF NOT EXISTS tarot_readings (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ... (agent-office 스키마 그대로)
      )
    """)
    rows = src.execute("SELECT * FROM tarot_readings").fetchall()
    cols = [c[0] for c in src.execute("SELECT * FROM tarot_readings LIMIT 1").description]
    placeholders = ",".join("?" * len(cols))
    cols_str = ",".join(cols)
    moved = 0
    for r in rows:
        cur = dst.execute(f"SELECT 1 FROM tarot_readings WHERE id = ?", (r[0],))
        if cur.fetchone() is None:
            dst.execute(f"INSERT INTO tarot_readings ({cols_str}) VALUES ({placeholders})", r)
            moved += 1
    dst.commit()
    print(f"migrated {moved} / {len(rows)} rows")

if __name__ == "__main__":
    migrate()

볼륨 공유 전략: tarot-lab의 /app/data를 agent-office의 /app/data와 같은 NAS 호스트 디렉토리에 마운트. tarot.db는 신규 파일이라 별도 마운트 가능.

5-4. docker-compose / nginx / deploy 갱신

docker-compose.yml에 추가:

tarot-lab:
  build: ./tarot-lab
  container_name: tarot-lab
  restart: unless-stopped
  ports:
    - "18250:8000"
  environment:
    - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    - TAROT_MODEL=${TAROT_MODEL:-claude-sonnet-4-6}
    - TAROT_DATA_PATH=/app/data
  volumes:
    - ${RUNTIME_PATH:-.}/data:/app/data

nginx/default.conf에 추가, 기존 /api/agent-office/tarot/은 제거:

location /api/tarot/ {
    proxy_pass http://tarot-lab:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_connect_timeout 60s;
}

deploy 스크립트 5위치 (memory의 "배포 스크립트 동기화" 참조):

  • scripts/deploy-nas.sh의 CONTAINERS 배열
  • scripts/deploy.sh의 SERVICES, DIRS 배열
  • 컨테이너 목록 하드코딩된 모든 위치에 tarot-lab 추가 (Phase 1) / saju-lab 추가 (Phase 2)

5-5. agent-office cutover

마이그레이션 + 데이터 검증 후:

  • agent-office/app/tarot/ 디렉토리 통째로 제거
  • agent-office/app/routers/tarot.py 제거
  • agent-office/app/main.py에서 tarot router import + include_router 줄 제거
  • agent-office/app/models.py에서 TarotCardDraw, TarotInterpretRequest, TarotInterpretResponse, TarotSaveRequest, TarotPatchRequest 제거
  • agent-office/app/db.py에서 save_tarot_reading, get_tarot_reading, list_tarot_readings, update_tarot_reading, delete_tarot_reading, _tarot_row_to_dict 제거
  • agent-office/app/db.py의 CREATE TABLE에서 tarot_readings 줄 제거 (또는 idempotent 유지: 기존 DB 호환 위해 CREATE IF NOT EXISTS는 유지하되 코드 경로 제거)
  • agent-office/tests/test_tarot_*.py 4개 제거
  • agent-office pytest 통과 확인

5-6. web-ui api.js URL 변경

web-ui/src/api.js의 tarot helpers 6개:

  • tarotInterpret: /api/agent-office/tarot/interpret/api/tarot/interpret
  • tarotSaveReading: /api/agent-office/tarot/readings/api/tarot/readings
  • tarotListReadings: 동일 변환
  • tarotGetReading: 동일 변환
  • tarotPatchReading: 동일 변환
  • tarotDeleteReading: 동일 변환

Phase 1 검증: npm run devhttp://127.0.0.1:3007/tarot → 3장 리딩 1회 e2e 동작 확인.


6. Phase 2: saju-lab 신설

6-1. 계산 엔진 포팅 (TypeScript → Python)

핵심 위험: 계산 엔진은 ~1500줄 TypeScript로 매년 검증된 코드. Python으로 옮기면서 미세한 버그가 들어가면 모든 사주 해석이 잘못됨.

대응 전략 — Reference Output 비교 테스트:

  1. saju-web의 lib/saju-calculator.ts 코드를 Node.js로 직접 실행 (node -e "...")
  2. 알려진 입력 30~50쌍에 대해 calculateSaju(year, month, day, hour, gender) + performFullAnalysis(saju, currentYear) + calculateDaeun(...) 호출 결과를 JSON 파일로 저장
  3. tests/fixtures/reference_saju.json 형식:
    [
      {
        "input": {"year": 1990, "month": 5, "day": 15, "hour": 14, "gender": "male"},
        "expected": {
          "saju": {...},
          "analysis": {...},
          "daeun": [...]
        }
      },
      ... (50개)
    ]
    
  4. Python 포팅 후 pytest로 매 입력 → expected와 1:1 비교 (assert deep_equal(actual, expected))

포팅 순서 (의존성 그래프):

  1. calculator/constants.py — 모든 상수 (천간 10·지지 12·오행 5·십성·십이운성·지장간·신살)
  2. calculator/solar_terms.pysxtwl Python 라이브러리 사용 (24절기 + 음력)
  3. calculator/lunar.pysxtwl 음력↔양력 변환
  4. calculator/core.pyget_year_ganzi, get_month_ganzi (절기 기반), get_day_ganzi, get_hour_ganzi, get_ten_god, get_twelve_fortune, calculate_saju
  5. calculator/shinsal.py — 지장간(get_hidden_stems, get_all_hidden_stems), 지지 상호작용(analyze_branch_interactions), 신살(calculate_shinsal), 공망(calculate_gongmang)
  6. calculator/analysis.py — 오행 점수(calculate_detailed_element_balance, calculate_element_score), 신강신약(analyze_day_master_strength), 용신(estimate_yongshin), 세운(calculate_seun), 종합(perform_full_analysis)
  7. calculator/daeun.pycalculate_daeun, get_current_daeun, get_daeun_description
  8. calculator/compatibility.py — 두 사주의 오행 매칭 + 지지 합/충 점수화 → 0~100 점수

각 단계마다 reference test 통과를 게이트로.

6-2. Claude 프롬프트 (tarot 패턴 재활용)

interpret/prompt.py — 사주 12항목 해석:

  • 시스템 프롬프트: "당신은 한국 전통 사주명리학 전문가다. 다음 사주 + 분석 결과를 보고, JSON 스키마로 12항목 해석을 작성하라. 각 항목은 evidence 필드를 포함해 어떤 사주 요소에서 결론을 도출했는지 명시하라."
  • 12항목: 타고난 기질 / 오행 밸런스 / 지지 상호작용 / 신살 영향 / 재물운 / 직업 적성 / 애정운 / 건강운 / 현재 대운 / 올해 세운 / 인생 황금기 / 종합 조언
  • JSON 응답 스키마:
    {
      "items": [
        { "key": "기질", "title": "...", "content": "...", "evidence": {"saju_element": "...", "reasoning": "..."} },
        ...
      ],
      "summary": "...",
      "advice": "...",
      "warning": "...",
      "confidence": "high|medium|low"
    }
    
  • cache_control: ephemeral을 system 블록에 적용

interpret/compat_prompt.py — 궁합 해석:

  • 두 사주 + 궁합 점수 + 오행 상생/상극 분석 → JSON 응답
  • evidence: 어떤 지지 합/충에서 점수가 나왔는지 명시

interpret/schema.py — validate 함수:

  • validate_saju_interpretation(parsed): items 12개 존재 / 각 evidence 채워졌는지 / confidence 값 검증
  • validate_compat_interpretation(parsed): 마찬가지

interpret/pipeline.py — Claude 호출 (tarot pipeline.py 거의 그대로 복사 + 사주용 prompt/schema 사용):

  • max_tokens 2400 (12항목 + 종합이라 더 길음)
  • reroll 1회
  • latency_ms / tokens 로깅

6-3. DB 스키마

saju-lab/app/db.py:

SAJU_DB_SCHEMA = """
CREATE TABLE IF NOT EXISTS saju_records (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  birth_year INTEGER NOT NULL,
  birth_month INTEGER NOT NULL,
  birth_day INTEGER NOT NULL,
  birth_hour INTEGER,
  gender TEXT NOT NULL,
  calendar_type TEXT DEFAULT 'solar',
  saju_data JSON NOT NULL,
  analysis_data JSON NOT NULL,
  daeun_data JSON NOT NULL,
  interpretation_json JSON,
  model TEXT,
  tokens_in INTEGER DEFAULT 0,
  tokens_out INTEGER DEFAULT 0,
  cost_usd REAL DEFAULT 0,
  latency_ms INTEGER DEFAULT 0,
  reroll_count INTEGER DEFAULT 0,
  favorite INTEGER DEFAULT 0,
  memo TEXT,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS compat_records (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  person_a JSON NOT NULL,
  person_b JSON NOT NULL,
  score INTEGER NOT NULL,
  breakdown JSON NOT NULL,
  interpretation_json JSON,
  model TEXT,
  tokens_in INTEGER DEFAULT 0,
  tokens_out INTEGER DEFAULT 0,
  cost_usd REAL DEFAULT 0,
  favorite INTEGER DEFAULT 0,
  memo TEXT,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""

CRUD 함수: save_saju_record, get_saju_record, list_saju_records, update_saju_record, delete_saju_record + compat 5개.

6-4. API 엔드포인트

routers/saju.py:

메서드 경로 설명
POST /api/saju/interpret 입력 → 계산 + AI 해석 + DB 저장. 응답에 saju/analysis/daeun/interpretation/reading_id 포함
GET /api/saju/readings 페이지네이션 목록 (page, size, favorite)
GET /api/saju/readings/{id} 상세 조회
PATCH /api/saju/readings/{id} favorite, memo 수정
DELETE /api/saju/readings/{id} 삭제
GET /api/saju/current-fortune?reading_id={id} 저장된 사주 기반 오늘의 세운 (실시간 계산, AI 호출 없음)

routers/compat.py:

메서드 경로 설명
POST /api/saju/compat/interpret 두 사람 입력 → 두 사주 계산 + 궁합 점수 + AI 해석 + DB 저장
GET /api/saju/compat/readings 목록
GET /api/saju/compat/readings/{id} 상세
PATCH /api/saju/compat/readings/{id} favorite, memo
DELETE /api/saju/compat/readings/{id} 삭제

6-5. docker-compose / nginx 등록

docker-compose.yml에 saju-lab 항목 추가 (tarot-lab과 동일 패턴, 포트 18300).

nginx/default.conf에 추가:

location /api/saju/ {
    proxy_pass http://saju-lab:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_connect_timeout 60s;
}

deploy 스크립트 5위치에 saju-lab 추가.

6-6. web-ui /saju 페이지

시안 추후 제공 (사용자 확인). 시안 받은 후 tarot 페이지 패턴 따라 구현:

  • 입력 폼: 생년월일 + 시간 + 성별 + 양력/음력 (양력 default)
  • 결과 페이지: 사주판 시각화 + 오행 차트 + 대운 타임라인 + AI 12항목 아코디언
  • 궁합: 두 사람 입력 폼 + 결과 카드
  • 인사이트 패널 (tarot의 InterpretationPanel.jsx 패턴 차용)

api.js에 helpers 추가:

  • sajuInterpret, sajuListReadings, sajuGetReading, sajuPatchReading, sajuDeleteReading, sajuCurrentFortune
  • compatInterpret, compatListReadings, compatGetReading, compatPatchReading, compatDeleteReading

routes.jsx에 라우트 추가:

  • /saju (입력), /saju/result (사주 결과), /saju/compatibility (입력), /saju/compatibility/result (궁합 결과)

components/Icons.jsxIconSaju 추가.


7. 데이터 흐름

tarot-lab (Phase 1)

[web-ui /tarot/reading]
  ↓ POST /api/tarot/interpret { cards, question, category, spread_type }
[nginx /api/tarot/ → tarot-lab:8000]
  ↓ pipeline.interpret() → Claude API
  ↓ validate + reroll
[tarot-lab]
  ↓ POST /api/tarot/readings { ... save body }
  ↓ db.save_tarot_reading() → tarot.db INSERT
  ← { id, created_at }

saju-lab (Phase 2)

[web-ui /saju/result]
  ↓ POST /api/saju/interpret { year, month, day, hour, gender, calendarType }
[nginx /api/saju/ → saju-lab:8000]
  ↓ calculator.calculate_saju() → SajuData
  ↓ calculator.perform_full_analysis() → SajuAnalysis
  ↓ calculator.calculate_daeun() → DaeunPillar[]
  ↓ interpret.pipeline.interpret() → Claude API
  ↓ validate + reroll
  ↓ db.save_saju_record() → saju.db INSERT
  ← { saju, analysis, daeun, interpretation, reading_id, cost_usd, latency_ms }
[web-ui]

saju-lab 궁합

[web-ui /saju/compatibility/result]
  ↓ POST /api/saju/compat/interpret { person_a: {...}, person_b: {...} }
[saju-lab]
  ↓ calculate_saju(person_a) + calculate_saju(person_b)
  ↓ compatibility.calculate_compatibility(saju_a, saju_b) → { score, breakdown }
  ↓ interpret.compat_pipeline.interpret() → Claude API
  ↓ db.save_compat_record()
  ← { saju_a, saju_b, score, breakdown, interpretation, reading_id }

8. 에러 처리

시나리오 처리
Claude API HTTP error TarotError / SajuError raise → FastAPI 500
Claude JSON 파싱 실패 _extract_json codeblock 스트립 + 첫 { / 마지막 } 추출. 실패 시 reroll
validate 실패 (필수 필드 누락) reroll 1회. 그래도 실패 시 _Error("검증 실패") raise → 500
계산 엔진 입력 오류 (잘못된 날짜 등) Pydantic validation → 422
DB 락 sqlite WAL 모드. 짧은 retry 없이 raise (드물게 발생)
마이그레이션 스크립트 중복 실행 INSERT OR IGNORE 패턴 / 멱등

9. 테스트 전략

tarot-lab

  • 기존 21 tests 이관 + import 경로 수정 후 100% 통과

saju-lab — 계산 엔진

  • Reference output 비교가 핵심. 30~50개 입력 → JSON 저장 → Python 결과와 deep_equal 비교
  • 각 모듈 단위 테스트 (constants, solar_terms, lunar, core, shinsal, analysis, daeun, compatibility)
  • 회귀 방지: 추가 입력 케이스 발견 시 fixtures에 추가

saju-lab — Claude 파이프라인

  • httpx mock (respx 또는 monkeypatch) 사용 (tarot 패턴 그대로)
  • validate / reroll / JSON 파싱 폴백 / cost 계산 검증

saju-lab — 라우터

  • TestClient 기반 e2e (FastAPI 표준)
  • DB tmp_path fixture

통합 검증 (Phase 1, Phase 2 끝)


10. 환경변수 정리

tarot-lab 신규 환경변수 (docker-compose env):

  • ANTHROPIC_API_KEY (필수)
  • TAROT_MODEL (기본 claude-sonnet-4-6)
  • TAROT_COST_INPUT_PER_M (기본 3.0)
  • TAROT_COST_OUTPUT_PER_M (기본 15.0)
  • TAROT_TIMEOUT_SEC (기본 180)
  • TAROT_DATA_PATH (기본 /app/data)

saju-lab 신규 환경변수:

  • ANTHROPIC_API_KEY (필수)
  • SAJU_MODEL (기본 claude-sonnet-4-6)
  • SAJU_COST_INPUT_PER_M, SAJU_COST_OUTPUT_PER_M
  • SAJU_TIMEOUT_SEC
  • SAJU_DATA_PATH

11. 마이그레이션 위험 + 완화

위험 영향 완화
TS→Python 포팅 미세 차이 (예: 절기 일자 1일 차이) 모든 사주 결과 변형 Reference output 비교 테스트 50건 + sxtwl로 절기 동일 알고리즘 사용
tarot.db 마이그레이션 중 데이터 손실 사용자 리딩 이력 손실 멱등 스크립트 + 검증 후 cutover. agent-office의 원본 데이터는 cutover 후에도 30일 유지 (테이블만 DROP 안 함)
두 컨테이너 추가로 NAS 메모리 압박 다른 서비스 OOM python:3.12-slim 기반 ~150MB. 18GB RAM 여유 충분
API prefix 변경 missed 위치 (web-ui에서 일부 호출만 변경) 일부 페이지 404 grep 검색 (/api/agent-office/tarot) 후 일괄 변경
nginx restart 누락 라우팅 안 됨 docker compose up -d --build → nginx 컨테이너 재시작 자동 (deployer 패턴)
saju-web 코드 사라짐 (참조 못 하게 됨) 검증 어려움 saju-web 디렉토리는 그대로 유지 (포팅 끝나도 archive로 보존)

12. 향후 (v2, 본 spec 밖)

  • 토정비결 (12개월 운세) — saju-lab v2에서 추가
  • 정밀 음력 + 윤달 처리 검증
  • 자동 마이그레이션 스크립트의 ON DELETE CASCADE 검토 (이력 정합성)
  • agent-office의 tarot 관련 텔레그램 명령이 있다면 그것도 saju-lab에 추가할지 검토
  • saju-lab UI 디자인 시안 확정 후 별도 짧은 plan으로 진행

13. 참고 자료

  • saju-web/PROJECT_OVERVIEW.md — 마이그레이션 원본 명세
  • web-backend/CLAUDE.md — lab 서비스 패턴 참조
  • agent-office/app/tarot/, agent-office/app/routers/tarot.py — Phase 1 이관 원본
  • web-backend/insta-lab/, music-lab/, realestate-lab/ — Dockerfile + 디렉토리 구조 참조 패턴
  • sxtwl (Python 만세력 라이브러리) — solarlunar 대체
  • docs/superpowers/specs/2026-05-23-tarot-lab-design.md — 본 작업의 직전 spec (tarot-lab 원본 설계)