Files
web-page-backend/docs/superpowers/specs/2026-05-26-saju-ui-v2-redesign-design.md
gahusb fd40777177 docs(spec): 호령 사주 UI v2 리디자인 — 디자인 시스템 + 4 라우트 동시 교체
백호 사주도사 프로토타입(JSX 11파일 + styles.css)을 web-ui로 옮기는 작업의 spec.
주요 결정: (1) 사주 4 라우트 동시 리디자인 + /saju/me placeholder 신설 (2)
useViewportMode 1024px 분기로 모바일/데스크탑 컴포넌트 분리 (3) BottomNav 5항목
+ DesktopHeader 도입 (4) v1 components 12개 + SajuNav + Saju.css 전체 교체.

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

20 KiB
Raw Blame History

호령 사주 UI v2 리디자인 — 디자인 문서

  • 상태: Spec 단계 (brainstorming 종료, plan 대기)
  • 작성일: 2026-05-26
  • 대상 저장소: web-ui (React + Vite, /saju 라우트 트리)
  • 참조 디자인 소스: C:\Users\jaeoh\Desktop\workspace\source\images\saju_page\사주풀이\ (백호 사주도사 프로토타입: babel/standalone JSX 11 파일 + styles.css)
  • 선행 시스템: saju-lab UI v1 (web-ui/src/pages/saju/, 호령 캐릭터 7 PNG 자산 포함)
  • 백엔드 변경 없음: saju-lab /api/saju/* API는 그대로 사용

1. 목적 & 성공 기준

목적

v1의 임시 구조(컴포넌트 12개 직렬 배치, 단일 SajuNav)를 한국 전통 명리학 미학에 충실한 풀 디자인 시스템으로 교체. 4 라우트(/saju, /saju/result, /saju/today, /saju/compatibility)를 동시 리디자인하고 신규 /saju/me placeholder 추가.

성공 기준

  1. 4 라우트가 새 디자인 토큰/컴포넌트/네비로 일관되게 동작
  2. 1024px breakpoint에서 모바일(BottomNav) ↔ 데스크탑(헤더 nav) 자동 전환
  3. useSajuReading hook + 기존 API 호출 0개 변경, 응답 매핑만 추가
  4. 호령 PNG 7개 자산 100% 재사용 (variant API로 추상화)
  5. v1 컴포넌트 12개 + SajuNav 제거 — 두 디자인 시스템 동시 유지 X
  6. 시각 QA: 골든 패스(메인→입력→result→today→compatibility) + 1024px ± 경계 + me placeholder 모두 정상

2. 미학 방향 (Aesthetic Direction)

컨셉: 한국 전통 명리학 + 차분한 호령 캐릭터. 디자인 프로토타입이 이미 강하게 commit한 방향을 충실히 옮긴다.

2.1 타이포

  • Display: Nanum Myeongjo (weight 800, letter-spacing: -0.02em) — 페이지 타이틀, h1, 큰 한자
  • Body: Nanum Gothic (weight 400/700, letter-spacing: -0.01em) — 본문, 버튼, 캡션
  • Fallback serif: Gowun Batang
  • Google Fonts CSS 로드는 web-ui/index.html에 link 추가 (페이지 import 대신 — preconnect로 LCP 개선)
  • Inter/Roboto/system-ui 같은 generic AI sans는 사용 금지

2.2 컬러 시스템 (CSS 토큰)

디자인 프로토타입 styles.css:root 변수를 그대로 도입:

토큰 용도
--navy #1F2A44 dominant body color, dark surface
--navy-deep #141B30 night-bg gradient 하단
--navy-soft #2E3B5A 보조 dark
--ivory #F7F2E8 paper 배경, dark surface 위 텍스트
--ivory-soft #FBF7EF 카드 배경
--ivory-warm #F0E9D9 액센트 배경
--gold #D4AF37 sharp accent, 보더, ornament
--gold-soft #E8C76B 활성 상태 텍스트
--gold-dim #B89530 비활성 골드
--green / --green-soft / --green-bg 한국 전통 녹색 궁합 화면 accent
--purple / --purple-soft / --purple-bg #6A4C7C 계열 사주풀이 accent
--pink / --pink-deep / --pink-bg #F2C7CD 계열 보조
--gray / --gray-soft #6B6B6B / #9A968D 메타 텍스트
--gray-line / --gray-line-strong 보더
--shadow-card / --shadow-pop / --shadow-dark 그림자 단계

화면별 accent 단일 색 (팔레트 골고루 분산 안티패턴 회피):

  • 홈 (/saju) — navy
  • 오늘 (/saju/today) — gold
  • 궁합 (/saju/compatibility) — green
  • 사주풀이 (/saju/result) — purple
  • 마이 (/saju/me) — gray

2.3 배경 텍스처

  • .paper-bg — radial gold/purple wash + 페이퍼 노이즈 (사주풀이, 오늘, 궁합, 마이)
  • .night-bg — 밤하늘 gradient (홈 hero)
  • .mt-wash — 데스크탑 헤더 산수화 SVG decoration (좌하단 + 우하단 산 outline, opacity 0.35)
  • 단색 배경은 카드 내부에서만 (--ivory-soft)

2.4 차별화 요소 (UNFORGETTABLE)

  1. OrnateFrame — 한국 전통 더블 보더 + 4 코너 꺽쇠 SVG (<path d="M0 4 L0 0 L4 0" />)
  2. MascotBubble — 호령 발자국이 매 말풍선마다 paw-bob 2.4s ease infinite로 미세 bobbing
  3. OrnamentBloom — 골드 꽃봉오리 SVG가 모든 섹션 타이틀 좌우 ornament
  4. TopRibbon — 구름 SVG ribbon이 페이지 상단에 은은히
  5. CharBox — 사주명식 천간/지지 한자 Nanum Myeongjo 800 + 원소별 색 (목=green, 화=red, 토=earth, 금=gold, 수=blue)

2.5 모션

  • screenIn 0.3s cubic-bezier(0.16,1,0.3,1) translateY(6→0) — 라우트 진입 fade-up
  • paw-bob 2.4s ease infinite — 호령 발자국
  • BottomNav 활성 항목 배경 색 전환 0.2s
  • 과한 마이크로 인터랙션 X — "페이지당 1 hero 모션" 원칙

3. 아키텍처 & 라우팅

3.1 라우트 매핑

라우트 디자인 화면 파일 상태
/saju HomeScreen Saju.jsx 교체 (v1 메인)
/saju/result?rid=N SajuScreen (4탭) SajuResult.jsx 교체 (v1 결과)
/saju/today?rid=N TodayScreen Today.jsx 교체 (v1 오늘)
/saju/compatibility MatchScreen Compatibility.jsx placeholder → 본격 구현
/saju/compatibility/result?cid=N (디자인에 없음) CompatibilityResult.jsx 디자인 토큰만 라이트 리스타일
/saju/me MeScreen placeholder Me.jsx 신규

라우트 수: 5 신규 진입점 + 1 sub. routes.jsx/saju/me lazy import 추가.

3.2 디렉토리 구조

web-ui/src/pages/saju/
├── _shell/                        # v2 디자인 시스템 + 네비
│   ├── tokens.css                 # CSS 변수 정의
│   ├── shell.css                  # paper-bg, night-bg, mt-wash, OrnateFrame, screenIn
│   ├── useViewportMode.js         # 1024px breakpoint hook
│   ├── BottomNav.jsx              # 모바일 5항목 (home/today/match/saju/me)
│   ├── DesktopHeader.jsx          # 데스크탑 horizontal nav + 로고
│   ├── Mascot.jsx                 # variant API: full|head|upper|greeting|thinking|pointing|happy
│   ├── MascotBubble.jsx           # tone: ivory|navy|purple|green
│   ├── OrnateFrame.jsx
│   ├── OrnamentBloom.jsx
│   ├── TopRibbon.jsx
│   ├── TitleBlock.jsx
│   ├── PrimaryButton.jsx          # gold inset shadow
│   ├── GhostButton.jsx
│   ├── Icons.jsx                  # 5 nav icon + IconPaw/IconChevron/IconSparkle/IconYinYang
│   └── helpers/
│       ├── daeunLabel.js          # age → 성장기/학습기/...
│       ├── deriveTraits.js        # elements + sipsin → 6 성향
│       └── hexA.js                # hex → rgba(x,x,x,a)
├── Saju.jsx                       # routes 진입, useViewportMode → 분기
├── SajuResult.jsx
├── Today.jsx
├── Compatibility.jsx
├── CompatibilityResult.jsx
├── Me.jsx
└── views/                         # mobile/desktop 컴포넌트 분리
    ├── home.mobile.jsx
    ├── home.desktop.jsx
    ├── saju.mobile.jsx            # 4탭 (basic/chart/flow/traits)
    ├── saju.desktop.jsx           # 데스크탑은 4탭 그대로 vs 2-column 변형 — plan에서 결정
    ├── today.mobile.jsx
    ├── today.desktop.jsx
    ├── match.mobile.jsx
    └── match.desktop.jsx

Me 페이지는 mobile/desktop 분리 안 함 (placeholder라 단순 — Me.jsx 본문에 직접 구현).


기존 v1 파일들:
- `components/` 디렉토리 **전체 삭제** (SajuNav, HoryungMascot, SajuInputForm, ActionCard, SajuPillars, ElementBarChart, FortuneRing, ScoreCard, LuckyBox, InterpretAccordion, MonthlyFlow, HoryungQuote)
- `hooks/useSajuForm.js`, `hooks/useSajuReading.js` 유지 (데이터 흐름)
- `Saju.css` 신규 `_shell/tokens.css` + `_shell/shell.css`로 교체

---

## 4. 컴포넌트 명세

### 4.1 `useViewportMode()`
```js
function useViewportMode() {
  const [mode, setMode] = useState(() =>
    typeof window !== 'undefined' && window.innerWidth >= 1024 ? 'desktop' : 'mobile'
  );
  useEffect(() => {
    const onResize = () => {
      const next = window.innerWidth >= 1024 ? 'desktop' : 'mobile';
      setMode(prev => (prev === next ? prev : next));
    };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return mode;
}
  • 디자인 프로토타입의 동일 hook 그대로 포팅
  • SSR 안전 (typeof window 체크) — Vite 기본 CSR이라 항상 window 존재하지만 방어
  • debounce 없음 — resize 빈도가 낮고 setState가 동일 값일 때 reflow 없음 (Object.is 비교)

4.2 <Mascot variant="...">

variant 매핑 PNG (기존 v1 자산)
full /images/saju/horyung/horyung-main.png
head /images/saju/horyung/horyung-bust.png (얼굴 중심 crop)
upper /images/saju/horyung/horyung-front.png
greeting /images/saju/horyung/horyung-greeting.png
thinking /images/saju/horyung/horyung-thinking.png
pointing /images/saju/horyung/horyung-pointing.png
happy /images/saju/horyung/horyung-happy.png

props: variant, size (px), style (override). <img loading="lazy">.

4.3 <BottomNav current onChange theme>

  • position: fixed; bottom: 0 — iPhone frame이 아닌 실제 모바일 뷰포트의 하단
  • 5 아이템: home/today/match/saju/me. NavLink 사용으로 라우트 매핑 (useLocation으로 current 결정)
  • theme: 'ivory' (paper 배경) / 'navy' (night 배경) — backdrop-filter blur 적용
  • 활성 항목: 화면별 accent 색 배경(opacity 0.10~0.18) + 라벨 weight 700

4.4 <DesktopHeader>

  • position: sticky; top: 0; z-index: 30 — 스크롤 시 상단 고정
  • 좌측: 로고 ( 한자 + "호령사주" Nanum Myeongjo)
  • 중앙: nav 5 링크 (BottomNav와 동일 항목, horizontal 배치)
  • 우측: 미사용 (향후 me 메뉴)
  • 배경: --ivory-soft + 하단 --gray-line 1px

4.5 <OrnateFrame children color bg radius padding double>

  • 디자인 프로토타입 common.jsx의 OrnateFrame 그대로 포팅
  • double=true면 inset 4px 위치에 추가 보더
  • 4 코너 꺽쇠 SVG (rotate 0/90/180/270)

4.6 <MascotBubble text align tone tail paw>

  • tone 팔레트 (ivory/navy/green/purple) → bg/border/text 색
  • paw=true면 우하단 IconPaw + paw-bob 애니메이션
  • tail=true면 풍선 꼬리 (rotate 45deg 사각형)

4.7 Buttons

  • PrimaryButton: gold inset shadow (inset 0 1px 0 rgba(212,175,55,0.4)) + 풀 너비 옵션
  • GhostButton: 투명 배경 + 보더만, 동일 폰트/spacing

4.8 Me.jsx (placeholder, mobile/desktop 공통)

  • paper-bg + <TopRibbon> + <Mascot variant="thinking"> + <MascotBubble tone="purple"> "곧 만나요" + 비활성 카드 4개 (이력/북마크/설정/문의 — disabled)
  • 뷰포트 분리 없이 단일 컴포넌트 (placeholder라 단순)

4.9 입력 폼 컴포넌트 (Home에서 사용)

  • <InputRow label name type ...> — 디자인 토큰 적용된 단일 행 (label 좌측 64px + input 우측)
  • <DateSelect>, <TimeSelect>, <GenderToggle>, <CalendarToggle> (양/음력) — useSajuForm state와 연결
  • Phase 2에서 신설. 기존 v1 SajuInputForm.jsx의 검증 로직만 이식, 시각 표현은 새 디자인

5. 데이터 흐름

5.1 hook 재사용

  • useSajuReading(rid) — 그대로 유지. api.jssajuGetReading(id) 호출 → reading 객체 반환
  • useSajuForm() — 그대로 유지. 입력 검증 + sajuInterpret(body) 호출 + navigate

5.2 매핑 헬퍼 (_shell/helpers/)

daeunLabel(age) → string

  • age < 10 → "성장기"
  • age < 20 → "학습기"
  • age < 30 → "도전기"
  • age < 40 → "성장기"
  • age < 50 → "전성기"
  • age < 60 → "안정기"
  • age < 70 → "정리기"
  • age >= 70 → "여유기"

deriveTraits(elements, sipsin)[{id, ko, icon, color}] (최대 6개)

  • 강한 원소 1~2개 → 매칭 성향:
    • fire >= 30{id:'challenge', ko:'도전정신', color:'#C04A4A'}
    • metal >= 30{id:'lead', ko:'리더십', color:'#D4AF37'}
    • wood >= 30{id:'adapt', ko:'적응력', color:'#4E6B5C'}
    • water >= 30{id:'wisdom', ko:'지혜', color:'#3A5A8C'}
    • earth >= 30{id:'wealth', ko:'풍부함', color:'#A67B3F'}
  • 일간 강도 (신강/신약) → will (의지)
  • 결과 6개 미만이면 다음으로 강한 원소 추가
  • 순서: 강한 원소 점수 내림차순

hexA(hex, alpha)rgba(...) 문자열

  • 디자인 프로토타입 동일 헬퍼

5.3 SAJU_DATA mock → 실제 API 매핑 표

디자인 mock 필드 (screen-saju.jsx) API 응답 경로 (saju-lab) 비고
name, birth, gender, birthTime, birthPlace reading.input.* 직접 매핑
sajuLabel reading.label "경오년 신사월 갑자일 OO시"
ilgan reading.ilgan {ko, ch, element, sound}
pillars[] reading.pillars year/month/day/hour 4기둥
pillars[].cheongan.color 원소→색 매핑 (elementColor()) wood=green, fire=red, earth=earth, metal=gold, water=blue
pillars[].sipsin, jijang reading.pillars[i].sipsin, jijang
ohaeng[] reading.analysis.elements {wood, fire, earth, metal, water}[{id, ko, ch, value, color}] 변환
daeun[] reading.daeun (8개) labeldaeunLabel(age) 헬퍼, current는 현재 나이 기반 derive
traits[] deriveTraits(elements, sipsin) 헬퍼로 derive (API 응답에 직접 없음)
TraitsTab title, desc 상위 3 성향 → 정적 desc 사전 매핑 YAGNI: 백엔드에 trait description 추가는 향후 작업
Today: fortune_scores, lucky, monthly_flow API 응답에 이미 존재 그대로 사용

5.4 BottomNav active state

const { pathname } = useLocation();
const current =
  pathname === '/saju' ? 'home'
  : pathname.startsWith('/saju/today') ? 'today'
  : pathname.startsWith('/saju/compatibility') ? 'match'
  : pathname.startsWith('/saju/result') ? 'saju'
  : pathname.startsWith('/saju/me') ? 'me'
  : 'home';

6. 반응형 & 네비게이션 전략

6.1 1024px breakpoint

  • < 1024px → 모바일: 페이지 컴포넌트가 <MobileXxx> 렌더, <BottomNav> 표시
  • >= 1024px → 데스크탑: <DesktopXxx> 렌더, <DesktopHeader> 표시
  • 페이지 진입 시 useViewportMode()가 결정. resize 시 동적 전환

6.2 iPhone frame 제거

  • 디자인 프로토타입은 모바일 미리보기용으로 iPhone 외곽선을 그렸으나 실제 모바일 디바이스는 OS frame이 있으므로 frame DOM 제거
  • StatusBar(BrandStatusBar)도 미사용 — 실제 디바이스 status bar 자연스럽게 사용

6.3 컨테이너 max-width

  • 모바일: 100% (BottomNav만 fixed)
  • 데스크탑: 콘텐츠 max-width 1200px, margin: 0 auto. mt-wash 배경은 viewport 풀

6.4 transition between modes

  • 1024px 경계에서 mode 변경 시 컴포넌트가 unmount → 새 컴포넌트 mount → screenIn 0.3s 재생
  • 폼 입력 중 transition 발생 시: useSajuForm 상태는 hook이 보관하므로 데이터 유실 X

7. 점진적 구현 단계 (Phase Plan)

각 Phase 끝에 npm run devhttp://localhost:3007/saju 시각 확인 + git commit. PR은 Phase 13, 45, 6 (fixup) 3개로 분할 권장.

Phase 산출물 검증
1. Shell + 토큰 _shell/ 전체 + Me.jsx + 라우트 /saju/me 추가 + Google Fonts link /saju/me 진입 시 placeholder + BottomNav/Header 모두 정상. 기존 4 페이지 무손상
2. Home Saju.jsx + views/home.{mobile,desktop}.jsx + 입력 폼 + 호령 hero 모바일/데스크탑 모두 입력 → submit → /saju/result?rid=N 이동
3. SajuResult SajuResult.jsx + views/saju.{mobile,desktop}.jsx 4탭 + 매핑 헬퍼 실제 reading 데이터로 4탭 모두 정상 표시. 일간 표시·오행 막대·대운 흐름·성향 derive 검증
4. Today Today.jsx + views/today.{mobile,desktop}.jsx fortune_scores·lucky·monthly_flow 표시. PrimaryButton "다른 운세 보기" → SajuResult 이동
5. Compatibility Compatibility.jsx + views/match.{mobile,desktop}.jsx 본격 구현. CompatibilityResult.jsx 라이트 리스타일 두 사람 입력 폼 + compat API 호출 + 결과 화면
6. QA + cleanup v1 components/ 삭제, Saju.css 제거, 시각 QA, 1024px 경계 chrome devtools 골든 패스 통과, dead code 없음

8. 에러 / 빈 상태

상황 UI
API 실패 (네트워크/500) <OrnateFrame color="--purple"> + <MascotBubble tone="purple"> "아이고, 다시 시도해주세요" + <GhostButton> 새로고침
?rid= 없이 /saju/result 직접 진입 <MascotBubble tone="ivory"> "사주를 먼저 입력해주세요" + <PrimaryButton color="--purple"> "사주 입력하러 가기" → /saju
?rid= 없이 /saju/today 직접 진입 동일 패턴, accent gold
?cid= 없이 /saju/compatibility/result 진입 동일 패턴, accent green
/saju/me <MascotBubble tone="purple"> "곧 만나요" + 비활성 placeholder 카드 4개
백엔드 timeout (사주 해석 30~60초) 로딩 화면: <Mascot variant="thinking"> + <MascotBubble> "호령이 풀이 중이에요..." + spinner

9. 검증 전략

9.1 자동 테스트

  • useViewportMode.test.jsvi.mock window.innerWidth + resize 이벤트 dispatch, 1023/1024 경계 변환 확인
  • daeunLabel.test.js — 8 구간 모두 정답 매핑
  • deriveTraits.test.js — 강한 원소 1~5개 입력에 대한 정렬·중복 제거 확인
  • Mascot.test.jsx — 7 variant 모두 올바른 src prop

9.2 시각 검증 (Phase 마다 dev server)

  1. npm run devhttp://localhost:3007/saju 진입
  2. 모바일 chrome devtools (375×667 iPhone SE, 390×844 iPhone 12)
  3. 데스크탑 (1280×720 이상)
  4. 1024px 경계 ± 1px (1023↔1024)에서 mode 전환 확인
  5. 5 라우트 모두 BottomNav active 상태 + DesktopHeader active 상태 일치
  6. 호령 PNG 7 variant 모두 로드 확인 (Network 탭)
  7. 폰트 로드 (Nanum Myeongjo, Nanum Gothic, Gowun Batang)

9.3 회귀

  • 기존 reading_id URL 호환 (/saju/result?rid=N 패턴 유지)
  • useSajuReading hook 응답 매핑이 v1과 동일 데이터 표시
  • saju-lab API 호출 0개 변경 (네트워크 탭 비교)

10. YAGNI 명시 제외

다음은 이번 v2에서 의도적으로 제외:

  • i18n / 다국어
  • 다크모드 토글 (디자인 자체가 화면별 light/dark scope 고정)
  • 호령 마스코트 드래그·물리 모션 (paw-bob bobbing만)
  • BottomNav 햅틱·진동
  • 인증/로그인 (Me는 placeholder, 향후 별도 spec)
  • PWA / 오프라인 캐시
  • 백엔드 trait description API (deriveTraits 프론트 헬퍼로 충분)
  • 디자인 프로토타입의 desktop-shell.jsx full conversion — DesktopHeader만 차용, shell 전체는 v2 컨테이너에 흡수

11. 마이그레이션 노트

11.1 삭제 대상 (Phase 6에서 일괄 정리)

  • web-ui/src/pages/saju/components/ 전체 12 파일
  • web-ui/src/pages/saju/Saju.css
  • v1 Compatibility.jsx의 placeholder 본문 (본격 구현으로 교체)

11.2 보존 대상

  • web-ui/src/pages/saju/hooks/useSajuForm.js, useSajuReading.js (데이터 흐름)
  • web-ui/public/images/saju/horyung/ 7 PNG 자산 (Mascot variant API가 매핑)
  • web-ui/src/api.js saju 헬퍼 함수들

11.3 routes.jsx 변경

기존 import 라인 유지 + Me lazy import 추가:

+ const SajuMe = lazy(() => import('./pages/saju/Me'));

children 배열에 me 라우트 추가:

  path: '/saju',
  children: [
    { index: true, element: <Saju /> },
    { path: 'result', element: <SajuResult /> },
    { path: 'today', element: <SajuToday /> },
    { path: 'compatibility', element: <Compatibility /> },
    { path: 'compatibility/result', element: <CompatibilityResult /> },
+   { path: 'me', element: <SajuMe /> },
  ],

12. Plan 단계로 넘길 결정 사항

다음은 plan 작성 시 구체화:

  • 각 view 파일별 line budget (현실적 500~800 라인 예상, 더 크면 sub 컴포넌트 분할)
  • 색→원소 매핑 함수 (elementColor(elementId)) 위치 — _shell/helpers/ vs view 안 인라인
  • 데스크탑 saju.desktop.jsx의 4탭 유지 vs 2-column 변형 (디자인 프로토타입의 desktop-saju.jsx 상세 검토 후 결정)
  • 데스크탑 헤더의 me 메뉴 (향후 인증 위치 — 현재는 nav 5번째 링크)
  • 시각 QA 시 사용자 직접 확인 단계 (Claude가 puppeteer로 자동화하지 않음 — 시각 판단은 사람)
  • <InputRow> 등 입력 컴포넌트의 상세 props 시그니처