Files
web-page-backend/docs/superpowers/specs/2026-05-26-saju-ui-design.md
gahusb afb4175bd5 docs(spec): saju-lab UI v1 — 호령 사주 페이지 설계
- 시안 4종(메인/오늘운세/궁합/사주풀이) + 호령 캐릭터 시트 + 컬러시트 기반
- v1 범위: 메인 + 사주풀이 + 오늘운세 (궁합은 v2 placeholder)
- 백엔드 확장: fortune_scores + lucky + monthly_flow 산출
- 입력 흐름: reading_id URL 공유 + useSajuReading 캐시
- 데스크탑 우선 + 태블릿 반응형
- CSS .saju-page scope로 격리 + Pretendard + Noto Serif KR 폰트

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:51:34 +09:00

15 KiB

saju-lab UI v1 — 호령 사주 페이지 설계

작성일: 2026-05-26 상태: Spec (구현 plan 작성 전) 전제: saju-lab 백엔드 완성 (474 tests, SHA 8123f75) + web-ui Task 28 (api helpers + placeholder pages)


1. 목표

사용자 시안 4종(source/images/saju_page/horyung_saju_main.png, _today.png, _gunghab.png, _saju.png) + 캐릭터 시트(source/characters/horyung.png) + 컬러시트(saju_color_sheet.png) 기반으로 web-ui /saju/* 페이지를 호령 마스코트와 함께 구축한다.

v1 범위: 메인 / 오늘의 운세 / 사주풀이 3개 페이지. 궁합은 v2 placeholder.


2. 결정된 핵심 사항

항목 결정
캐릭터 자산 horyung.png + saju_color_sheet.png에서 PNG 6개 추출
백엔드 확장 saju-lab에 fortune_scores + lucky + monthly_flow 산출 추가
입력 흐름 메인에서 사주 1회 입력 → reading_id를 다른 페이지 URL query로 공유
v1 페이지 메인 + 사주풀이 + 오늘운세 (궁합은 v2)
반응형 데스크탑(1280+) 우선 + 태블릿 그라데이션
컬러 시안 추출 — 크림 베이스 + 다크 네이비 + 골드 + 살구 + 청록
폰트 Pretendard (본문) + Noto Serif KR (큰 제목, Google Fonts)
CSS 격리 .saju-page scope (다른 페이지에 새지 않음)

3. 백엔드 확장 (saju-lab)

3-1. 신설 모듈

saju-lab/app/calculator/fortune_scores.py — 4 카테고리 점수:

calculate_fortune_scores(saju, analysis, current_year) → {
  wealth:  0-100  (재물운)
  romance: 0-100  (연애운)
  social:  0-100  (인간관계)
  career:  0-100  (직장운)
  overall: 0-100  (가중평균: wealth*0.3 + career*0.3 + romance*0.2 + social*0.2)
}

알고리즘 (각 base 60에서 가산/감산, clamp 0-100):

  • wealth: +정재 강도 / +편재 강도 / +식상→재 통로 / -비겁 강도 / +세운재성
  • romance: +일지 합 / +정관·정재 균형 / -일지 충 / +세운 도화살
  • social: +인성 / +비겁 적정 / +식상 / +격국 균형 / +천을귀인
  • career: +정관 강도 / +편관 제어 / +일간 신강 / +세운 관성

saju-lab/app/calculator/lucky.py — 럭키 데이터:

calculate_lucky(saju, analysis, target_date) → {
  color: [str, str]   # 용신 오행 컬러 1~2개 (예: ["청록", "녹색"])
  number: int 1-9     # (일진 천간 idx + 시진 천간 idx) % 9 + 1
  direction: str      # 용신 오행 방향 (동/남/중앙/서/북)
  good_signs: [str]   # 세운 천간이 일간 재성 → "재물 기회" 등
  warnings: [str]     # 세운 지지가 일지 충 → "대인 갈등 주의"
}

오행→컬러/방향 매핑은 정적 dict. 럭키 숫자는 일진+시진(시간 미상 시 일진만)으로 산출.

saju-lab/app/calculator/monthly_flow.py — 12개월 운세 흐름:

calculate_monthly_flow(saju, year) → [
  {month: 1, stem: "壬", branch: "寅", score: 65, label: "변동"},
  {month: 2, stem: "癸", branch: "卯", score: 70, label: "성장"},
  ... 12 entries
]

각 월: 해당 월의 60갑자(寅월부터 12월 사이클) → 일간 관계(상생/상극/충/합) → score 0-100 + label(변동/성장/안정/도전/정체 등).

3-2. routers/saju.py 응답 확장

SajuInterpretResponse에 3 필드 추가:

fortune_scores: dict   # {wealth, romance, social, career, overall}
lucky: dict            # {color, number, direction, good_signs, warnings}
monthly_flow: list[dict]  # 12 entries

interpret_saju_endpoint에서 계산 + DB 저장 + 응답 포함.

3-3. db.py 스키마 마이그레이션

saju_records 테이블에 ALTER TABLE로 3 컬럼 추가 (idempotent):

  • fortune_scores_json TEXT
  • lucky_json TEXT
  • monthly_flow_json TEXT

init_db()에 try/except OperationalError 패턴 (이미 존재하면 skip).

_saju_row_to_dict에서 3 컬럼 JSON 파싱하여 응답에 포함.

3-4. 테스트

  • test_fortune_scores.py — 5-8 case (정재 강함 → wealth 80+, 일지 충 → romance 50-, clamp 검증)
  • test_lucky.py — 5 case (오행→컬러/방향 매핑, 럭키 숫자 1-9 범위)
  • test_monthly_flow.py — 3 case (12 entries 정확, 일간 충 월 score 낮음)

기존 30 reference fixture 비교는 영향 없음 (응답에 새 필드만 추가).


4. 프론트엔드 구조 (web-ui)

4-1. 디렉토리

web-ui/
├── public/images/saju/
│   ├── horyung/
│   │   ├── horyung-front.png        # 시안 main hero용 (정면, 큰 사이즈)
│   │   ├── horyung-bust.png         # 작은 카드용 (가슴샷)
│   │   ├── horyung-greeting.png     # 인사 표정 (메인 좌상단)
│   │   ├── horyung-thinking.png     # 생각하는 표정 (사주풀이)
│   │   ├── horyung-pointing.png     # 가르치는 표정 (오늘운세)
│   │   └── horyung-happy.png        # 기쁜 표정 (점수 높을 때)
│   ├── frame-cloud.png              # 시안의 한국화 산 배경 (hero용)
│   ├── pattern-cloud.svg            # 한국 전통 구름 패턴
│   └── icons/
│       ├── icon-today.svg
│       ├── icon-heart.svg
│       └── icon-book.svg
└── src/pages/saju/
    ├── Saju.css                     # 모든 saju 페이지 공통 스타일 (격리)
    ├── data/
    │   └── constants.js             # 4 카테고리 메타, 컬러 토큰
    ├── hooks/
    │   ├── useSajuForm.js
    │   └── useSajuReading.js        # reading_id → fetched data + 캐시
    ├── components/
    │   ├── HoryungMascot.jsx
    │   ├── SajuNav.jsx              # 시안 상단 네비게이션 (호령사주 로고 + nav)
    │   ├── SajuInputForm.jsx
    │   ├── ActionCard.jsx           # 3 카드 (오늘운세/궁합/사주풀이)
    │   ├── ScoreCard.jsx            # 카테고리 점수 카드
    │   ├── FortuneRing.jsx          # 종합점 ring SVG
    │   ├── LuckyBox.jsx             # 럭키 컬러/숫자/방향
    │   ├── ElementBarChart.jsx      # 오행 5색 가로 바
    │   ├── SajuPillars.jsx          # 4기둥 8자 표시
    │   ├── MonthlyFlow.jsx          # 12개월 운세 흐름 차트
    │   ├── InterpretAccordion.jsx   # AI 12항목 아코디언
    │   └── HoryungQuote.jsx         # 호령 말풍선
    ├── Saju.jsx                     # 메인 페이지
    ├── SajuResult.jsx               # 사주풀이 결과
    ├── Today.jsx                    # 오늘의 운세
    └── Compatibility.jsx            # v2 placeholder

4-2. 라우팅 (변경 없음, Task 28에서 등록됨)

경로 컴포넌트 reading_id 필요
/saju Saju.jsx (메인) 아니오
/saju/result?rid=N SajuResult.jsx
/saju/today?rid=N Today.jsx
/saju/compatibility Compatibility.jsx (placeholder)

기존 /saju/result 등은 Task 28에서 placeholder로 등록 — 본 task에서 실제 컴포넌트로 교체.

4-3. 데이터 흐름

[사용자] → /saju (메인)
   ↓ 사주 입력
   ↓ sajuInterpret(form)
   ↓ POST /api/saju/interpret
[saju-lab] 계산 + Claude AI + fortune_scores + lucky + monthly_flow
   ↓ 응답: { reading_id, ... 풍부한 데이터 }
[프론트] navigate(`/saju/result?rid=${reading_id}`)

[사주풀이 페이지] /saju/result?rid=N
   ↓ useSajuReading(N) → sajuGetReading(N)
   ↓ GET /api/saju/readings/N
   ↓ saju_data + analysis_data + daeun_data + interpretation_json + fortune_scores + lucky + monthly_flow
   ↓ 렌더

[오늘운세] /saju/today?rid=N — 사용자가 메인 또는 사주풀이에서 클릭
   ↓ useSajuReading(N) + sajuCurrentFortune(N)
   ↓ 렌더: ring + 4 score + lucky + 오늘 세운

4-4. 호령 마스코트

HoryungMascot.jsxpose prop으로 6개 PNG 중 선택.

<HoryungMascot pose="greeting" size="lg" />     // 메인 좌상단
<HoryungMascot pose="thinking" size="md" />      // 사주풀이
<HoryungMascot pose="pointing" size="md" />      // 오늘운세
<HoryungMascot pose="happy" size="sm" />         // 점수 높을 때 (옵션)

onError 핸들러로 PNG 누락 시 silent (디자인 깨짐 방지).

4-5. CSS 격리 + 컬러 시스템

Saju.css:

.saju-page {
  /* 베이스 */
  --saju-cream: #FAF6EE;
  --saju-paper: #F2EAD8;
  --saju-ink: #2E2D45;        /* 다크 네이비 (헤더, 본문) */
  --saju-ink-deep: #1F1D38;
  
  /* 액센트 */
  --saju-gold: #D4A574;
  --saju-gold-deep: #B5874E;
  --saju-apricot: #C58F76;
  --saju-rose: #D9A2A6;
  --saju-jade: #4B7065;
  --saju-violet: #6A5285;
  
  /* 카테고리 (3 ActionCard) */
  --saju-today-bg: #4B7065;      /* 청록 (오늘운세) */
  --saju-gunghab-bg: #A8736E;    /* 살구 (궁합) */
  --saju-saju-bg: #4F4A78;       /* 보라 (사주풀이) */
  
  /* 점수 카테고리 (4 ScoreCard) */
  --saju-wealth: #D4A574;        /* 골드 (재물) */
  --saju-romance: #D9A2A6;       /* 로즈 (연애) */
  --saju-social: #4B7065;        /* 청록 (인간관계) */
  --saju-career: #6A5285;        /* 보라 (직장) */
  
  min-height: 100vh;
  background: var(--saju-cream);
  color: var(--saju-ink);
  font-family: 'Pretendard', sans-serif;
}

