chore(saju-ui-v2): v1 components/ + Saju.css 일괄 삭제 (Phase 6 cleanup)

This commit is contained in:
2026-05-27 07:48:32 +09:00
parent be762e1ee8
commit d29fdac4a0
13 changed files with 0 additions and 772 deletions

View File

@@ -1,430 +0,0 @@
/* saju-page scope — 다른 페이지에 영향 없음 */
.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;
padding: 0;
margin: 0;
}
.saju-page * { box-sizing: border-box; }
.saju-page .saju-h1,
.saju-page .saju-h2,
.saju-page .saju-h3 {
font-family: 'Noto Serif KR', 'Pretendard', serif;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--saju-ink);
margin: 0;
}
.saju-page .saju-h1 { font-size: clamp(2.5rem, 4vw, 3.5rem); line-height: 1.2; }
.saju-page .saju-h2 { font-size: clamp(1.8rem, 3vw, 2.5rem); line-height: 1.3; }
.saju-page .saju-h3 { font-size: clamp(1.2rem, 2vw, 1.5rem); }
/* 호령 마스코트 */
.horyung-mascot { display: block; object-fit: contain; }
.horyung-mascot--sm { width: 80px; height: auto; }
.horyung-mascot--md { width: 180px; height: auto; }
.horyung-mascot--lg { width: 320px; height: auto; }
/* 상단 네비게이션 */
.saju-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background: var(--saju-ink);
color: var(--saju-cream);
}
.saju-nav__logo {
font-family: 'Noto Serif KR', serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--saju-cream);
text-decoration: none;
}
.saju-nav__links {
display: flex;
gap: 1.5rem;
list-style: none;
padding: 0;
margin: 0;
}
.saju-nav__links a {
color: var(--saju-cream);
text-decoration: none;
font-size: 0.95rem;
opacity: 0.85;
}
.saju-nav__links a:hover { opacity: 1; }
.saju-nav__cta {
background: var(--saju-gold);
color: var(--saju-ink);
border: none;
padding: 0.5rem 1.25rem;
border-radius: 999px;
font-weight: 600;
cursor: pointer;
font-family: 'Pretendard', sans-serif;
text-decoration: none;
}
/* Hero */
.saju-hero {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 3rem;
padding: 3rem 2rem;
max-width: 1400px;
margin: 0 auto;
}
.saju-hero__left {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.saju-quote-box {
background: var(--saju-paper);
padding: 1rem 1.25rem;
border-radius: 12px;
border: 1px solid var(--saju-gold-deep);
color: var(--saju-ink);
font-size: 0.9rem;
line-height: 1.5;
max-width: 280px;
}
.saju-hero__right {
display: flex;
flex-direction: column;
gap: 1.5rem;
justify-content: center;
}
.saju-sub {
color: var(--saju-ink);
opacity: 0.7;
margin: 0;
line-height: 1.6;
}
/* ActionCard */
.saju-action-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
.saju-action-card {
background: var(--saju-saju-bg);
color: var(--saju-cream);
padding: 1.5rem 1rem;
border-radius: 16px;
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
transition: transform 0.2s;
font-family: 'Pretendard', sans-serif;
}
.saju-action-card:hover { transform: translateY(-4px); }
.saju-action-card--today { background: var(--saju-today-bg); }
.saju-action-card--gunghab { background: var(--saju-gunghab-bg); }
.saju-action-card--saju { background: var(--saju-saju-bg); }
.saju-action-card[aria-disabled="true"] { opacity: 0.6; cursor: not-allowed; }
.saju-action-card__icon { font-size: 2rem; }
.saju-action-card__title { font-size: 1.1rem; font-weight: 700; }
.saju-action-card__desc { font-size: 0.85rem; opacity: 0.85; text-align: center; }
/* Bottom */
.saju-bottom {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
padding: 3rem 2rem;
max-width: 1400px;
margin: 0 auto;
background: var(--saju-ink);
color: var(--saju-cream);
border-radius: 24px 24px 0 0;
}
.saju-form { display: flex; flex-direction: column; gap: 1rem; }
.saju-form input,
.saju-form select {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--saju-gold-deep);
background: var(--saju-ink-deep);
color: var(--saju-cream);
font-family: inherit;
font-size: 1rem;
}
.saju-form button {
background: var(--saju-gold);
color: var(--saju-ink);
border: none;
padding: 0.875rem;
border-radius: 999px;
font-weight: 700;
cursor: pointer;
font-family: inherit;
font-size: 1rem;
}
.saju-form button:disabled { opacity: 0.6; cursor: not-allowed; }
.saju-form__error {
background: rgba(217, 162, 166, 0.2);
color: var(--saju-rose);
padding: 0.75rem;
border-radius: 8px;
font-size: 0.9rem;
}
/* Fortune ring */
.saju-fortune-ring {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.saju-fortune-ring svg { width: 200px; height: 200px; }
.saju-fortune-ring__score {
position: absolute;
font-family: 'Noto Serif KR', serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--saju-ink);
}
.saju-fortune-ring__total { font-size: 0.9rem; color: var(--saju-ink); opacity: 0.6; }
/* ScoreCard */
.saju-score-card {
background: var(--saju-cream);
border-radius: 16px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
border: 1px solid var(--saju-paper);
}
.saju-score-card__head { display: flex; align-items: center; gap: 0.5rem; }
.saju-score-card__icon { font-size: 1.5rem; }
.saju-score-card__title { font-weight: 700; font-size: 0.95rem; }
.saju-score-card__value {
font-family: 'Noto Serif KR', serif;
font-size: 2rem;
font-weight: 700;
color: var(--saju-ink);
}
.saju-score-card__bar {
height: 6px;
background: var(--saju-paper);
border-radius: 3px;
overflow: hidden;
}
.saju-score-card__bar > div { height: 100%; background: var(--saju-gold); transition: width 0.5s; }
/* Lucky box */
.saju-lucky-box {
background: var(--saju-paper);
border-radius: 16px;
padding: 1.5rem;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.saju-lucky-box__item { text-align: center; }
.saju-lucky-box__label { font-size: 0.8rem; color: var(--saju-ink); opacity: 0.7; margin-bottom: 0.25rem; }
.saju-lucky-box__value {
font-family: 'Noto Serif KR', serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--saju-ink);
}
/* SajuPillars */
.saju-pillars {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
.saju-pillar {
background: var(--saju-paper);
border-radius: 12px;
padding: 1rem;
text-align: center;
}
.saju-pillar__label { font-size: 0.8rem; color: var(--saju-ink); opacity: 0.6; margin-bottom: 0.5rem; }
.saju-pillar__stem,
.saju-pillar__branch {
font-family: 'Noto Serif KR', serif;
font-size: 1.75rem;
font-weight: 700;
display: block;
}
.saju-pillar__stem-kr,
.saju-pillar__branch-kr { font-size: 0.85rem; opacity: 0.7; }
.saju-pillar__ten-god,
.saju-pillar__fortune { font-size: 0.75rem; margin-top: 0.25rem; opacity: 0.7; }
/* Element bars */
.saju-element-bars {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.5rem;
background: var(--saju-cream);
border-radius: 16px;
}
.saju-element-bar {
display: grid;
grid-template-columns: 60px 1fr 50px;
align-items: center;
gap: 0.75rem;
}
.saju-element-bar__label { font-size: 0.9rem; font-weight: 700; }
.saju-element-bar__track {
height: 12px;
background: var(--saju-paper);
border-radius: 6px;
overflow: hidden;
}
.saju-element-bar__fill {
height: 100%;
border-radius: 6px;
transition: width 0.5s;
}
.saju-element-bar__fill--木 { background: #4B7065; }
.saju-element-bar__fill--火 { background: #C56F5C; }
.saju-element-bar__fill--土 { background: #D4A574; }
.saju-element-bar__fill--金 { background: #B8B5A8; }
.saju-element-bar__fill--水 { background: #4A5878; }
.saju-element-bar__value { text-align: right; font-size: 0.85rem; opacity: 0.7; }
/* Monthly flow */
.saju-monthly-flow {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 0.25rem;
padding: 1rem;
background: var(--saju-cream);
border-radius: 16px;
}
.saju-monthly-flow__cell {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0.25rem;
border-radius: 8px;
background: var(--saju-paper);
}
.saju-monthly-flow__month { font-size: 0.7rem; opacity: 0.7; }
.saju-monthly-flow__score {
font-family: 'Noto Serif KR', serif;
font-weight: 700;
font-size: 1rem;
}
.saju-monthly-flow__label { font-size: 0.7rem; opacity: 0.8; margin-top: 0.25rem; }
/* Horyung quote */
.saju-horyung-quote {
background: var(--saju-ink);
color: var(--saju-cream);
padding: 1.5rem;
border-radius: 16px;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.saju-horyung-quote__text { font-size: 0.95rem; line-height: 1.6; }
/* Interpret accordion */
.saju-interpret-accordion { display: flex; flex-direction: column; gap: 0.5rem; }
.saju-interpret-item {
background: var(--saju-cream);
border-radius: 12px;
border: 1px solid var(--saju-paper);
overflow: hidden;
}
.saju-interpret-item__header {
padding: 1rem;
background: var(--saju-paper);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
user-select: none;
}
.saju-interpret-item__body { padding: 1rem; font-size: 0.95rem; line-height: 1.6; }
.saju-interpret-item__evidence {
background: var(--saju-paper);
padding: 0.75rem;
border-radius: 8px;
margin-top: 0.75rem;
font-size: 0.85rem;
opacity: 0.85;
}
/* Stub */
.saju-stub {
max-width: 480px;
margin: 5rem auto;
text-align: center;
padding: 2rem;
background: var(--saju-paper);
border-radius: 24px;
}
.saju-stub a {
display: inline-block;
margin-top: 1.5rem;
background: var(--saju-gold);
color: var(--saju-ink);
padding: 0.75rem 1.5rem;
border-radius: 999px;
text-decoration: none;
font-weight: 700;
}
/* 반응형 */
@media (max-width: 1280px) {
.saju-hero { grid-template-columns: 1fr; text-align: center; }
.saju-hero__left { order: 2; }
.saju-hero__right { order: 1; }
.saju-bottom { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.saju-nav { padding: 0.75rem 1rem; flex-wrap: wrap; gap: 0.5rem; }
.saju-nav__links { display: none; }
.saju-action-cards { grid-template-columns: 1fr; }
.saju-pillars { grid-template-columns: repeat(2, 1fr); }
.saju-monthly-flow { grid-template-columns: repeat(4, 1fr); }
.horyung-mascot--lg { width: 220px; }
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const ICON = {
today: '☀',
heart: '♥',
book: '📖',
};
export default function ActionCard({ to, icon, title, desc, variant = 'saju', disabled = false }) {
const cls = `saju-action-card saju-action-card--${variant}`;
if (disabled) {
return (
<span className={cls} aria-disabled="true">
<span className="saju-action-card__icon">{ICON[icon] || '✦'}</span>
<span className="saju-action-card__title">{title}</span>
<span className="saju-action-card__desc">{desc || '준비 중'}</span>
</span>
);
}
return (
<Link to={to} className={cls}>
<span className="saju-action-card__icon">{ICON[icon] || '✦'}</span>
<span className="saju-action-card__title">{title}</span>
<span className="saju-action-card__desc">{desc}</span>
</Link>
);
}

View File

@@ -1,29 +0,0 @@
import React from 'react';
const ELEMENT_ORDER = ['木', '火', '土', '金', '水'];
const ELEMENT_KR = { '木': '목', '火': '화', '土': '토', '金': '금', '水': '수' };
export default function ElementBarChart({ scores }) {
if (!scores) return null;
const max = Math.max(...Object.values(scores), 1);
return (
<div className="saju-element-bars">
{ELEMENT_ORDER.map((e) => {
const value = scores[e] || 0;
const widthPct = (value / max) * 100;
return (
<div key={e} className="saju-element-bar">
<div className="saju-element-bar__label">{e} ({ELEMENT_KR[e]})</div>
<div className="saju-element-bar__track">
<div
className={`saju-element-bar__fill saju-element-bar__fill--${e}`}
style={{ width: `${widthPct}%` }}
/>
</div>
<div className="saju-element-bar__value">{value.toFixed(1)}%</div>
</div>
);
})}
</div>
);
}

View File

@@ -1,32 +0,0 @@
import React from 'react';
export default function FortuneRing({ score, max = 100 }) {
const radius = 80;
const circumference = 2 * Math.PI * radius;
const safe = Math.max(0, Math.min(score || 0, max));
const dashOffset = circumference - (safe / max) * circumference;
return (
<div className="saju-fortune-ring">
<svg viewBox="0 0 200 200">
<circle
cx="100" cy="100" r={radius}
stroke="var(--saju-paper)" strokeWidth="14" fill="none"
/>
<circle
cx="100" cy="100" r={radius}
stroke="var(--saju-gold)" strokeWidth="14" fill="none"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
strokeLinecap="round"
transform="rotate(-90 100 100)"
style={{ transition: 'stroke-dashoffset 0.6s ease' }}
/>
</svg>
<div style={{ position: 'absolute', textAlign: 'center' }}>
<div className="saju-fortune-ring__score">{safe}</div>
<div className="saju-fortune-ring__total">/ {max}</div>
</div>
</div>
);
}

View File

@@ -1,22 +0,0 @@
import React from 'react';
const POSE_TO_FILE = {
front: '/images/saju/horyung/horyung-front.png',
bust: '/images/saju/horyung/horyung-bust.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',
};
export default function HoryungMascot({ pose = 'front', size = 'lg', className = '' }) {
const src = POSE_TO_FILE[pose] || POSE_TO_FILE.front;
return (
<img
src={src}
alt="호령"
className={`horyung-mascot horyung-mascot--${size} ${className}`}
onError={(e) => { e.target.style.visibility = 'hidden'; }}
/>
);
}

View File

@@ -1,12 +0,0 @@
import React from 'react';
import HoryungMascot from './HoryungMascot';
export default function HoryungQuote({ pose = 'thinking', text }) {
if (!text) return null;
return (
<div className="saju-horyung-quote">
<HoryungMascot pose={pose} size="sm" />
<div className="saju-horyung-quote__text">{text}</div>
</div>
);
}

View File

@@ -1,38 +0,0 @@
import React, { useState } from 'react';
export default function InterpretAccordion({ items }) {
const [openKey, setOpenKey] = useState(items?.[0]?.key);
if (!items || items.length === 0) return null;
return (
<div className="saju-interpret-accordion">
{items.map((it) => {
const isOpen = openKey === it.key;
return (
<div key={it.key} className="saju-interpret-item">
<div
className="saju-interpret-item__header"
onClick={() => setOpenKey(isOpen ? null : it.key)}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setOpenKey(isOpen ? null : it.key); }}
>
<span>{it.title || it.key}</span>
<span aria-hidden>{isOpen ? '▾' : '▸'}</span>
</div>
{isOpen && (
<div className="saju-interpret-item__body">
<p style={{ margin: 0 }}>{it.content}</p>
{it.evidence && (
<div className="saju-interpret-item__evidence">
<strong>근거:</strong> {it.evidence.saju_element}<br />
<strong>해석 논리:</strong> {it.evidence.reasoning}
</div>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,21 +0,0 @@
import React from 'react';
export default function LuckyBox({ lucky }) {
if (!lucky) return null;
return (
<div className="saju-lucky-box">
<div className="saju-lucky-box__item">
<div className="saju-lucky-box__label">럭키 컬러</div>
<div className="saju-lucky-box__value">{(lucky.color || []).join(' · ')}</div>
</div>
<div className="saju-lucky-box__item">
<div className="saju-lucky-box__label">럭키 숫자</div>
<div className="saju-lucky-box__value">{lucky.number}</div>
</div>
<div className="saju-lucky-box__item">
<div className="saju-lucky-box__label">럭키 방향</div>
<div className="saju-lucky-box__value">{lucky.direction}</div>
</div>
</div>
);
}

View File

@@ -1,26 +0,0 @@
import React from 'react';
const LABEL_COLOR = {
'성장': '#4B7065',
'안정': '#D4A574',
'변동': '#6A5285',
'도전': '#C58F76',
'정체': '#888',
};
export default function MonthlyFlow({ flow }) {
if (!flow || flow.length === 0) return null;
return (
<div className="saju-monthly-flow">
{flow.map((m) => (
<div key={m.month} className="saju-monthly-flow__cell">
<span className="saju-monthly-flow__month">{m.month}</span>
<span className="saju-monthly-flow__score" style={{ color: LABEL_COLOR[m.label] }}>
{m.score}
</span>
<span className="saju-monthly-flow__label">{m.label}</span>
</div>
))}
</div>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
export default function SajuInputForm({ form, onChange, onSubmit, loading, error }) {
return (
<form className="saju-form" onSubmit={onSubmit}>
<h3 className="saju-h3" style={{ color: 'var(--saju-cream)', marginBottom: '0.5rem' }}>
사주풀이 시작하기
</h3>
<input
type="text"
placeholder="이름 (선택)"
value={form.name}
onChange={(e) => onChange('name', e.target.value)}
disabled={loading}
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.5rem' }}>
<input type="number" placeholder="년 (1900-2100)" value={form.year}
onChange={(e) => onChange('year', e.target.value)} disabled={loading} min="1900" max="2100" />
<input type="number" placeholder="월" value={form.month}
onChange={(e) => onChange('month', e.target.value)} disabled={loading} min="1" max="12" />
<input type="number" placeholder="일" value={form.day}
onChange={(e) => onChange('day', e.target.value)} disabled={loading} min="1" max="31" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.5rem' }}>
<input type="number" placeholder="시 (선택, 0-23)" value={form.hour}
onChange={(e) => onChange('hour', e.target.value)} disabled={loading} min="0" max="23" />
<select value={form.gender} onChange={(e) => onChange('gender', e.target.value)} disabled={loading}>
<option value="male"></option>
<option value="female"></option>
</select>
<select value={form.calendar_type} onChange={(e) => onChange('calendar_type', e.target.value)} disabled={loading}>
<option value="solar">양력</option>
<option value="lunar">음력</option>
</select>
</div>
{error && <div className="saju-form__error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? '호령이 풀어보는 중...' : '사주풀이 시작하기 ✦'}
</button>
</form>
);
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
export default function SajuNav() {
return (
<nav className="saju-nav" aria-label="호령 사주">
<Link to="/saju" className="saju-nav__logo">호령사주</Link>
<ul className="saju-nav__links">
<li><NavLink to="/saju/today">오늘의 운세</NavLink></li>
<li><NavLink to="/saju/compatibility">궁합보기</NavLink></li>
<li><NavLink to="/saju/result">사주풀이</NavLink></li>
</ul>
<Link to="/saju" className="saju-nav__cta">사주풀이 시작하기</Link>
</nav>
);
}

View File

@@ -1,38 +0,0 @@
import React from 'react';
const PILLAR_LABELS = { year: '년주', month: '월주', day: '일주', hour: '시주' };
export default function SajuPillars({ saju }) {
if (!saju) return null;
const pillars = ['year', 'month', 'day', 'hour'];
return (
<div className="saju-pillars">
{pillars.map((p) => {
const data = saju[p];
if (!data) {
return (
<div key={p} className="saju-pillar">
<div className="saju-pillar__label">{PILLAR_LABELS[p]}</div>
<div style={{ opacity: 0.4 }}>-</div>
</div>
);
}
return (
<div key={p} className="saju-pillar">
<div className="saju-pillar__label">{PILLAR_LABELS[p]}</div>
<div>
<span className="saju-pillar__stem">{data.stem}</span>
<span className="saju-pillar__stem-kr"> ({data.stem_kr})</span>
</div>
<div>
<span className="saju-pillar__branch">{data.branch}</span>
<span className="saju-pillar__branch-kr"> ({data.branch_kr})</span>
</div>
<div className="saju-pillar__ten-god">{data.ten_god}</div>
<div className="saju-pillar__fortune">{data.fortune}</div>
</div>
);
})}
</div>
);
}

View File

@@ -1,38 +0,0 @@
import React from 'react';
const ICON_BY_CATEGORY = {
wealth: '💰',
romance: '💖',
social: '🤝',
career: '💼',
};
const COLOR_VAR_BY_CATEGORY = {
wealth: 'var(--saju-wealth)',
romance: 'var(--saju-romance)',
social: 'var(--saju-social)',
career: 'var(--saju-career)',
};
const TITLE_BY_CATEGORY = {
wealth: '재물운',
romance: '연애운',
social: '인간관계',
career: '직장운',
};
export default function ScoreCard({ category, score }) {
const safe = Math.max(0, Math.min(score || 0, 100));
return (
<div className="saju-score-card">
<div className="saju-score-card__head">
<span className="saju-score-card__icon">{ICON_BY_CATEGORY[category]}</span>
<span className="saju-score-card__title">{TITLE_BY_CATEGORY[category]}</span>
</div>
<div className="saju-score-card__value">{safe}<small style={{ fontSize: '1rem', opacity: 0.5 }}>/100</small></div>
<div className="saju-score-card__bar">
<div style={{ width: `${safe}%`, background: COLOR_VAR_BY_CATEGORY[category] }} />
</div>
</div>
);
}