.saju-page .saju-h1,
.saju-page .saju-h2 {
  font-family: 'Noto Serif KR', serif;
  font-weight: 700;
  letter-spacing: -0.02em;
}

모든 saju 컴포넌트의 클래스는 saju- prefix로 시작 (다른 페이지와 격리).

4-6. 반응형

  • 기준: 1280px+ 데스크탑 (시안 그대로)
  • 768~1280px 태블릿: hero 컬럼 → 세로 스택, action card 3 → 2x2 grid
  • ~768px 모바일: 호령 작게 (size="sm"), action card 1열, 입력 폼 세로

@media 쿼리로 Saju.css 안에서 처리.

4-7. 폰트

index.html에 Google Fonts preconnect + Noto Serif KR 추가:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@500;700&display=swap" rel="stylesheet">

큰 제목(h1/h2)만 Noto Serif KR, 본문은 기존 Pretendard.


5. 컴포넌트별 세부

5-1. Saju.jsx (메인)

레이아웃 (시안 horyung_saju_main.png):

  • 상단: SajuNav (호령사주 로고 + 4 nav + "사주풀이 시작하기" 버튼)
  • Hero: 좌측 호령(front + greeting 박스) / 우측 큰 h1 + 3 ActionCard
  • Bottom: 좌측 통계 미리보기 / 우측 SajuInputForm

폴백: reading_id가 query에 있으면 (/saju?rid=N) 통계 영역에 미리보기 점수 + 마지막 분석 결과로.

5-2. SajuResult.jsx (사주풀이)

레이아웃 (시안 horyung_saju_saju.png):

  • 상단: SajuNav + "사주풀이" 큰 타이틀 + 기본 정보 (이름, 생년월일) + 호령(thinking)
  • 중단 좌: 사주 4기둥 표 (SajuPillars) + 오행 바 차트 (ElementBarChart)
  • 중단 우: 호령의 비전 박스 (HoryungQuote — interpretation의 summary 발췌)
  • 하단: 성격강점 / 직업운 / 재물운 / 연애운 4 카드 (12항목 중 추출) + 12개월 운세 흐름 (MonthlyFlow)
  • 우하단: 이번 달 핵심 결정 포인트 (interpretation_json.advice)

데이터: useSajuReading(rid) → saju + analysis + daeun + interpretation_json + monthly_flow

5-3. Today.jsx (오늘의 운세)

레이아웃 (시안 horyung_saju_today.png):

  • 상단: SajuNav + "오늘의 운세" 큰 타이틀 + 호령(pointing) + 풍경 배경
  • 중단: FortuneRing(overall) + 4 ScoreCard(wealth/romance/social/career) + LuckyBox
  • 하단: 행운 알림 / 위험 알림 (lucky.good_signs, lucky.warnings)
  • 최하단: 다음 페이지 (사주풀이 / 궁합보기) 버튼

데이터: useSajuReading(rid) → fortune_scores + lucky + sajuCurrentFortune(rid) → 오늘 세운

5-4. Compatibility.jsx (v2 placeholder)

export default function Compatibility() {
  return (
    <div className="saju-page saju-page--compat-stub">
      <SajuNav />
      <div className="saju-stub">
        <HoryungMascot pose="thinking" />
        <h2>궁합보기는  만나요!</h2>
        <p> 사람의 사주를 함께 풀어보는 기능을 준비 중입니다.</p>
        <Link to="/saju">메인으로 돌아가기</Link>
      </div>
    </div>
  );
}

백엔드 /api/saju/compat/*는 이미 동작하지만 UI는 v2에서 정식 구현.


6. 에러 처리

시나리오 처리
메인 입력 폼 — 잘못된 날짜 Pydantic 422 → 폼에서 "올바른 날짜를 입력해주세요"
Claude API 504/500 "잠시 후 다시 시도해주세요" + 사용자 입력 보존
reading_id 무효(404) "사주 결과를 찾을 수 없습니다" + 메인으로 돌아가기 버튼
호령 PNG 누락 onError로 silent hide (디자인은 살짝 빈 자리, 동작은 정상)
fortune_scores 산출 실패 (예외) 기본값 60/60/60/60으로 fallback + 콘솔 warn

7. 테스트 전략

백엔드

  • fortune_scores: 5-8 unit test (각 카테고리 high/low 케이스 + clamp)
  • lucky: 5 unit test (오행→컬러 매핑, 숫자 1-9 범위, 방향)
  • monthly_flow: 3 unit test (12 entries, 점수 범위, 충/합 영향)
  • 기존 30 reference fixture 비교: 영향 없음 (응답 추가 필드만)

프론트

  • 컴포넌트 단위 테스트는 v1 범위 밖 (수동 e2e 검증)
  • 로컬 e2e: npm run dev + 입력 → 사주풀이/오늘운세 1회 정상 동작
  • 호령 6 PNG 모두 존재 확인 (수동)
  • 반응형 — Chrome DevTools 1280/1024/768 3가지 확인

8. 위험 + 완화

위험 완화
호령 PNG crop 좌표가 부정확 plan 단계에서 PIL로 trial-and-error + 사용자 검수. onError로 silent fallback
fortune_scores 점수 산식이 명리학적 부정확 v1은 plausible default + base 60으로 보수적. 실사용 피드백으로 튜닝
시안 색상과 미세 차이 시안 PNG에서 color picker로 hex 추출 후 CSS variable로 명시
Noto Serif KR Google Fonts 로드 지연 display=swap로 폰트 fallback (Pretendard) → 깜빡임 최소화
reading_id 만료(DB row 삭제) 404 graceful fallback + 새 입력 유도
Claude 응답 시간 초과 nginx timeout 300s + 폼에서 progress 표시

9. 향후 (v2, 본 spec 밖)

  • 궁합보기 페이지 정식 구현 (시안 horyung_saju_gunghab.png 기반)
  • 상담안내 페이지 (nav에 있는 메뉴)
  • 즐겨찾기/히스토리 페이지 (sajuListReadings 활용)
  • 사주풀이 PDF 내보내기
  • 호령 캐릭터 lottie 애니메이션 (정적 PNG → 동적)

10. 참고

  • 시안: source/images/saju_page/horyung_saju_{main,today,gunghab,saju}.png
  • 캐릭터: source/characters/horyung.png
  • 컬러시트: source/images/saju_page/saju_color_sheet.png
  • 백엔드: web-backend/saju-lab/ (SHA 8123f75)
  • 직전 spec: docs/superpowers/specs/2026-05-25-saju-tarot-lab-migration-design.md (saju-lab 백엔드 설계)
  • web-ui Task 28 commit: e634cde (api.js + routes + IconSaju + placeholder pages)