Migrate saju service UI
This commit is contained in:
@@ -12,6 +12,7 @@ import MascotBubble from './_shell/MascotBubble';
|
||||
import OrnateFrame from './_shell/OrnateFrame';
|
||||
import PrimaryButton from './_shell/PrimaryButton';
|
||||
import GhostButton from './_shell/GhostButton';
|
||||
import MatchResultDesktop from './views/match-result.desktop.jsx';
|
||||
import { compatGetReading } from '../../api';
|
||||
|
||||
export default function CompatibilityResult() {
|
||||
@@ -35,9 +36,12 @@ export default function CompatibilityResult() {
|
||||
return (
|
||||
<div className="saju-v2">
|
||||
{mode === 'desktop' && <DesktopHeader />}
|
||||
<main className="page paper-bg screen-in">
|
||||
<TopRibbon color="#4E6B5C" opacity={0.6} />
|
||||
<div style={{ maxWidth: mode === 'desktop' ? 720 : 'none', margin: '0 auto', padding: '24px 20px 40px' }}>
|
||||
{result && mode === 'desktop' ? (
|
||||
<MatchResultDesktop result={result} />
|
||||
) : (
|
||||
<main className="page paper-bg screen-in">
|
||||
<TopRibbon color="#4E6B5C" opacity={0.6} />
|
||||
<div style={{ maxWidth: mode === 'desktop' ? 720 : 'none', margin: '0 auto', padding: '24px 20px 40px' }}>
|
||||
{!cid && (
|
||||
<>
|
||||
<TitleBlock title="궁합 결과" gold="#4E6B5C" />
|
||||
@@ -100,8 +104,9 @@ export default function CompatibilityResult() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
{mode === 'mobile' && <BottomNav theme="ivory" />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,10 @@ import TopRibbon from './_shell/TopRibbon';
|
||||
import Mascot from './_shell/Mascot';
|
||||
import MascotBubble from './_shell/MascotBubble';
|
||||
import OrnateFrame from './_shell/OrnateFrame';
|
||||
import DesktopHero from './_shell/DesktopHero';
|
||||
import DesktopFooter from './_shell/DesktopFooter';
|
||||
import PrimaryButton from './_shell/PrimaryButton';
|
||||
import { IconPaw } from './_shell/Icons';
|
||||
|
||||
const DISABLED_CARDS = [
|
||||
{ title: '내 사주 이력', desc: '저장된 풀이를 한 번에' },
|
||||
@@ -21,6 +25,31 @@ export default function Me() {
|
||||
return (
|
||||
<div className="saju-v2">
|
||||
{mode === 'desktop' && <DesktopHeader />}
|
||||
{mode === 'desktop' ? (
|
||||
<main className="page paper-bg screen-in" style={{ marginTop: -78, paddingTop: 78 }}>
|
||||
<DesktopHero
|
||||
title="상담안내"
|
||||
subtitle="필요할 때 언제든 1:1 상담으로 함께 합니다."
|
||||
accent="#1F2A44"
|
||||
bubble={<div>걱정 마세요!<br />저와 함께 차근차근<br />풀어가요.</div>}
|
||||
/>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto', padding: '0 36px 32px' }}>
|
||||
<div className="k-frame" style={{ padding: '42px 48px', textAlign: 'center' }}>
|
||||
<div className="font-title" style={{ fontSize: 24, color: '#1F2A44', letterSpacing: '-0.03em' }}>
|
||||
전문가와 1:1 맞춤 상담
|
||||
</div>
|
||||
<div style={{ marginTop: 12, fontSize: 15, color: '#6B6B6B', lineHeight: 1.75 }}>
|
||||
사주풀이를 넘어, 인생의 큰 결정 앞에서 길잡이가 필요하실 때<br />
|
||||
검증된 명리학 전문가가 30분간 깊이 있게 풀어드립니다.
|
||||
</div>
|
||||
<PrimaryButton color="#1F2A44" full={false} style={{ margin: '24px auto 0', borderRadius: 999 }}>
|
||||
상담 신청하기 <IconPaw size={13} color="#E8C76B" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
) : (
|
||||
<main className="page paper-bg screen-in">
|
||||
<TopRibbon />
|
||||
<div style={{
|
||||
@@ -45,6 +74,7 @@ export default function Me() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
{mode === 'mobile' && <BottomNav theme="ivory" />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import './_shell/tokens.css';
|
||||
import './_shell/shell.css';
|
||||
import useViewportMode from './_shell/useViewportMode';
|
||||
@@ -8,10 +8,10 @@ import BottomNav from './_shell/BottomNav';
|
||||
import DesktopHeader from './_shell/DesktopHeader';
|
||||
import Mascot from './_shell/Mascot';
|
||||
import MascotBubble from './_shell/MascotBubble';
|
||||
import PrimaryButton from './_shell/PrimaryButton';
|
||||
import GhostButton from './_shell/GhostButton';
|
||||
import SajuMobile from './views/saju.mobile.jsx';
|
||||
import SajuDesktop from './views/saju.desktop.jsx';
|
||||
import sampleReading from './sampleReading';
|
||||
|
||||
export default function SajuResult() {
|
||||
const mode = useViewportMode();
|
||||
@@ -23,7 +23,10 @@ export default function SajuResult() {
|
||||
return (
|
||||
<div className="saju-v2">
|
||||
{mode === 'desktop' && <DesktopHeader />}
|
||||
{!rid && <EmptyState />}
|
||||
{!rid && (mode === 'desktop'
|
||||
? <SajuDesktop reading={sampleReading} />
|
||||
: <SajuMobile reading={sampleReading} />
|
||||
)}
|
||||
{rid && loading && <LoadingState />}
|
||||
{rid && error && <ErrorState />}
|
||||
{rid && data && (mode === 'desktop'
|
||||
@@ -35,20 +38,6 @@ export default function SajuResult() {
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<main className="page paper-bg screen-in" style={{ padding: '40px 24px', textAlign: 'center' }}>
|
||||
<Mascot variant="greeting" size={160} style={{ margin: '0 auto 16px' }} />
|
||||
<MascotBubble tone="ivory" tail={false}
|
||||
text="사주를 먼저 입력해주세요."
|
||||
style={{ margin: '0 auto 24px' }} />
|
||||
<Link to="/saju" style={{ display: 'inline-block' }}>
|
||||
<PrimaryButton color="#6A4C7C" full={false}>사주 입력하러 가기</PrimaryButton>
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return (
|
||||
<main className="page paper-bg screen-in" style={{ padding: '60px 24px', textAlign: 'center' }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import './_shell/tokens.css';
|
||||
import './_shell/shell.css';
|
||||
import useViewportMode from './_shell/useViewportMode';
|
||||
@@ -8,10 +8,10 @@ import BottomNav from './_shell/BottomNav';
|
||||
import DesktopHeader from './_shell/DesktopHeader';
|
||||
import Mascot from './_shell/Mascot';
|
||||
import MascotBubble from './_shell/MascotBubble';
|
||||
import PrimaryButton from './_shell/PrimaryButton';
|
||||
import GhostButton from './_shell/GhostButton';
|
||||
import TodayMobile from './views/today.mobile.jsx';
|
||||
import TodayDesktop from './views/today.desktop.jsx';
|
||||
import sampleReading from './sampleReading';
|
||||
|
||||
export default function Today() {
|
||||
const mode = useViewportMode();
|
||||
@@ -23,7 +23,10 @@ export default function Today() {
|
||||
return (
|
||||
<div className="saju-v2">
|
||||
{mode === 'desktop' && <DesktopHeader />}
|
||||
{!rid && <EmptyState />}
|
||||
{!rid && (mode === 'desktop'
|
||||
? <TodayDesktop reading={sampleReading} />
|
||||
: <TodayMobile reading={sampleReading} />
|
||||
)}
|
||||
{rid && loading && <LoadingState />}
|
||||
{rid && error && <ErrorState />}
|
||||
{rid && data && (mode === 'desktop'
|
||||
@@ -35,20 +38,6 @@ export default function Today() {
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<main className="page paper-bg screen-in" style={{ padding: '40px 24px', textAlign: 'center' }}>
|
||||
<Mascot variant="greeting" size={160} style={{ margin: '0 auto 16px' }} />
|
||||
<MascotBubble tone="ivory" tail={false}
|
||||
text="사주를 먼저 입력해주세요."
|
||||
style={{ margin: '0 auto 24px' }} />
|
||||
<Link to="/saju" style={{ display: 'inline-block' }}>
|
||||
<PrimaryButton color="#D4AF37" full={false}>사주 입력하러 가기</PrimaryButton>
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return (
|
||||
<main className="page paper-bg screen-in" style={{ padding: '60px 24px', textAlign: 'center' }}>
|
||||
|
||||
13
src/pages/saju/_shell/BrandMark.jsx
Normal file
13
src/pages/saju/_shell/BrandMark.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function BrandMark({ size = 36 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
||||
<circle cx="20" cy="20" r="18" stroke="#B89530" strokeWidth="1.2" />
|
||||
<circle cx="20" cy="20" r="14" stroke="#D4AF37" strokeWidth="1" opacity="0.7" />
|
||||
<path d="M20 5v30M5 20h30M9 9l22 22M31 9 9 31" stroke="#D4AF37" strokeWidth="0.7" opacity="0.4" />
|
||||
<circle cx="20" cy="20" r="4" fill="#D4AF37" />
|
||||
<circle cx="20" cy="20" r="1.8" fill="#FBF7EF" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
50
src/pages/saju/_shell/DesktopFooter.jsx
Normal file
50
src/pages/saju/_shell/DesktopFooter.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { IconSparkle, IconSun, IconUser } from './Icons';
|
||||
|
||||
function ShieldIcon() {
|
||||
return (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#D4AF37" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z" />
|
||||
<path d="M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const FOOTER_ITEMS = [
|
||||
{ label: '전통 명리학 기반', desc: '깊이 있는 전통 해석', icon: <IconSun size={22} stroke="#D4AF37" /> },
|
||||
{ label: 'AI 맞춤 인사이트', desc: '데이터 기반 정확도 향상', icon: <IconSparkle size={20} color="#D4AF37" /> },
|
||||
{ label: '1:1 상담 연계', desc: '필요시 전문가 상담 연결', icon: <IconUser size={22} stroke="#D4AF37" /> },
|
||||
{ label: '안전한 개인정보 관리', desc: '철저한 보안과 비식별 처리', icon: <ShieldIcon /> },
|
||||
];
|
||||
|
||||
export default function DesktopFooter() {
|
||||
return (
|
||||
<footer style={{
|
||||
marginTop: 48,
|
||||
borderTop: '1px solid rgba(31,42,68,0.08)',
|
||||
background: 'rgba(247,242,232,0.6)',
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: 1400, margin: '0 auto', padding: '24px 36px',
|
||||
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24,
|
||||
}}>
|
||||
{FOOTER_ITEMS.map((item) => (
|
||||
<div key={item.label} style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
background: 'rgba(212,175,55,0.10)', border: '1px solid rgba(212,175,55,0.3)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>{item.icon}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#1F2A44', letterSpacing: '-0.02em' }}>{item.label}</div>
|
||||
<div style={{ fontSize: 11, color: '#6B6B6B', marginTop: 2, letterSpacing: '-0.01em' }}>{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', padding: '14px 0 28px', fontSize: 11, color: '#9A968D', letterSpacing: '0.04em' }}>
|
||||
© 2026 호령사주 · BAEKHO SAJU DOSA
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { NAV_ITEMS } from './BottomNav';
|
||||
import BrandMark from './BrandMark';
|
||||
import { IconChevron } from './Icons';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ id: 'today', to: '/saju/today', label: '오늘의 운세' },
|
||||
{ id: 'match', to: '/saju/compatibility', label: '궁합보기' },
|
||||
{ id: 'saju', to: '/saju/result', label: '사주풀이' },
|
||||
{ id: 'me', to: '/saju/me', label: '상담안내' },
|
||||
];
|
||||
|
||||
function pathToCurrent(pathname) {
|
||||
if (pathname === '/saju' || pathname === '/saju/') return 'home';
|
||||
if (pathname.startsWith('/saju/today')) return 'today';
|
||||
if (pathname.startsWith('/saju/compatibility')) return 'match';
|
||||
if (pathname.startsWith('/saju/result')) return 'saju';
|
||||
@@ -18,40 +25,70 @@ export default function DesktopHeader() {
|
||||
|
||||
return (
|
||||
<header style={{
|
||||
position: 'sticky', top: 0, zIndex: 30, height: 64,
|
||||
background: '#FBF7EF', borderBottom: '1px solid rgba(31,42,68,0.10)',
|
||||
display: 'flex', alignItems: 'center', padding: '0 32px',
|
||||
backdropFilter: 'blur(14px)',
|
||||
position: 'sticky', top: 10, zIndex: 40,
|
||||
width: 'calc(100% - 72px)', maxWidth: 1368, height: 68,
|
||||
margin: '10px auto 0',
|
||||
background: 'rgba(251,247,239,0.88)',
|
||||
border: '1px solid rgba(31,42,68,0.13)',
|
||||
borderRadius: 999,
|
||||
display: 'flex', alignItems: 'center', padding: '0 22px 0 28px',
|
||||
backdropFilter: 'blur(18px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(18px) saturate(150%)',
|
||||
boxShadow: '0 8px 24px rgba(31,42,68,0.06), inset 0 1px 0 rgba(255,255,255,0.75)',
|
||||
}}>
|
||||
<button onClick={() => navigate('/saju')} style={{
|
||||
background: 'transparent', border: 'none', display: 'flex', alignItems: 'center', gap: 10,
|
||||
background: 'transparent', border: 'none', display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: 0, minWidth: 220,
|
||||
}}>
|
||||
<BrandMark size={40} />
|
||||
<span className="font-title" style={{
|
||||
fontSize: 26, color: '#D4AF37', lineHeight: 1,
|
||||
}}>壽</span>
|
||||
<span className="font-title" style={{
|
||||
fontSize: 18, color: '#1F2A44', letterSpacing: '-0.02em',
|
||||
fontSize: 26, color: '#1F2A44', letterSpacing: '-0.03em', lineHeight: 1,
|
||||
}}>호령사주</span>
|
||||
</button>
|
||||
<nav aria-label="사주 메뉴" style={{ marginLeft: 40, display: 'flex', gap: 8 }}>
|
||||
|
||||
<nav aria-label="사주 메뉴" style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
gap: 24, flex: 1,
|
||||
}}>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = item.id === current;
|
||||
return (
|
||||
<button key={item.id} onClick={() => navigate(item.to)}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
style={{
|
||||
background: active ? 'rgba(31,42,68,0.06)' : 'transparent', border: 'none',
|
||||
padding: '8px 14px', borderRadius: 8,
|
||||
color: active ? item.accent : '#6B6B6B',
|
||||
fontSize: 13, fontWeight: active ? 700 : 500, letterSpacing: '-0.02em',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: active ? 'rgba(31,42,68,0.07)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: active ? 18 : 0,
|
||||
padding: active ? '11px 24px' : '11px 10px',
|
||||
color: active ? '#1F2A44' : '#202638',
|
||||
fontSize: 16,
|
||||
fontWeight: active ? 800 : 700,
|
||||
letterSpacing: '-0.03em',
|
||||
}}>
|
||||
<item.Icon size={16} stroke={active ? item.accent : '#9A968D'} strokeWidth={active ? 1.8 : 1.5} />
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<button onClick={() => navigate('/saju')} style={{
|
||||
padding: '13px 22px 13px 26px',
|
||||
borderRadius: 999,
|
||||
background: '#1F2A44',
|
||||
color: '#F7F2E8',
|
||||
border: '1px solid rgba(212,175,55,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.02em',
|
||||
boxShadow: '0 6px 18px rgba(31,42,68,0.18), inset 0 1px 0 rgba(212,175,55,0.3)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
사주풀이 시작하기
|
||||
<IconChevron dir="right" size={14} color="#E8C76B" />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
68
src/pages/saju/_shell/DesktopHero.jsx
Normal file
68
src/pages/saju/_shell/DesktopHero.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import Mascot from './Mascot';
|
||||
import OrnamentBloom from './OrnamentBloom';
|
||||
import { IconPaw } from './Icons';
|
||||
|
||||
export default function DesktopHero({
|
||||
title,
|
||||
subtitle,
|
||||
accent = '#D4AF37',
|
||||
bubble,
|
||||
mascotVariant = 'full',
|
||||
}) {
|
||||
return (
|
||||
<section className="mt-wash" style={{ position: 'relative', padding: '56px 36px 64px', overflow: 'hidden' }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto', position: 'relative', zIndex: 2 }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="bloom-row" style={{ color: accent, display: 'inline-flex', alignItems: 'center', gap: 12 }}>
|
||||
<svg width="60" height="6" viewBox="0 0 60 6">
|
||||
<path d="M0 3 L56 3" stroke={accent} strokeWidth="1" />
|
||||
<circle cx="58" cy="3" r="2" fill={accent} />
|
||||
</svg>
|
||||
<OrnamentBloom size={22} color={accent} />
|
||||
<svg width="60" height="6" viewBox="0 0 60 6">
|
||||
<circle cx="2" cy="3" r="2" fill={accent} />
|
||||
<path d="M4 3 L60 3" stroke={accent} strokeWidth="1" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="font-title" style={{
|
||||
margin: '14px 0 0', fontSize: 72, color: '#1F2A44',
|
||||
letterSpacing: '-0.04em', lineHeight: 1,
|
||||
}}>{title}</h1>
|
||||
<div style={{
|
||||
marginTop: 18, fontSize: 16, color: '#6B6B6B',
|
||||
letterSpacing: '-0.01em',
|
||||
}}>{subtitle}</div>
|
||||
</div>
|
||||
|
||||
{bubble && (
|
||||
<div style={{ position: 'absolute', right: 220, top: 36, maxWidth: 210 }}>
|
||||
<div style={{
|
||||
background: '#FBF7EF', border: '1px solid rgba(31,42,68,0.12)',
|
||||
borderRadius: 18, padding: '15px 17px',
|
||||
fontSize: 13, color: '#1F2A44', lineHeight: 1.65, letterSpacing: '-0.01em',
|
||||
boxShadow: '0 4px 14px rgba(31,42,68,0.08)', position: 'relative',
|
||||
}}>
|
||||
{bubble}
|
||||
<div style={{
|
||||
position: 'absolute', right: -7, bottom: 18,
|
||||
width: 14, height: 14, background: '#FBF7EF',
|
||||
borderRight: '1px solid rgba(31,42,68,0.12)',
|
||||
borderBottom: '1px solid rgba(31,42,68,0.12)',
|
||||
transform: 'rotate(-45deg)',
|
||||
}} />
|
||||
<div style={{ textAlign: 'right', marginTop: 4, color: '#B89530', opacity: 0.7 }}>
|
||||
<IconPaw size={11} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Mascot variant={mascotVariant} size={220} style={{
|
||||
position: 'absolute', right: -10, top: -8, width: 220, pointerEvents: 'none',
|
||||
filter: 'drop-shadow(0 8px 24px rgba(31,42,68,0.18))',
|
||||
}} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +50,43 @@ export function IconUser({ size = 20, stroke = 'currentColor', strokeWidth }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function IconStar({ size = 16, filled = true, color = '#D4AF37' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24"
|
||||
fill={filled ? color : 'none'} stroke={color} strokeWidth="1.4"
|
||||
strokeLinejoin="round">
|
||||
<path d="M12 3l2.7 5.7 6.3.9-4.6 4.4 1.1 6.2L12 17.3 6.5 20.2l1.1-6.2L3 9.6l6.3-.9z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconMoney({ size = 20, stroke = 'currentColor', strokeWidth }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...base(size, stroke, strokeWidth)}>
|
||||
<path d="M9 5c-2 0-3 1.5-3 3l6 2c2 .7 3 1.5 3 3 0 1.8-1.5 3-4 3" />
|
||||
<path d="M12 4v2M12 18v2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCalendar({ size = 20, stroke = 'currentColor', strokeWidth }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...base(size, stroke, strokeWidth)}>
|
||||
<rect x="4" y="6" width="16" height="14" rx="2" />
|
||||
<path d="M4 10h16M9 4v4M15 4v4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconClock({ size = 20, stroke = 'currentColor', strokeWidth }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...base(size, stroke, strokeWidth)}>
|
||||
<circle cx="12" cy="12" r="8.5" />
|
||||
<path d="M12 7.5V12l3 2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPaw({ size = 12, color = 'currentColor' }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width={size} height={size} fill={color}>
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
const VARIANT_TO_SRC = {
|
||||
full: '/images/saju/horyung/horyung-main.png',
|
||||
head: '/images/saju/horyung/horyung-bust.png',
|
||||
upper: '/images/saju/horyung/horyung-front.png',
|
||||
head: '/images/saju/horyung/horyung-head.png',
|
||||
upper: '/images/saju/horyung/horyung-upper.png',
|
||||
greeting: '/images/saju/horyung/horyung-greeting.png',
|
||||
thinking: '/images/saju/horyung/horyung-thinking.png',
|
||||
pointing: '/images/saju/horyung/horyung-pointing.png',
|
||||
|
||||
21
src/pages/saju/_shell/PanelHeader.jsx
Normal file
21
src/pages/saju/_shell/PanelHeader.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import OrnamentBloom from './OrnamentBloom';
|
||||
|
||||
export default function PanelHeader({
|
||||
title,
|
||||
color = '#1F2A44',
|
||||
accent = '#D4AF37',
|
||||
right = null,
|
||||
icon = null,
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
{icon || <OrnamentBloom size={20} color={accent} />}
|
||||
<h3 className="font-title" style={{
|
||||
margin: 0, fontSize: 18, color, letterSpacing: '-0.02em',
|
||||
}}>{title}</h3>
|
||||
<div style={{ flex: 1 }} />
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
/* paper texture */
|
||||
.saju-v2 .paper-bg {
|
||||
background:
|
||||
linear-gradient(rgba(247, 242, 232, 0.86), rgba(251, 247, 239, 0.92)),
|
||||
url('/images/saju/horyung/background.png') center top / cover no-repeat,
|
||||
radial-gradient(ellipse at top, rgba(212, 175, 55, 0.06), transparent 60%),
|
||||
radial-gradient(ellipse at bottom, rgba(106, 76, 124, 0.04), transparent 60%),
|
||||
linear-gradient(180deg, var(--ivory) 0%, var(--ivory-soft) 100%);
|
||||
@@ -30,6 +32,8 @@
|
||||
.saju-v2 .mt-wash {
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(rgba(251, 247, 239, 0.82), rgba(244, 236, 219, 0.9)),
|
||||
url('/images/saju/horyung/background.png') center top / cover no-repeat,
|
||||
radial-gradient(ellipse 70% 50% at 10% 80%, rgba(31, 42, 68, 0.06), transparent 65%),
|
||||
radial-gradient(ellipse 60% 40% at 90% 70%, rgba(31, 42, 68, 0.05), transparent 65%),
|
||||
radial-gradient(ellipse 100% 60% at 50% 100%, rgba(212, 175, 55, 0.04), transparent 70%),
|
||||
@@ -49,6 +53,34 @@
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 380 180' fill='none' stroke='%231F2A44' stroke-width='1' opacity='0.4'><path d='M0 160 L50 100 L100 140 L160 70 L220 130 L280 90 L330 140 L380 110 L380 180 L0 180 Z'/></svg>");
|
||||
}
|
||||
|
||||
.saju-v2 .k-frame {
|
||||
position: relative;
|
||||
background: rgba(251, 247, 239, 0.9);
|
||||
border: 1px solid rgba(31, 42, 68, 0.10);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset, 0 8px 28px rgba(31, 42, 68, 0.05);
|
||||
}
|
||||
|
||||
.saju-v2 .k-frame::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border: 1px solid rgba(212, 175, 55, 0.16);
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.saju-v2 .k-frame.dark {
|
||||
background: #1F2A44;
|
||||
border: 1px solid rgba(212, 175, 55, 0.4);
|
||||
color: #F7F2E8;
|
||||
box-shadow: 0 1px 0 rgba(212, 175, 55, 0.2) inset, 0 12px 40px rgba(31, 42, 68, 0.2);
|
||||
}
|
||||
|
||||
.saju-v2 .k-frame.dark::before {
|
||||
border-color: rgba(212, 175, 55, 0.25);
|
||||
}
|
||||
|
||||
/* screen entry */
|
||||
@keyframes saju-screen-in {
|
||||
from { transform: translateY(6px); opacity: 0.8; }
|
||||
@@ -76,6 +108,6 @@
|
||||
@media (min-width: 1024px) {
|
||||
.saju-v2 .page {
|
||||
padding-bottom: 0;
|
||||
padding-top: var(--desktop-header-h);
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
51
src/pages/saju/sampleReading.js
Normal file
51
src/pages/saju/sampleReading.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const sampleReading = {
|
||||
id: null,
|
||||
name: '홍길동',
|
||||
birth_year: 1990,
|
||||
birth_month: 5,
|
||||
birth_day: 20,
|
||||
birth_hour: 10,
|
||||
gender: 'male',
|
||||
calendar_type: 'solar',
|
||||
birth_place: '서울특별시',
|
||||
saju_data: {
|
||||
year: { stem: '己', stem_kr: '음토', branch: '巳', branch_kr: '사화', ten_god: '정인', fortune: '丙 庚 戊' },
|
||||
month: { stem: '丙', stem_kr: '양화', branch: '子', branch_kr: '자수', ten_god: '편관', fortune: '壬 癸' },
|
||||
day: { stem: '庚', stem_kr: '양금', branch: '申', branch_kr: '신금', ten_god: '-', fortune: '庚 壬 戊' },
|
||||
hour: { stem: '辛', stem_kr: '음금', branch: '巳', branch_kr: '사화', ten_god: '겁재', fortune: '丙 庚 戊' },
|
||||
},
|
||||
analysis_data: {
|
||||
element_scores: { '木': 20, '火': 35, '土': 25, '金': 55, '水': 30 },
|
||||
day_master_strength: { result: '강함', score: 78, reasons: ['금 기운 우세', '일간 중심 안정'] },
|
||||
},
|
||||
fortune_scores: {
|
||||
overall: 78,
|
||||
wealth: 80,
|
||||
romance: 70,
|
||||
social: 75,
|
||||
career: 82,
|
||||
},
|
||||
lucky: {
|
||||
color: ['#1F2A44', '#E8C76B', '#6B4423', '#D89098', '#F7F2E8'],
|
||||
number: 8,
|
||||
direction: '동쪽',
|
||||
time: '오전 10시 ~ 12시',
|
||||
good_signs: ['작은 기회가 큰 흐름으로 이어질 수 있어요.'],
|
||||
warnings: ['충동적인 결정은 피하고 여유를 가지세요.'],
|
||||
},
|
||||
daeun_data: [
|
||||
{ age: 0, start_year: 1990, end_year: 1999, stem: '戊', branch: '戌' },
|
||||
{ age: 10, start_year: 2000, end_year: 2009, stem: '丁', branch: '酉' },
|
||||
{ age: 20, start_year: 2010, end_year: 2019, stem: '丙', branch: '申' },
|
||||
{ age: 30, start_year: 2020, end_year: 2029, stem: '乙', branch: '未' },
|
||||
{ age: 40, start_year: 2030, end_year: 2039, stem: '甲', branch: '午' },
|
||||
{ age: 50, start_year: 2040, end_year: 2049, stem: '癸', branch: '巳' },
|
||||
{ age: 60, start_year: 2050, end_year: 2059, stem: '壬', branch: '辰' },
|
||||
{ age: 70, start_year: 2060, end_year: 2069, stem: '辛', branch: '卯' },
|
||||
],
|
||||
interpretation_json: {
|
||||
summary: '당신은 강한 의지와 추진력을 가진 분입니다. 새로운 것을 두려워하지 않고 도전하는 용기가 큰 장점이며, 주변 사람에게 신뢰감을 주는 리더형의 흐름이 보입니다.',
|
||||
},
|
||||
};
|
||||
|
||||
export default sampleReading;
|
||||
@@ -1,24 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TitleBlock from '../_shell/TitleBlock';
|
||||
import Mascot from '../_shell/Mascot';
|
||||
import MascotBubble from '../_shell/MascotBubble';
|
||||
import OrnateFrame from '../_shell/OrnateFrame';
|
||||
import PanelHeader from '../_shell/PanelHeader';
|
||||
import DesktopFooter from '../_shell/DesktopFooter';
|
||||
import PrimaryButton from '../_shell/PrimaryButton';
|
||||
import InputRow from '../_shell/InputRow';
|
||||
import { IconSparkle, IconChevron, IconSun, IconHeart, IconYinYang } from '../_shell/Icons';
|
||||
import {
|
||||
IconChevron, IconHeart, IconMoney, IconPaw, IconSparkle, IconSun, IconUser, IconYinYang,
|
||||
} from '../_shell/Icons';
|
||||
import useSajuForm from '../hooks/useSajuForm';
|
||||
|
||||
const ACTIONS = [
|
||||
{ to: '/saju/today', icon: IconSun, label: '오늘의 운세', desc: '오늘 한 줄로 보는 운세', color: '#D4AF37' },
|
||||
{ to: '/saju/compatibility', icon: IconHeart, label: '궁합보기', desc: '두 사람의 만남 풀이', color: '#4E6B5C' },
|
||||
{ to: '/saju/result', icon: IconYinYang, label: '사주풀이', desc: '내 사주 자세히', color: '#6A4C7C' },
|
||||
];
|
||||
|
||||
const inputStyle = {
|
||||
flex: 1, padding: '8px 10px', border: '1px solid rgba(31,42,68,0.12)',
|
||||
borderRadius: 8, background: '#FBF7EF', fontSize: 13, color: '#1F2A44',
|
||||
fontFamily: 'inherit',
|
||||
flex: 1, padding: '8px 10px', border: '1px solid rgba(247,242,232,0.16)',
|
||||
borderRadius: 8, background: 'rgba(247,242,232,0.08)', color: '#F7F2E8',
|
||||
fontSize: 13, fontFamily: 'inherit',
|
||||
};
|
||||
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
@@ -31,103 +26,233 @@ function timeValue(form) {
|
||||
return `${pad(form.hour)}:00`;
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ to: '/saju/today', icon: IconSun, title: '오늘의 운세', desc: '오늘의 흐름과 운세를 한눈에 확인하세요.', color: '#D4AF37' },
|
||||
{ to: '/saju/compatibility', icon: IconHeart, title: '궁합보기', desc: '소중한 인연과 궁합을 확인해 보세요.', color: '#D89098' },
|
||||
{ to: '/saju/result', icon: IconYinYang, title: '사주풀이', desc: '내 사주의 구조와 운세를 자세히 풀이해 드립니다.', color: '#3A5A8C' },
|
||||
];
|
||||
|
||||
export default function HomeDesktop() {
|
||||
const navigate = useNavigate();
|
||||
const { form, handleChange, handleSubmit, loading, error } = useSajuForm();
|
||||
|
||||
const onDate = (e) => {
|
||||
const v = e.target.value;
|
||||
if (!v) { handleChange('year', ''); handleChange('month', ''); handleChange('day', ''); return; }
|
||||
const [y, m, d] = v.split('-');
|
||||
handleChange('year', y);
|
||||
handleChange('month', String(parseInt(m, 10)));
|
||||
handleChange('day', String(parseInt(d, 10)));
|
||||
const value = e.target.value;
|
||||
if (!value) { handleChange('year', ''); handleChange('month', ''); handleChange('day', ''); return; }
|
||||
const [year, month, day] = value.split('-');
|
||||
handleChange('year', year);
|
||||
handleChange('month', String(parseInt(month, 10)));
|
||||
handleChange('day', String(parseInt(day, 10)));
|
||||
};
|
||||
|
||||
const onTime = (e) => {
|
||||
const v = e.target.value;
|
||||
if (!v) { handleChange('hour', ''); return; }
|
||||
const [h] = v.split(':');
|
||||
handleChange('hour', String(parseInt(h, 10)));
|
||||
const value = e.target.value;
|
||||
if (!value) { handleChange('hour', ''); return; }
|
||||
const [hour] = value.split(':');
|
||||
handleChange('hour', String(parseInt(hour, 10)));
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="page mt-wash screen-in">
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '48px 32px' }}>
|
||||
<TitleBlock title="호령이 안내하는 사주"
|
||||
subtitle="오랜 명리학 지혜와 AI 인사이트로 당신만의 길을 비춥니다." />
|
||||
<div style={{
|
||||
marginTop: 32, display: 'grid', gridTemplateColumns: '1fr 480px', gap: 40, alignItems: 'start',
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
|
||||
<Mascot variant="full" size={260} />
|
||||
<MascotBubble tone="ivory" align="left"
|
||||
text={'안녕하세요!\n저는 호령이에요.\n사주를 입력해 보실래요?'}
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 32, display: 'grid', gap: 12 }}>
|
||||
{ACTIONS.map((a) => (
|
||||
<button key={a.to} onClick={() => navigate(a.to)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 16,
|
||||
background: '#FBF7EF', border: `1px solid ${a.color}40`,
|
||||
borderRadius: 12, padding: '16px 20px', color: '#1F2A44',
|
||||
fontSize: 14, fontWeight: 700, letterSpacing: '-0.01em', textAlign: 'left',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
}}>
|
||||
<a.icon size={24} stroke={a.color} strokeWidth={1.8} />
|
||||
<span style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 15 }}>{a.label}</div>
|
||||
<div style={{ fontSize: 12, color: '#6B6B6B', fontWeight: 500, marginTop: 2 }}>{a.desc}</div>
|
||||
</span>
|
||||
<IconChevron dir="right" size={16} color="#B89530" />
|
||||
</button>
|
||||
))}
|
||||
<main className="page mt-wash screen-in" style={{ marginTop: -78, paddingTop: 88 }}>
|
||||
<section style={{
|
||||
maxWidth: 1400, margin: '0 auto', minHeight: 540,
|
||||
padding: '36px 48px 0', position: 'relative', overflow: 'visible',
|
||||
border: '1px solid rgba(31,42,68,0.10)', borderRadius: 32,
|
||||
background:
|
||||
"linear-gradient(90deg, rgba(251,247,239,0.62) 0%, rgba(251,247,239,0.82) 52%, rgba(251,247,239,0.94) 100%), url('/images/saju/horyung/background.png') center top / cover no-repeat",
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.75)',
|
||||
}}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.15fr', gap: 34, alignItems: 'center', minHeight: 480 }}>
|
||||
<div style={{ position: 'relative', alignSelf: 'stretch' }}>
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, top: 142, zIndex: 2,
|
||||
background: 'rgba(251,247,239,0.86)', border: '1px solid rgba(31,42,68,0.12)',
|
||||
borderRadius: 24, padding: '18px 22px', width: 210,
|
||||
boxShadow: '0 8px 22px rgba(31,42,68,0.08)',
|
||||
color: '#1F2A44', fontSize: 14, lineHeight: 1.7, letterSpacing: '-0.02em',
|
||||
}}>
|
||||
안녕하세요!<br />저는 호령이에요.<br />당신의 길을 비춰드릴게요.
|
||||
<div style={{ textAlign: 'right', color: '#B89530', marginTop: 4 }}><IconPaw size={12} /></div>
|
||||
</div>
|
||||
<Mascot variant="full" size={430} style={{
|
||||
position: 'absolute', left: 110, bottom: -20,
|
||||
filter: 'drop-shadow(0 16px 44px rgba(31,42,68,0.18))',
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<OrnateFrame color="#D4AF37" bg="#FBF7EF" double radius={16} padding="24px 22px">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="font-title" style={{
|
||||
fontSize: 18, color: '#1F2A44', marginBottom: 12, textAlign: 'center',
|
||||
}}>사주 입력</div>
|
||||
<InputRow label="이름">
|
||||
<input value={form.name} onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="홍길동" style={inputStyle} />
|
||||
</InputRow>
|
||||
<InputRow label="생년월일">
|
||||
<div style={{ padding: '50px 0 134px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, color: '#A67B3F', marginBottom: 14 }}>
|
||||
<IconSparkle size={14} color="#B89530" />
|
||||
<span style={{ fontSize: 15, fontWeight: 800, letterSpacing: '-0.01em' }}>전통 명리학 × AI 인사이트</span>
|
||||
</div>
|
||||
<h1 className="font-title" style={{
|
||||
margin: 0, fontSize: 56, lineHeight: 1.18,
|
||||
color: '#1F2A44', letterSpacing: '-0.055em',
|
||||
}}>
|
||||
호령이 반갑게<br />
|
||||
맞이하는<br />
|
||||
<span style={{ color: '#A67B3F', whiteSpace: 'nowrap' }}>오늘의 사주</span>
|
||||
</h1>
|
||||
<p style={{
|
||||
margin: '22px 0 0', maxWidth: 560, fontSize: 17,
|
||||
color: '#202638', lineHeight: 1.75, letterSpacing: '-0.02em',
|
||||
}}>
|
||||
오랜 지혜와 AI 분석으로 정확하고 깊이 있는 당신만의 운명을 안내해 드립니다.
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
marginTop: 34, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0,
|
||||
maxWidth: 520, border: '1px solid rgba(31,42,68,0.10)',
|
||||
borderRadius: 28, background: 'rgba(251,247,239,0.78)', overflow: 'hidden',
|
||||
}}>
|
||||
<MiniTrust icon={<IconYinYang size={22} stroke="#B89530" />} title="전통 명리학 기반" desc="정통 사주 해석" />
|
||||
<MiniTrust icon={<IconSparkle size={20} color="#3A5A8C" />} title="AI 분석 인사이트" desc="정확한 인사이트" />
|
||||
<MiniTrust icon={<IconUser size={22} stroke="#3A5A8C" />} title="개인정보 보호" desc="안심 서비스" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute', left: 48, right: 48, bottom: -40,
|
||||
background: '#1F2A44', borderRadius: 22, padding: '18px 22px',
|
||||
border: '1px solid rgba(212,175,55,0.35)',
|
||||
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16,
|
||||
boxShadow: '0 18px 36px rgba(31,42,68,0.22)',
|
||||
zIndex: 3,
|
||||
}}>
|
||||
{FEATURES.map((feature) => (
|
||||
<button key={feature.title} onClick={() => navigate(feature.to)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 18, textAlign: 'left',
|
||||
background: '#FBF7EF', color: '#1F2A44',
|
||||
border: '1px solid rgba(212,175,55,0.42)', borderRadius: 16,
|
||||
padding: '18px 20px',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 58, height: 58, borderRadius: '50%',
|
||||
background: `${feature.color}26`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${feature.color}66`,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<feature.icon size={30} stroke={feature.color} strokeWidth={1.6} />
|
||||
</span>
|
||||
<span style={{ flex: 1 }}>
|
||||
<span className="font-title" style={{ display: 'block', fontSize: 24, letterSpacing: '-0.04em' }}>{feature.title}</span>
|
||||
<span style={{ display: 'block', marginTop: 5, fontSize: 13, lineHeight: 1.5, color: '#3E4456', letterSpacing: '-0.02em' }}>{feature.desc}</span>
|
||||
</span>
|
||||
<IconChevron dir="right" size={16} color="#B89530" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style={{
|
||||
maxWidth: 1160, margin: '88px auto 0', padding: '0 24px',
|
||||
display: 'grid', gridTemplateColumns: '1.15fr 0.85fr', gap: 24,
|
||||
}}>
|
||||
<OrnateFrame color="#D4AF37" bg="rgba(251,247,239,0.86)" radius={18} padding="22px 22px" double>
|
||||
<PanelHeader title="오늘의 운세 한눈에 보기" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '150px 1fr', gap: 20, alignItems: 'center' }}>
|
||||
<div style={{ textAlign: 'center', borderRight: '1px solid rgba(31,42,68,0.08)' }}>
|
||||
<div className="font-title" style={{ fontSize: 58, color: '#1F2A44', lineHeight: 1 }}>78</div>
|
||||
<div style={{ fontSize: 18, color: '#1F2A44' }}>/100</div>
|
||||
<div style={{ marginTop: 8, fontSize: 14, fontWeight: 700, color: '#1F2A44' }}>종합운</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-title" style={{ fontSize: 22, color: '#B89530', letterSpacing: '-0.03em' }}>
|
||||
새로운 기회가 찾아오는 날입니다.
|
||||
</div>
|
||||
<p style={{ margin: '8px 0 14px', color: '#3E4456', fontSize: 13, lineHeight: 1.7 }}>
|
||||
작은 실천이 큰 변화를 만듭니다. 주변의 조언에 귀 기울여 보세요.
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
<ScorePill icon={<IconMoney size={15} stroke="#D4AF37" />} label="재물운" value="80" />
|
||||
<ScorePill icon={<IconHeart size={15} stroke="#D89098" />} label="연애운" value="70" />
|
||||
<ScorePill icon={<IconSun size={15} stroke="#4E6B5C" />} label="건강운" value="75" />
|
||||
<ScorePill icon={<IconUser size={15} stroke="#3A5A8C" />} label="직장운" value="82" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OrnateFrame>
|
||||
|
||||
<OrnateFrame color="#D4AF37" bg="#1F2A44" radius={18} padding="22px 28px" double>
|
||||
<form onSubmit={handleSubmit} style={{ color: '#F7F2E8' }}>
|
||||
<div className="font-title" style={{ fontSize: 22, color: '#E8C76B', textAlign: 'center', letterSpacing: '-0.03em' }}>
|
||||
사주풀이를 시작해 보세요
|
||||
</div>
|
||||
<div style={{ marginTop: 6, color: '#D9D2C0', fontSize: 13, textAlign: 'center' }}>
|
||||
정확한 사주 분석을 위해 생년월일시를 입력해 주세요.
|
||||
</div>
|
||||
<div style={{ marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 10px' }}>
|
||||
<DarkInputRow label="이름">
|
||||
<input value={form.name} onChange={(e) => handleChange('name', e.target.value)} placeholder="홍길동" style={inputStyle} />
|
||||
</DarkInputRow>
|
||||
<DarkInputRow label="생년월일">
|
||||
<input type="date" value={dateValue(form)} onChange={onDate} style={inputStyle} />
|
||||
</InputRow>
|
||||
<InputRow label="시간">
|
||||
</DarkInputRow>
|
||||
<DarkInputRow label="시간">
|
||||
<input type="time" value={timeValue(form)} onChange={onTime} style={inputStyle} />
|
||||
</InputRow>
|
||||
<InputRow label="성별">
|
||||
<select value={form.gender} onChange={(e) => handleChange('gender', e.target.value)}
|
||||
style={inputStyle}>
|
||||
</DarkInputRow>
|
||||
<DarkInputRow label="성별">
|
||||
<select value={form.gender} onChange={(e) => handleChange('gender', e.target.value)} style={inputStyle}>
|
||||
<option value="male">남</option>
|
||||
<option value="female">여</option>
|
||||
</select>
|
||||
</InputRow>
|
||||
<InputRow label="달력">
|
||||
<select value={form.calendar_type}
|
||||
onChange={(e) => handleChange('calendar_type', e.target.value)} style={inputStyle}>
|
||||
<option value="solar">양력</option>
|
||||
<option value="lunar">음력</option>
|
||||
</select>
|
||||
</InputRow>
|
||||
{error && (
|
||||
<div style={{ padding: '10px 14px', color: '#C04A4A', fontSize: 12 }}>{error}</div>
|
||||
)}
|
||||
<div style={{ padding: '14px 14px 6px' }}>
|
||||
<PrimaryButton color="#6A4C7C" type="submit">
|
||||
{loading ? '호령이 풀이 중...' : '내 사주 보기'}
|
||||
{!loading && <IconSparkle size={12} color="#E8C76B" />}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</OrnateFrame>
|
||||
</div>
|
||||
</div>
|
||||
</DarkInputRow>
|
||||
</div>
|
||||
<DarkInputRow label="달력">
|
||||
<select value={form.calendar_type} onChange={(e) => handleChange('calendar_type', e.target.value)} style={inputStyle}>
|
||||
<option value="solar">양력</option>
|
||||
<option value="lunar">음력</option>
|
||||
</select>
|
||||
</DarkInputRow>
|
||||
{error && <div style={{ marginTop: 10, fontSize: 12, color: '#F2C7CD', textAlign: 'center' }}>{error}</div>}
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<PrimaryButton color="#D9AD61" type="submit" style={{ color: '#1F2A44' }}>
|
||||
{loading ? '호령이 풀이 중...' : '사주풀이 시작하기'}
|
||||
{!loading && <IconPaw size={14} color="#1F2A44" />}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</OrnateFrame>
|
||||
</section>
|
||||
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniTrust({ icon, title, desc }) {
|
||||
return (
|
||||
<div style={{ padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, borderRight: '1px solid rgba(31,42,68,0.08)' }}>
|
||||
<span style={{ width: 38, height: 38, borderRadius: '50%', background: 'rgba(31,42,68,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{icon}</span>
|
||||
<span>
|
||||
<span style={{ display: 'block', fontSize: 12, color: '#1F2A44', fontWeight: 800, whiteSpace: 'nowrap' }}>{title}</span>
|
||||
<span style={{ display: 'block', marginTop: 2, fontSize: 11, color: '#6B6B6B', whiteSpace: 'nowrap' }}>{desc}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScorePill({ icon, label, value }) {
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid rgba(31,42,68,0.10)', borderRadius: 12,
|
||||
background: 'rgba(251,247,239,0.82)', padding: '9px 10px',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
{icon}
|
||||
<span style={{ fontSize: 12, color: '#1F2A44', fontWeight: 700 }}>{label}</span>
|
||||
<span className="font-title" style={{ marginLeft: 'auto', color: '#1F2A44', fontSize: 17 }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DarkInputRow({ label, children }) {
|
||||
return (
|
||||
<label style={{ display: 'grid', gap: 5 }}>
|
||||
<span style={{ fontSize: 11, color: '#D9D2C0', fontWeight: 700 }}>{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
232
src/pages/saju/views/match-result.desktop.jsx
Normal file
232
src/pages/saju/views/match-result.desktop.jsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DesktopHero from '../_shell/DesktopHero';
|
||||
import DesktopFooter from '../_shell/DesktopFooter';
|
||||
import PanelHeader from '../_shell/PanelHeader';
|
||||
import {
|
||||
IconChevron, IconHeart, IconPaw, IconSparkle, IconSun, IconUser,
|
||||
} from '../_shell/Icons';
|
||||
import hexA from '../_shell/helpers/hexA';
|
||||
|
||||
export default function MatchResultDesktop({ result }) {
|
||||
const navigate = useNavigate();
|
||||
const interp = result?.interpretation_json || {};
|
||||
const score = Math.round(result?.score || interp.score || 86);
|
||||
const names = {
|
||||
a: result?.person_a?.name || '나',
|
||||
b: result?.person_b?.name || '상대방',
|
||||
};
|
||||
const strengths = interp.strengths?.length ? interp.strengths : ['서로에게 긍정적인 영향을 주며 함께 목표를 이루기 좋아요.'];
|
||||
const challenges = interp.challenges?.length ? interp.challenges : ['감정 표현 방식이 달라 오해가 생길 수 있으니 배려가 필요해요.'];
|
||||
const summary = interp.summary || '두 분은 서로의 부족한 부분을 채워주며 함께 성장해 나갈 수 있는 좋은 인연이에요. 서로의 다름을 인정하고 존중한다면 더욱 길고 단단한 관계로 발전할 수 있습니다.';
|
||||
|
||||
return (
|
||||
<main className="page paper-bg screen-in" style={{ marginTop: -78, paddingTop: 78 }}>
|
||||
<DesktopHero
|
||||
title="궁합보기"
|
||||
subtitle="소중한 인연의 흐름을 살펴보세요."
|
||||
accent="#4E6B5C"
|
||||
bubble={<div>두 분의 인연을<br />제가 잘 살펴봤어요!<br />함께 행복한 길을 걸어가세요.</div>}
|
||||
/>
|
||||
|
||||
<div style={{ maxWidth: 1320, margin: '0 auto', padding: '0 36px 36px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: 18, alignItems: 'center' }}>
|
||||
<PersonSummary label="나" name={names.a} chipColor="#4E6B5C" chipBg="#E6EBE5" />
|
||||
<div style={{
|
||||
width: 70, height: 70, borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #F2C7CD, #D89098)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 24px rgba(216,144,152,0.38), inset 0 1px 0 rgba(255,255,255,0.5)',
|
||||
}}>
|
||||
<IconHeart size={34} stroke="#FFF" strokeWidth={2} />
|
||||
</div>
|
||||
<PersonSummary label="상대방" name={names.b} chipColor="#D89098" chipBg="#FBE8EB" />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 18, display: 'grid', gridTemplateColumns: '360px 1fr', gap: 18 }}>
|
||||
<div className="k-frame dark" style={{
|
||||
padding: 28, textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#E8C76B' }}>
|
||||
<span style={{ width: 28, height: 1, background: '#E8C76B' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 800, letterSpacing: '0.12em' }}>궁합 점수</span>
|
||||
<span style={{ width: 28, height: 1, background: '#E8C76B' }} />
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 82, color: '#F7F2E8', letterSpacing: '-0.05em', lineHeight: 1 }}>
|
||||
{score}<span style={{ fontSize: 28, color: '#E8C76B', fontWeight: 400 }}>점</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#D9D2C0' }}>
|
||||
상위권의 좋은 궁합이에요.
|
||||
</div>
|
||||
<div style={{ width: '84%', height: 7, borderRadius: 999, background: 'rgba(247,242,232,0.1)', marginTop: 8, overflow: 'hidden' }}>
|
||||
<div style={{ width: `${Math.min(100, score)}%`, height: '100%', background: 'linear-gradient(90deg, #B89530, #E8C76B, #D89098)', borderRadius: 999 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
|
||||
<SubScoreCard color="#D4AF37" icon={<IconSun size={18} stroke="#D4AF37" />} label="성향 궁합" score={Math.min(100, score + 2)} desc="가치관과 성향이 조화를 이룹니다." />
|
||||
<SubScoreCard color="#3A5A8C" icon={<SpeechIcon size={18} stroke="#3A5A8C" />} label="대화 궁합" score={Math.max(0, score - 4)} desc="편안한 대화를 나눌 수 있어요." />
|
||||
<SubScoreCard color="#D89098" icon={<IconHeart size={18} stroke="#D89098" />} label="연애 궁합" score={Math.min(100, score + 4)} desc="설렘과 안정감을 함께 줍니다." />
|
||||
<SubScoreCard color="#A67B3F" icon={<RingIcon />} label="결혼 궁합" score={Math.max(0, score - 2)} desc="함께 미래를 그리기 좋은 균형입니다." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 18, display: 'grid', gridTemplateColumns: '1fr 1.4fr 1fr', gap: 18 }}>
|
||||
<div className="k-frame" style={{ padding: '22px 24px' }}>
|
||||
<PanelHeader title="오행 균형" accent="#4E6B5C" />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative', padding: '8px 0 18px' }}>
|
||||
<Circle label="나" sub="목(木)" color="#4E6B5C" />
|
||||
<Circle label="상생의" sub="흐름" color="#1F2A44" center />
|
||||
<Circle label="상대방" sub="화(火)" color="#D89098" />
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#6B6B6B', lineHeight: 1.7, textAlign: 'center' }}>
|
||||
서로를 북돋우는 상생의 기운이 강합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="k-frame" style={{ padding: '22px 24px' }}>
|
||||
<PanelHeader title="궁합 해석" accent="#4E6B5C" />
|
||||
<div style={{ fontSize: 14, color: '#1F2A44', lineHeight: 1.85, whiteSpace: 'pre-line' }}>{summary}</div>
|
||||
<div style={{
|
||||
marginTop: 14, padding: '14px 16px', borderRadius: 12,
|
||||
background: 'rgba(78,107,92,0.06)', border: '1px dashed rgba(78,107,92,0.3)',
|
||||
fontSize: 14, color: '#4E6B5C', lineHeight: 1.65, textAlign: 'center',
|
||||
}}>
|
||||
서로에게 따뜻한 빛이 되어주는 인연입니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="k-frame" style={{ padding: '22px 24px' }}>
|
||||
<PanelHeader title="한눈에 보는 궁합 요약" accent="#4E6B5C" />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<SummaryItem color="#4E6B5C" title="좋은 점" desc={strengths[0]} />
|
||||
<SummaryItem color="#D89098" title="조심할 점" desc={challenges[0]} />
|
||||
<SummaryItem color="#3A5A8C" title="추천 대화법" desc="감정을 솔직히 표현하고 상대의 이야기를 끝까지 들어주세요." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'center', gap: 14 }}>
|
||||
<button onClick={() => navigate('/saju/compatibility')} style={buttonGhost()}>
|
||||
<IconChevron dir="left" size={13} color="#1F2A44" /> 새로운 궁합 보기
|
||||
</button>
|
||||
<button onClick={() => navigate('/saju')} style={buttonPrimary()}>
|
||||
사주풀이 시작하기 <IconPaw size={13} color="#E8C76B" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonSummary({ label, name, chipColor, chipBg }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '20px 22px', display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<span style={{
|
||||
padding: '5px 16px', borderRadius: 999,
|
||||
background: chipBg, color: chipColor, fontSize: 13, fontWeight: 800,
|
||||
border: `1px solid ${hexA(chipColor, 0.4)}`,
|
||||
}}>{label}</span>
|
||||
<span style={{ fontSize: 18, fontWeight: 800, color: '#1F2A44' }}>{name}</span>
|
||||
<span style={{ padding: '3px 9px', borderRadius: 8, background: 'rgba(212,175,55,0.10)', color: '#B89530', fontSize: 11, fontWeight: 800 }}>양력</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: '50%',
|
||||
background: chipBg, border: `1px solid ${hexA(chipColor, 0.35)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<IconUser size={24} stroke={chipColor} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubScoreCard({ color, icon, label, score, desc }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '20px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
width: 34, height: 34, borderRadius: '50%',
|
||||
background: hexA(color, 0.10), border: `1px solid ${hexA(color, 0.35)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>{icon}</div>
|
||||
<span className="font-title" style={{ fontSize: 18, color: '#1F2A44', letterSpacing: '-0.03em' }}>{label}</span>
|
||||
</div>
|
||||
<div style={{ height: 6, marginTop: 16, borderRadius: 999, background: 'rgba(31,42,68,0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ width: `${score}%`, height: '100%', background: color, borderRadius: 999 }} />
|
||||
</div>
|
||||
<div className="font-title" style={{ marginTop: 10, fontSize: 24, color, textAlign: 'center' }}>
|
||||
{score}<span style={{ fontSize: 13, color: '#1F2A44', fontWeight: 400 }}>점</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 7, fontSize: 12, color: '#6B6B6B', lineHeight: 1.55, textAlign: 'center' }}>{desc}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Circle({ label, sub, color, center }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: center ? 108 : 104, height: center ? 108 : 104, borderRadius: '50%',
|
||||
background: color, color: '#F7F2E8',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column',
|
||||
marginLeft: center ? -20 : 0, marginRight: center ? -20 : 0, zIndex: center ? 2 : 1,
|
||||
border: center ? '2px solid #D4AF37' : 'none',
|
||||
boxShadow: center ? '0 10px 24px rgba(31,42,68,0.18)' : 'none',
|
||||
}}>
|
||||
<span className="font-title" style={{ fontSize: center ? 14 : 18, color: center ? '#E8C76B' : '#F7F2E8' }}>{label}</span>
|
||||
<span style={{ fontSize: 12, opacity: 0.9 }}>{sub}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryItem({ color, title, desc }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: '50%',
|
||||
background: hexA(color, 0.12), border: `1px solid ${hexA(color, 0.35)}`,
|
||||
flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color, fontSize: 12, fontWeight: 800,
|
||||
}}>{title[0]}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 800, color: '#1F2A44', marginBottom: 3 }}>{title}</div>
|
||||
<div style={{ fontSize: 12, color: '#6B6B6B', lineHeight: 1.6 }}>{desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buttonPrimary() {
|
||||
return {
|
||||
padding: '14px 26px', borderRadius: 999, background: '#1F2A44', color: '#F7F2E8',
|
||||
border: '1px solid rgba(212,175,55,0.4)', fontSize: 14, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
};
|
||||
}
|
||||
|
||||
function buttonGhost() {
|
||||
return {
|
||||
padding: '14px 26px', borderRadius: 999, background: '#FBF7EF', color: '#1F2A44',
|
||||
border: '1px solid rgba(31,42,68,0.22)', fontSize: 14, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
};
|
||||
}
|
||||
|
||||
function SpeechIcon({ size = 16, stroke = '#3A5A8C' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 5h16v11H10l-4 4v-4H4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function RingIcon({ size = 18, stroke = '#A67B3F' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="1.7">
|
||||
<path d="M9 6l3-3 3 3" />
|
||||
<circle cx="12" cy="16" r="5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,209 @@
|
||||
import React from 'react';
|
||||
import MatchMobile from './match.mobile.jsx';
|
||||
import DesktopHero from '../_shell/DesktopHero';
|
||||
import DesktopFooter from '../_shell/DesktopFooter';
|
||||
import PanelHeader from '../_shell/PanelHeader';
|
||||
import PrimaryButton from '../_shell/PrimaryButton';
|
||||
import {
|
||||
IconCalendar, IconClock, IconHeart, IconPaw, IconSparkle, IconUser,
|
||||
} from '../_shell/Icons';
|
||||
import hexA from '../_shell/helpers/hexA';
|
||||
|
||||
export default function MatchDesktop(props) {
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
function dateValue(person) {
|
||||
if (!person.year || !person.month || !person.day) return '';
|
||||
return `${person.year}-${pad(person.month)}-${pad(person.day)}`;
|
||||
}
|
||||
function timeValue(person) {
|
||||
if (person.hour === '' || person.hour == null) return '';
|
||||
return `${pad(person.hour)}:00`;
|
||||
}
|
||||
function onDate(person, onChange, event) {
|
||||
const value = event.target.value;
|
||||
if (!value) return onChange({ ...person, year: '', month: '', day: '' });
|
||||
const [year, month, day] = value.split('-');
|
||||
return onChange({ ...person, year: parseInt(year, 10), month: parseInt(month, 10), day: parseInt(day, 10) });
|
||||
}
|
||||
function onTime(person, onChange, event) {
|
||||
const value = event.target.value;
|
||||
if (!value) return onChange({ ...person, hour: null });
|
||||
const [hour] = value.split(':');
|
||||
return onChange({ ...person, hour: parseInt(hour, 10) });
|
||||
}
|
||||
|
||||
export default function MatchDesktop({
|
||||
personA, personB, onChangeA, onChangeB, onSubmit, loading, error,
|
||||
}) {
|
||||
return (
|
||||
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
||||
<MatchMobile {...props} />
|
||||
<main className="page paper-bg screen-in" style={{ marginTop: -78, paddingTop: 78 }}>
|
||||
<DesktopHero
|
||||
title="궁합보기"
|
||||
subtitle="소중한 인연의 흐름을 살펴보세요."
|
||||
accent="#4E6B5C"
|
||||
bubble={<div>두 분의 인연을<br />제가 잘 살펴봐드릴게요!<br />함께 행복한 길을 걸어가시길 바라요.</div>}
|
||||
/>
|
||||
|
||||
<form onSubmit={onSubmit} style={{ maxWidth: 1320, margin: '0 auto', padding: '0 36px 36px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 72px 1fr', gap: 18, alignItems: 'center' }}>
|
||||
<PersonCard
|
||||
label="나"
|
||||
chipColor="#4E6B5C"
|
||||
chipBg="#E6EBE5"
|
||||
avatarBg="#E7ECF3"
|
||||
person={personA}
|
||||
onChange={onChangeA}
|
||||
/>
|
||||
<div style={{
|
||||
width: 68, height: 68, borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #F2C7CD, #D89098)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 24px rgba(216,144,152,0.38), inset 0 1px 0 rgba(255,255,255,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.5)',
|
||||
}}>
|
||||
<IconHeart size={34} stroke="#FFF" strokeWidth={2} />
|
||||
</div>
|
||||
<PersonCard
|
||||
label="상대방"
|
||||
chipColor="#D89098"
|
||||
chipBg="#FBE8EB"
|
||||
avatarBg="#FBE8EB"
|
||||
person={personB}
|
||||
onChange={onChangeB}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ marginTop: 16, textAlign: 'center', color: '#C04A4A', fontSize: 13, fontWeight: 700 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="k-frame" style={{ marginTop: 18, padding: '22px 28px', display: 'grid', gridTemplateColumns: '1fr auto', gap: 24, alignItems: 'center' }}>
|
||||
<div>
|
||||
<PanelHeader title="궁합 분석 준비" accent="#4E6B5C" />
|
||||
<div style={{ marginTop: -8, fontSize: 13, color: '#6B6B6B', lineHeight: 1.7 }}>
|
||||
두 사람의 사주를 바탕으로 성향, 대화, 연애, 결혼 가능성까지 다양한 측면에서 조화와 흐름을 분석합니다.
|
||||
</div>
|
||||
</div>
|
||||
<PrimaryButton color="#1F2A44" type="submit" full={false} style={{ borderRadius: 999, minWidth: 220 }}>
|
||||
{loading ? '호령이 비교 중...' : '궁합보기 시작'}
|
||||
{!loading && <IconPaw size={13} color="#E8C76B" />}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonCard({ label, chipColor, chipBg, avatarBg, person, onChange }) {
|
||||
const activeGender = person.gender || 'male';
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 18 }}>
|
||||
<span style={{
|
||||
padding: '5px 16px', borderRadius: 999,
|
||||
background: chipBg, color: chipColor, fontSize: 14, fontWeight: 800,
|
||||
border: `1px solid ${hexA(chipColor, 0.4)}`, letterSpacing: '-0.02em',
|
||||
}}>{label}</span>
|
||||
<input
|
||||
value={person.name || ''}
|
||||
onChange={(event) => onChange({ ...person, name: event.target.value })}
|
||||
placeholder="이름"
|
||||
style={{
|
||||
flex: 1, minWidth: 120, padding: '12px 14px', borderRadius: 10,
|
||||
border: '1px solid rgba(31,42,68,0.12)', background: '#FBF7EF',
|
||||
color: '#1F2A44', fontSize: 15, fontWeight: 700,
|
||||
}}
|
||||
/>
|
||||
<span style={{
|
||||
padding: '4px 10px', borderRadius: 8, background: 'rgba(212,175,55,0.10)',
|
||||
color: '#B89530', fontSize: 11, fontWeight: 800,
|
||||
}}>{person.calendar_type === 'lunar' ? '음력' : '양력'}</span>
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: '50%',
|
||||
background: avatarBg, border: `1px solid ${hexA(chipColor, 0.28)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', color: chipColor,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<IconUser size={30} stroke={chipColor} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 170px', gap: 10 }}>
|
||||
<FieldPill icon={<IconCalendar size={15} stroke="#B89530" />}>
|
||||
<input type="date" value={dateValue(person)} onChange={(event) => onDate(person, onChange, event)} style={fieldInputStyle} />
|
||||
</FieldPill>
|
||||
<select
|
||||
value={person.calendar_type}
|
||||
onChange={(event) => onChange({ ...person, calendar_type: event.target.value })}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="solar">양력</option>
|
||||
<option value="lunar">음력</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 140px', gap: 10, marginTop: 10 }}>
|
||||
<FieldPill icon={<IconClock size={15} stroke="#B89530" />}>
|
||||
<input type="time" value={timeValue(person)} onChange={(event) => onTime(person, onChange, event)} style={fieldInputStyle} />
|
||||
</FieldPill>
|
||||
<div style={{ display: 'flex', borderRadius: 10, overflow: 'hidden', border: '1px solid rgba(31,42,68,0.12)' }}>
|
||||
{[
|
||||
['male', '남'],
|
||||
['female', '여'],
|
||||
].map(([value, text]) => {
|
||||
const active = activeGender === value;
|
||||
return (
|
||||
<button key={value} type="button" onClick={() => onChange({ ...person, gender: value })} style={{
|
||||
flex: 1, border: 'none', padding: '11px 0',
|
||||
background: active ? chipColor : '#FBF7EF',
|
||||
color: active ? '#F7F2E8' : '#6B6B6B',
|
||||
fontSize: 13, fontWeight: 800,
|
||||
}}>{text}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => onChange({ name: '', year: '', month: '', day: '', hour: null, gender: activeGender, calendar_type: 'solar' })} style={{
|
||||
margin: '14px auto 0', display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: 'transparent', border: 'none', color: '#6B6B6B', fontSize: 12, fontWeight: 700,
|
||||
}}>
|
||||
<IconSparkle size={11} color="#B89530" /> 다시 입력하기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldPill({ icon, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '0 12px', borderRadius: 10,
|
||||
background: '#FBF7EF', border: '1px solid rgba(31,42,68,0.12)',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
{icon}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fieldInputStyle = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#1F2A44',
|
||||
fontSize: 14,
|
||||
fontFamily: 'inherit',
|
||||
padding: '11px 0',
|
||||
};
|
||||
|
||||
const selectStyle = {
|
||||
border: '1px solid rgba(31,42,68,0.12)',
|
||||
borderRadius: 10,
|
||||
background: '#FBF7EF',
|
||||
color: '#1F2A44',
|
||||
fontSize: 14,
|
||||
fontFamily: 'inherit',
|
||||
padding: '0 12px',
|
||||
};
|
||||
|
||||
@@ -1,10 +1,499 @@
|
||||
import React from 'react';
|
||||
import SajuMobile from './saju.mobile.jsx';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DesktopHero from '../_shell/DesktopHero';
|
||||
import DesktopFooter from '../_shell/DesktopFooter';
|
||||
import PanelHeader from '../_shell/PanelHeader';
|
||||
import OrnamentBloom from '../_shell/OrnamentBloom';
|
||||
import { IconChevron, IconPaw } from '../_shell/Icons';
|
||||
import deriveTraits from '../_shell/helpers/deriveTraits';
|
||||
import daeunLabel from '../_shell/helpers/daeunLabel';
|
||||
import hexA from '../_shell/helpers/hexA';
|
||||
|
||||
const HANJA_TO_ID = { '木': 'wood', '火': 'fire', '土': 'earth', '金': 'metal', '水': 'water' };
|
||||
const ID_TO_KO = { wood: '목', fire: '화', earth: '토', metal: '금', water: '수' };
|
||||
const ID_TO_CH = { wood: '木', fire: '火', earth: '土', metal: '金', water: '水' };
|
||||
const ID_TO_COLOR = {
|
||||
wood: '#4E6B5C', fire: '#C04A4A', earth: '#A67B3F',
|
||||
metal: '#D4AF37', water: '#3A5A8C',
|
||||
};
|
||||
const STEM_EL = { '甲': 'wood', '乙': 'wood', '丙': 'fire', '丁': 'fire', '戊': 'earth', '己': 'earth', '庚': 'metal', '辛': 'metal', '壬': 'water', '癸': 'water' };
|
||||
const BRANCH_EL = { '子': 'water', '丑': 'earth', '寅': 'wood', '卯': 'wood', '辰': 'earth', '巳': 'fire', '午': 'fire', '未': 'earth', '申': 'metal', '酉': 'metal', '戌': 'earth', '亥': 'water' };
|
||||
|
||||
const PILLAR_LABELS = { year: '년주', month: '월주', day: '일주', hour: '시주' };
|
||||
|
||||
function elementsByEngId(scores = {}) {
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(scores || {})) {
|
||||
const id = HANJA_TO_ID[key] || key;
|
||||
if (ID_TO_KO[id]) out[id] = Number(value) || 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function maxElement(elementsObj) {
|
||||
return ['wood', 'fire', 'earth', 'metal', 'water']
|
||||
.map((id) => ({ id, value: Math.round(elementsObj[id] || 0), color: ID_TO_COLOR[id] }))
|
||||
.reduce((best, item) => (item.value > best.value ? item : best), { id: 'metal', value: 0, color: '#D4AF37' });
|
||||
}
|
||||
|
||||
function normalizePillar(pillar = {}, key) {
|
||||
const stem = pillar.stem || '-';
|
||||
const branch = pillar.branch || '-';
|
||||
return {
|
||||
id: key,
|
||||
label: PILLAR_LABELS[key],
|
||||
cheongan: {
|
||||
ch: stem,
|
||||
ko: pillar.stem_kr || '',
|
||||
mark: stem && STEM_EL[stem] ? `(${ID_TO_KO[STEM_EL[stem]]})` : '',
|
||||
color: ID_TO_COLOR[STEM_EL[stem]] || '#1F2A44',
|
||||
},
|
||||
jiji: {
|
||||
ch: branch,
|
||||
ko: pillar.branch_kr || '',
|
||||
mark: branch && BRANCH_EL[branch] ? `(${ID_TO_KO[BRANCH_EL[branch]]})` : '',
|
||||
color: ID_TO_COLOR[BRANCH_EL[branch]] || '#1F2A44',
|
||||
},
|
||||
sipsin: pillar.ten_god || '-',
|
||||
jijang: pillar.hidden_stems || pillar.fortune || '-',
|
||||
};
|
||||
}
|
||||
|
||||
function readingToDesktopData(reading) {
|
||||
const saju = reading?.saju_data || {};
|
||||
const elementsObj = elementsByEngId(reading?.analysis_data?.element_scores);
|
||||
const strongest = maxElement(elementsObj);
|
||||
const pillars = ['year', 'month', 'day', 'hour'].map((key) => normalizePillar(saju[key], key));
|
||||
const daeun = (reading?.daeun_data || []).map((item) => ({
|
||||
age: `${item.age}~${item.age + 9}세`,
|
||||
rawAge: item.age,
|
||||
gan: item.stem || item.gan || '-',
|
||||
label: daeunLabel(item.age),
|
||||
current: item.start_year <= new Date().getFullYear() && new Date().getFullYear() <= item.end_year,
|
||||
startYear: item.start_year,
|
||||
endYear: item.end_year,
|
||||
}));
|
||||
const fallbackDaeun = [0, 10, 20, 30, 40, 50, 60, 70].map((age, index) => ({
|
||||
age: `${age}~${age + 9}세`,
|
||||
rawAge: age,
|
||||
gan: ['戊', '丁', '丙', '乙', '甲', '癸', '壬', '辛'][index],
|
||||
label: daeunLabel(age),
|
||||
current: age === 30,
|
||||
}));
|
||||
|
||||
return {
|
||||
name: reading?.name || '백호',
|
||||
gender: reading?.gender === 'female' ? '여' : '남',
|
||||
birth: `${reading?.birth_year || '1990'}년 ${reading?.birth_month || '01'}월 ${reading?.birth_day || '01'}일 ${reading?.birth_hour ?? '10'}:00`,
|
||||
lunar: reading?.calendar_type === 'lunar' ? '음력 입력' : '양력 입력',
|
||||
birthPlace: reading?.birth_place || '서울특별시',
|
||||
ilgan: pillars[2]?.cheongan || { ch: '庚', color: '#3A5A8C' },
|
||||
pillars,
|
||||
elementsObj,
|
||||
ohaeng: ['wood', 'fire', 'earth', 'metal', 'water'].map((id) => ({
|
||||
id, ko: ID_TO_KO[id], ch: ID_TO_CH[id],
|
||||
value: Math.round(elementsObj[id] || ({ wood: 20, fire: 35, earth: 25, metal: 55, water: 30 }[id])),
|
||||
color: ID_TO_COLOR[id],
|
||||
})),
|
||||
strongest,
|
||||
summary: reading?.interpretation_json?.summary || '의리가 강하고 책임감이 뛰어난 흐름입니다. 목표를 정하면 끝까지 해내는 추진력과 원칙을 중시하는 태도가 장점으로 드러납니다.',
|
||||
traits: deriveTraits(elementsObj, []),
|
||||
daeun: daeun.length ? daeun : fallbackDaeun,
|
||||
dayMasterStrength: reading?.analysis_data?.day_master_strength,
|
||||
};
|
||||
}
|
||||
|
||||
export default function SajuDesktop({ reading }) {
|
||||
const navigate = useNavigate();
|
||||
const data = readingToDesktopData(reading);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
||||
<SajuMobile reading={reading} />
|
||||
<main className="page paper-bg screen-in" style={{ marginTop: -78, paddingTop: 78 }}>
|
||||
<DesktopHero
|
||||
title="사주풀이"
|
||||
subtitle="당신의 사주 구조와 흐름을 깊이 있게 풀어드립니다."
|
||||
accent="#D4AF37"
|
||||
bubble={<div>사주의 흐름을 읽고,<br />당신의 길을 밝혀드립니다.</div>}
|
||||
/>
|
||||
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto', padding: '0 36px 32px' }}>
|
||||
<BasicInfoBar data={data} onEdit={() => navigate('/saju')} />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 360px', gap: 18, marginTop: 20 }}>
|
||||
<SajuStructureCard data={data} />
|
||||
<OhaengCard data={data} />
|
||||
<HoryungInsightCard data={data} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 12, marginTop: 18 }}>
|
||||
<TraitDeskCard color="#A67B3F" iconName="will" title="핵심 성향" body={data.summary} />
|
||||
<TraitDeskCard color="#4E6B5C" iconName="adapt" title="강점" bullets={data.traits.slice(0, 4).map((t) => t.ko || t.label)} />
|
||||
<TraitDeskCard color="#C04A4A" iconName="challenge" title="주의할 점" bullets={['고집이 강할 수 있음', '완벽주의 경향', '휴식이 부족해지기 쉬움']} />
|
||||
<TraitDeskCard color="#3A5A8C" iconName="lead" title="직업운" body={`${ID_TO_KO[data.strongest.id]}(${ID_TO_CH[data.strongest.id]}) 기운을 중심으로 체계적이고 집중력이 필요한 분야에서 강점이 드러납니다.`} />
|
||||
<TraitDeskCard color="#D89098" iconName="heart" title="연애운" body="신뢰와 안정감을 중시하며 깊이 있는 관계를 만들어갑니다. 따뜻한 표현이 관계의 열쇠입니다." />
|
||||
</div>
|
||||
|
||||
<DaeunDeskCard data={data} />
|
||||
<ConsultCTA onClick={() => navigate('/saju/me')} />
|
||||
</div>
|
||||
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function BasicInfoBar({ data, onEdit }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '18px 24px', display: 'flex', alignItems: 'center', gap: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: 10,
|
||||
background: 'rgba(212,175,55,0.10)', border: '1px solid rgba(212,175,55,0.4)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#B89530',
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#B89530" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="5" y="4" width="14" height="17" rx="2" />
|
||||
<path d="M8 8h8M8 12h8M8 16h5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 18, color: '#1F2A44', letterSpacing: '-0.02em' }}>기본 정보</div>
|
||||
</div>
|
||||
<InfoCol label="이름" value={data.name} />
|
||||
<InfoCol label="성별" value={data.gender} />
|
||||
<InfoCol label="양력" value={data.birth} />
|
||||
<InfoCol label="음력" value={data.lunar} />
|
||||
<InfoCol label="출생지" value={data.birthPlace} />
|
||||
<InfoCol label="사주명리" value={<span>양 · 일간 <span className="font-title" style={{ color: data.ilgan.color, fontSize: 14 }}>{data.ilgan.ch}</span></span>} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={onEdit} style={{
|
||||
padding: '8px 18px', borderRadius: 999,
|
||||
background: 'transparent', border: '1px solid rgba(31,42,68,0.2)',
|
||||
color: '#6B6B6B', fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
|
||||
}}>정보 수정</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoCol({ label, value }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#9A968D', letterSpacing: '-0.01em', fontWeight: 700 }}>{label}</div>
|
||||
<div style={{ fontSize: 13, color: '#1F2A44', whiteSpace: 'nowrap', letterSpacing: '-0.01em' }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SajuStructureCard({ data }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '20px 22px' }}>
|
||||
<PanelHeader title="사주 구조" />
|
||||
<table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: 0 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle()} />
|
||||
{data.pillars.map((pillar) => (
|
||||
<th key={pillar.id} style={thStyle({ active: pillar.id === 'day' })}>
|
||||
{pillar.id === 'day' && (
|
||||
<div style={{
|
||||
fontSize: 9, fontWeight: 700, color: '#F7F2E8', background: '#6A4C7C',
|
||||
padding: '2px 8px', borderRadius: 99, display: 'inline-block', marginBottom: 4,
|
||||
}}>일간</div>
|
||||
)}
|
||||
<div style={{ fontSize: 12, color: '#1F2A44', fontWeight: 700 }}>{pillar.label}</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Row label="천간" cells={data.pillars.map((pillar) => pillar.cheongan)} day />
|
||||
<Row label="지지" cells={data.pillars.map((pillar) => pillar.jiji)} day />
|
||||
<RowText label="십신" cells={data.pillars.map((pillar) => pillar.sipsin)} />
|
||||
<RowText label="지장간" cells={data.pillars.map((pillar) => pillar.jijang)} mono />
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{
|
||||
marginTop: 12, padding: '10px 14px',
|
||||
background: 'rgba(106,76,124,0.06)', borderRadius: 8,
|
||||
border: '1px dashed rgba(106,76,124,0.25)',
|
||||
fontSize: 11.5, color: '#6B6B6B', lineHeight: 1.6,
|
||||
}}>
|
||||
※ 일간(나)을 중심으로 사주의 흐름과 균형을 해석합니다.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const thStyle = ({ active = false } = {}) => ({
|
||||
padding: '8px 4px 12px',
|
||||
textAlign: 'center',
|
||||
borderBottom: '1px solid rgba(31,42,68,0.08)',
|
||||
background: active ? 'rgba(106,76,124,0.06)' : 'transparent',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
function Row({ label, cells, day }) {
|
||||
return (
|
||||
<tr>
|
||||
<td style={{ fontSize: 11, color: '#9A968D', fontWeight: 700, padding: '14px 8px', textAlign: 'center' }}>{label}</td>
|
||||
{cells.map((cell, index) => {
|
||||
const isDay = index === 2 && day;
|
||||
return (
|
||||
<td key={`${cell.ch}-${index}`} style={{
|
||||
padding: '10px 4px', textAlign: 'center',
|
||||
background: isDay ? 'rgba(106,76,124,0.06)' : 'transparent',
|
||||
borderTop: '1px solid rgba(31,42,68,0.04)',
|
||||
}}>
|
||||
<div className="font-title" style={{ fontSize: 28, color: cell.color, lineHeight: 1, letterSpacing: 0 }}>{cell.ch}</div>
|
||||
<div style={{ fontSize: 9.5, color: hexA(cell.color, 0.9), fontWeight: 700, marginTop: 3, letterSpacing: '-0.02em' }}>
|
||||
{cell.ko} <span style={{ color: '#9A968D', fontWeight: 500 }}>{cell.mark}</span>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RowText({ label, cells, mono }) {
|
||||
return (
|
||||
<tr>
|
||||
<td style={{ fontSize: 11, color: '#9A968D', fontWeight: 700, padding: '10px 8px', textAlign: 'center' }}>{label}</td>
|
||||
{cells.map((cell, index) => (
|
||||
<td key={`${cell}-${index}`} style={{
|
||||
padding: '8px 4px', textAlign: 'center',
|
||||
background: index === 2 ? 'rgba(106,76,124,0.06)' : 'transparent',
|
||||
borderTop: '1px solid rgba(31,42,68,0.04)',
|
||||
fontFamily: mono ? 'var(--font-title)' : 'inherit',
|
||||
fontSize: mono ? 13 : 12,
|
||||
color: '#1F2A44',
|
||||
letterSpacing: mono ? '0.1em' : '-0.01em',
|
||||
}}>{String(cell || '-')}</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function OhaengCard({ data }) {
|
||||
const strongest = maxElement(data.elementsObj);
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '20px 22px' }}>
|
||||
<PanelHeader title="오행 분석" />
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-around', height: 160, gap: 8, padding: '0 8px' }}>
|
||||
{data.ohaeng.map((element) => {
|
||||
const height = Math.min(100, Math.max(8, (element.value / 60) * 100));
|
||||
return (
|
||||
<div key={element.id} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ flex: 1, width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
||||
<div style={{ fontSize: 11, color: element.color, fontWeight: 700, textAlign: 'center', marginBottom: 4 }}>{element.value}%</div>
|
||||
<div style={{
|
||||
width: 28, margin: '0 auto', height: `${height}%`, minHeight: 6,
|
||||
background: `linear-gradient(180deg, ${hexA(element.color, 0.85)}, ${element.color})`,
|
||||
borderRadius: '6px 6px 2px 2px',
|
||||
boxShadow: `0 -2px 8px ${hexA(element.color, 0.3)}, inset 0 1px 0 rgba(255,255,255,0.3)`,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#1F2A44', fontWeight: 700, display: 'flex', alignItems: 'baseline', gap: 3 }}>
|
||||
{element.ko}<span style={{ fontSize: 10, color: '#9A968D' }}>({element.ch})</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: 16, padding: '12px 14px',
|
||||
background: hexA(strongest.color, 0.08), borderRadius: 8,
|
||||
border: `1px solid ${hexA(strongest.color, 0.2)}`,
|
||||
}}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: strongest.color, marginBottom: 4 }}>
|
||||
{ID_TO_KO[strongest.id]}({ID_TO_CH[strongest.id]})의 기운이 강한 사주입니다.
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#6B6B6B', lineHeight: 1.6 }}>
|
||||
강한 기운을 바탕으로 장점을 살리고 부족한 기운은 생활 습관과 관계에서 보완해 보세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HoryungInsightCard({ data }) {
|
||||
const strongest = maxElement(data.elementsObj);
|
||||
const items = [
|
||||
{ title: `일간이 ${data.ilgan.ch}이시네요.`, desc: '단단한 중심과 자기 기준을 갖고 흐름을 읽는 힘이 있습니다.' },
|
||||
{ title: `${ID_TO_KO[strongest.id]}(${ID_TO_CH[strongest.id]})의 기운이 두드러져요.`, desc: '해당 기운의 장점을 생활과 일의 방향으로 살려보세요.' },
|
||||
{ title: '균형을 보완하면 더욱 좋아요.', desc: '강한 기운만 밀어붙이기보다 부족한 기운을 의식하면 흐름이 부드러워집니다.' },
|
||||
{ title: '지금의 선택이 미래의 나를 만듭니다.', desc: '작은 실천을 꾸준히 쌓는 시기로 삼아보세요.' },
|
||||
];
|
||||
return (
|
||||
<div className="k-frame dark" style={{ padding: '22px 22px', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, color: '#E8C76B' }}>
|
||||
<svg width="32" height="6" viewBox="0 0 32 6"><path d="M0 3 L28 3" stroke="#E8C76B" strokeWidth="1" /><circle cx="30" cy="3" r="1.5" fill="#E8C76B" /></svg>
|
||||
<h3 className="font-title" style={{ margin: 0, fontSize: 17, color: '#E8C76B', letterSpacing: '-0.01em' }}>호령이의 해설</h3>
|
||||
<svg width="32" height="6" viewBox="0 0 32 6"><circle cx="2" cy="3" r="1.5" fill="#E8C76B" /><path d="M4 3 L32 3" stroke="#E8C76B" strokeWidth="1" /></svg>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, marginTop: 4 }}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.title} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%',
|
||||
background: 'rgba(212,175,55,0.12)', border: '1px solid rgba(212,175,55,0.35)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
color: '#E8C76B', fontSize: 13, fontWeight: 800,
|
||||
}}>{index + 1}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#F7F2E8', marginBottom: 3 }}>{item.title}</div>
|
||||
<div style={{ fontSize: 11.5, color: '#D9D2C0', lineHeight: 1.55 }}>{item.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: 6, padding: '14px 16px', borderRadius: 10,
|
||||
background: 'rgba(212,175,55,0.08)', border: '1px solid rgba(212,175,55,0.3)',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div className="font-title" style={{ fontSize: 14, color: '#E8C76B', lineHeight: 1.5 }}>
|
||||
지금의 선택이<br />미래의 나를 만듭니다.
|
||||
<span style={{ marginLeft: 4, opacity: 0.7 }}><IconPaw size={11} color="#E8C76B" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TraitDeskCard({ color, iconName, title, body, bullets }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '18px 18px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%',
|
||||
background: hexA(color, 0.10), border: `1px solid ${hexA(color, 0.35)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
<TraitIcon name={iconName} color={color} size={16} />
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 15, color: '#1F2A44', letterSpacing: '-0.02em' }}>{title}</div>
|
||||
</div>
|
||||
{body && <div style={{ fontSize: 12, color: '#6B6B6B', lineHeight: 1.65 }}>{body}</div>}
|
||||
{bullets && (
|
||||
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{bullets.map((item) => (
|
||||
<li key={item} style={{ fontSize: 12, color: '#1F2A44', display: 'flex', gap: 6 }}>
|
||||
<span style={{ color, flexShrink: 0 }}>·</span> {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TraitIcon({ name, color, size }) {
|
||||
const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth: '1.7', strokeLinecap: 'round', strokeLinejoin: 'round' };
|
||||
if (name === 'heart') return <svg {...common}><path d="M12 20s-7-4.5-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.5-7 10-7 10z" /></svg>;
|
||||
if (name === 'challenge') return <svg {...common}><path d="M12 3l10 17H2z" /><path d="M12 10v5M12 18v.5" /></svg>;
|
||||
if (name === 'lead') return <svg {...common}><path d="M4 16c4-6 8-6 16 0" /><path d="M8 12l4-4 4 4" /></svg>;
|
||||
if (name === 'adapt') return <svg {...common}><path d="M4 12c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z" /><circle cx="12" cy="12" r="2" /></svg>;
|
||||
return <svg {...common}><path d="M12 3v18M5 9l7-6 7 6M6 17h12" /></svg>;
|
||||
}
|
||||
|
||||
function DaeunDeskCard({ data }) {
|
||||
const current = data.daeun.find((item) => item.current) || data.daeun[0];
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '22px 24px', marginTop: 18 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
|
||||
<OrnamentBloom size={20} color="#D4AF37" />
|
||||
<h3 className="font-title" style={{ margin: 0, fontSize: 18, color: '#1F2A44', letterSpacing: '-0.02em' }}>대운 흐름</h3>
|
||||
<span style={{ fontSize: 12, color: '#9A968D' }}>10년 단위 운의 흐름을 살펴보세요.</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.2fr', gap: 20, alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{data.daeun.map((item, index) => (
|
||||
<React.Fragment key={`${item.age}-${index}`}>
|
||||
<DaeunNodeDesk {...item} />
|
||||
{index < data.daeun.length - 1 && (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: 12 }}>
|
||||
<IconChevron dir="right" size={12} color={item.current || data.daeun[index + 1].current ? '#6A4C7C' : '#D4AF37'} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
background: 'rgba(212,175,55,0.06)', borderRadius: 10,
|
||||
border: '1px dashed rgba(212,175,55,0.4)', padding: '14px 16px',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
||||
<span style={{
|
||||
fontSize: 10, fontWeight: 700, color: '#F7F2E8', background: '#6A4C7C',
|
||||
padding: '2px 8px', borderRadius: 99,
|
||||
}}>현재</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#1F2A44' }}>대운 해설 ({current?.age})</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#6B6B6B', lineHeight: 1.7 }}>
|
||||
자기 확장과 기반을 다지는 시기입니다.<br />
|
||||
꾸준한 노력과 인내가 결실을 맺고, 커리어와 재정적 성장이 기대됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DaeunNodeDesk({ age, gan, label, current }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, position: 'relative', minWidth: 64 }}>
|
||||
{current && (
|
||||
<div style={{
|
||||
position: 'absolute', top: -8, left: '50%', transform: 'translateX(-50%)',
|
||||
fontSize: 9, fontWeight: 700, color: '#F7F2E8', background: '#6A4C7C',
|
||||
padding: '2px 8px', borderRadius: 99, zIndex: 1, whiteSpace: 'nowrap',
|
||||
}}>현재</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, color: '#9A968D', marginTop: current ? 12 : 4, fontWeight: 700, whiteSpace: 'nowrap' }}>{age}</div>
|
||||
<div style={{
|
||||
width: 48, height: 58, borderRadius: '50% 50% 40% 40%',
|
||||
background: current ? '#1F2A44' : '#FBF7EF',
|
||||
border: current ? '2px solid #D4AF37' : '1px solid rgba(31,42,68,0.12)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: current ? '0 4px 14px rgba(31,42,68,0.3)' : 'none',
|
||||
}}>
|
||||
<span className="font-title" style={{ fontSize: 22, color: current ? '#E8C76B' : '#1F2A44' }}>{gan}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: current ? '#6A4C7C' : '#6B6B6B', fontWeight: current ? 700 : 500 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConsultCTA({ onClick }) {
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 18, padding: '28px 32px',
|
||||
background: '#1F2A44', color: '#F7F2E8',
|
||||
borderRadius: 14, border: '1px solid rgba(212,175,55,0.4)',
|
||||
display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 24,
|
||||
boxShadow: '0 12px 40px rgba(31,42,68,0.18)',
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, color: '#E8C76B' }}>
|
||||
<OrnamentBloom size={16} color="#E8C76B" />
|
||||
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.1em' }}>1:1 PERSONAL CONSULT</span>
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 22, color: '#F7F2E8', letterSpacing: '-0.02em' }}>
|
||||
더 깊은 해석이 필요하신가요?
|
||||
</div>
|
||||
<div style={{ marginTop: 6, fontSize: 13, color: '#D9D2C0' }}>
|
||||
개인 맞춤 상담을 통해 당신의 사주를 더 깊이 이해하고 명확한 방향을 찾아보세요.
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClick} style={{
|
||||
padding: '14px 24px', borderRadius: 99,
|
||||
background: '#E8C76B', color: '#1F2A44',
|
||||
border: 'none', fontSize: 14, fontWeight: 800,
|
||||
boxShadow: '0 6px 18px rgba(232,199,107,0.4), inset 0 1px 0 rgba(255,255,255,0.4)',
|
||||
display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
1:1 상담 신청하기 <IconPaw size={14} color="#1F2A44" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,245 @@
|
||||
import React from 'react';
|
||||
import TodayMobile from './today.mobile.jsx';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DesktopFooter from '../_shell/DesktopFooter';
|
||||
import Mascot from '../_shell/Mascot';
|
||||
import PanelHeader from '../_shell/PanelHeader';
|
||||
import {
|
||||
IconClock, IconHeart, IconMoney, IconPaw, IconSparkle, IconStar, IconSun,
|
||||
} from '../_shell/Icons';
|
||||
import hexA from '../_shell/helpers/hexA';
|
||||
|
||||
const SCORE_LABELS = [
|
||||
{ key: 'wealth', label: '재물운', color: '#D4AF37', icon: IconMoney, desc: '안정적인 흐름, 수입에 긍정적인 변화가 있어요.' },
|
||||
{ key: 'romance', label: '연애운', color: '#D89098', icon: IconHeart, desc: '진심이 통하는 하루, 관계가 한층 가까워져요.' },
|
||||
{ key: 'social', label: '건강운', color: '#4E6B5C', icon: LeafIcon, desc: '컨디션이 무난해요. 규칙적인 관리가 필요해요.' },
|
||||
{ key: 'career', label: '직장운', color: '#3A5A8C', icon: BriefcaseIcon, desc: '업무 성과가 좋아요. 기획력이 빛을 발합니다.' },
|
||||
];
|
||||
|
||||
export default function TodayDesktop({ reading }) {
|
||||
const navigate = useNavigate();
|
||||
const scores = reading?.fortune_scores || {};
|
||||
const lucky = reading?.lucky || {};
|
||||
const overall = Math.round(scores.overall || 78);
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short',
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 720, margin: '0 auto' }}>
|
||||
<TodayMobile reading={reading} />
|
||||
<main className="page paper-bg screen-in" style={{ marginTop: -78, paddingTop: 92 }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto', padding: '0 36px 0' }}>
|
||||
<div style={{ fontSize: 12, color: '#9A968D', marginBottom: 16, letterSpacing: '-0.01em' }}>
|
||||
홈 › <span style={{ color: '#1F2A44', fontWeight: 700 }}>오늘의 운세</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '320px 1fr', gap: 24, alignItems: 'flex-start' }}>
|
||||
<aside className="k-frame" style={{ padding: 24, textAlign: 'center', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
background: '#FBF7EF', border: '1px solid rgba(31,42,68,0.10)',
|
||||
borderRadius: 18, padding: '14px 16px',
|
||||
fontSize: 13, color: '#1F2A44', lineHeight: 1.75, letterSpacing: '-0.01em',
|
||||
}}>
|
||||
안녕하세요!<br />오늘의 운세를 정성껏<br />전해드릴게요.
|
||||
<span style={{ marginLeft: 4, color: '#B89530', opacity: 0.7 }}><IconPaw size={11} /></span>
|
||||
</div>
|
||||
<Mascot variant="full" size={260} style={{ margin: '10px auto 0' }} />
|
||||
<div className="k-frame dark" style={{ marginTop: 8, padding: '17px 14px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 11, color: '#E8C76B', fontWeight: 700, letterSpacing: '0.16em', marginBottom: 7 }}>오늘의 한마디</div>
|
||||
<div className="font-title" style={{ fontSize: 17, color: '#F7F2E8', lineHeight: 1.65 }}>
|
||||
흐름을 읽는 자가<br />기회를 얻습니다.
|
||||
</div>
|
||||
<div style={{ marginTop: 7, fontSize: 12, color: '#D9D2C0', letterSpacing: '-0.01em' }}>
|
||||
작은 선택이 큰 변화를 만듭니다.
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
||||
<div className="k-frame" style={{ padding: '0', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
padding: '32px 40px',
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(31,42,68,0.96) 0%, rgba(31,42,68,0.78) 34%, rgba(251,247,239,0.92) 72%), url(/images/saju/horyung/background.png) center / cover',
|
||||
minHeight: 150,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div>
|
||||
<h1 className="font-title" style={{ margin: 0, fontSize: 46, color: '#F7F2E8', letterSpacing: '-0.035em' }}>오늘의 운세</h1>
|
||||
<div style={{ marginTop: 8, fontSize: 15, color: '#F1E8D6', letterSpacing: '-0.01em' }}>오늘의 흐름을 한눈에 확인해 보세요.</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 15, color: '#1F2A44', fontWeight: 800 }}>{today}</div>
|
||||
<button style={{
|
||||
marginTop: 12, padding: '9px 16px', borderRadius: 999,
|
||||
border: '1px solid rgba(166,123,63,0.35)', background: 'rgba(251,247,239,0.7)',
|
||||
color: '#6B4423', fontSize: 12, fontWeight: 700,
|
||||
}}>간지 정보 보기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="k-frame" style={{ padding: '24px 28px', display: 'grid', gridTemplateColumns: '320px 1fr', gap: 28, alignItems: 'center' }}>
|
||||
<div style={{
|
||||
textAlign: 'center', padding: '24px 0', borderRadius: 14,
|
||||
background: 'rgba(212,175,55,0.06)', border: '1px dashed rgba(212,175,55,0.4)',
|
||||
}}>
|
||||
<div style={{ fontSize: 15, color: '#1F2A44', fontWeight: 800, marginBottom: 8 }}>오늘의 종합운</div>
|
||||
<div className="font-title" style={{ fontSize: 72, color: '#1F2A44', lineHeight: 1, letterSpacing: '-0.05em' }}>
|
||||
{overall}<span style={{ fontSize: 28, color: '#1F2A44', fontWeight: 400 }}>/100</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 12, display: 'flex', justifyContent: 'center', gap: 3 }}>
|
||||
{[1, 2, 3, 4, 5].map((i) => <IconStar key={i} filled={i <= Math.round(overall / 20)} size={18} color="#D4AF37" />)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-title" style={{ fontSize: 26, color: '#B89530', letterSpacing: '-0.03em' }}>
|
||||
새로운 기회가 찾아오는 날입니다.
|
||||
</div>
|
||||
<div style={{ marginTop: 12, fontSize: 14, color: '#3E4456', lineHeight: 1.8, letterSpacing: '-0.01em' }}>
|
||||
작은 실천이 큰 변화를 만듭니다. 주변의 조언에 귀 기울여 보세요.<br />
|
||||
따뜻한 말 한마디가 당신의 하루를 빛나게 할 것입니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
|
||||
{SCORE_LABELS.map((item) => (
|
||||
<FortuneCard key={item.key} {...item} value={Math.round(scores[item.key] || (item.key === 'career' ? 82 : item.key === 'wealth' ? 80 : item.key === 'romance' ? 70 : 75))} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
|
||||
<SmallCard color="#D4AF37" icon={IconSun} title="행운의 색" sub="오늘의 기운을 높여주는 색상">
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
|
||||
{(Array.isArray(lucky.color) ? lucky.color : ['#1F2A44', '#E8C76B', '#6B4423', '#D89098', '#F7F2E8']).map((color) => (
|
||||
<div key={color} style={{ width: 26, height: 26, borderRadius: '50%', background: color, border: '1px solid rgba(31,42,68,0.15)' }} />
|
||||
))}
|
||||
</div>
|
||||
</SmallCard>
|
||||
<SmallCard color="#A67B3F" icon={IconClock} title="행운의 시간" sub="기운이 상승하는 시간대">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 10, padding: '9px 12px', background: 'rgba(166,123,63,0.08)', borderRadius: 999, border: '1px dashed rgba(166,123,63,0.35)' }}>
|
||||
<IconClock size={14} stroke="#A67B3F" />
|
||||
<span style={{ fontSize: 13, color: '#1F2A44', fontWeight: 700 }}>{lucky.time || '오전 10시 ~ 12시'}</span>
|
||||
</div>
|
||||
</SmallCard>
|
||||
<SmallCard color="#4E6B5C" icon={LeafIcon} title="오늘의 조언" sub="오늘 마음에 새기면 좋은 말">
|
||||
<div style={{ marginTop: 10, fontSize: 13, color: '#1F2A44', lineHeight: 1.6 }}>
|
||||
기회는 준비된 마음을<br />늘 찾아옵니다.
|
||||
</div>
|
||||
</SmallCard>
|
||||
<SmallCard color="#C04A4A" icon={WarnIcon} title="주의할 점" sub="조심하면 좋은 부분">
|
||||
<div style={{ marginTop: 10, fontSize: 13, color: '#1F2A44', lineHeight: 1.6 }}>
|
||||
충동적인 결정은 피하고,<br />여유를 가지세요.
|
||||
</div>
|
||||
</SmallCard>
|
||||
</div>
|
||||
|
||||
<div className="k-frame" style={{
|
||||
padding: '24px 30px',
|
||||
display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 14, alignItems: 'center',
|
||||
}}>
|
||||
<div>
|
||||
<div className="font-title" style={{ fontSize: 22, color: '#1F2A44', letterSpacing: '-0.03em' }}>더 깊이 알고 싶으신가요?</div>
|
||||
<div style={{ fontSize: 13, color: '#6B6B6B', marginTop: 4 }}>
|
||||
오늘의 운세를 넘어, 당신만을 위한 정밀한 사주 분석으로 인생의 방향을 찾아드려요.
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => navigate(reading?.id ? `/saju/result?rid=${reading.id}` : '/saju/result')} style={buttonPrimary()}>
|
||||
사주풀이 시작하기 <IconPaw size={13} color="#E8C76B" />
|
||||
</button>
|
||||
<button style={buttonGhost()}>
|
||||
<IconSparkle size={13} color="#B89530" /> AI 맞춤 인사이트 보기
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function FortuneCard({ label, value, icon: IconComponent, desc, color }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '20px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
||||
<div style={{
|
||||
width: 42, height: 42, borderRadius: '50%',
|
||||
background: hexA(color, 0.12), border: `1px solid ${hexA(color, 0.35)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{React.createElement(IconComponent, { size: 20, stroke: color })}
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 19, color: '#1F2A44', letterSpacing: '-0.03em' }}>{label}</div>
|
||||
</div>
|
||||
<div className="font-title" style={{ fontSize: 30, color: '#1F2A44', lineHeight: 1 }}>
|
||||
{value}<span style={{ fontSize: 16, color: '#1F2A44', fontWeight: 400 }}>/100</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 12, fontSize: 12.5, color: '#6B6B6B', lineHeight: 1.55 }}>{desc}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SmallCard({ color, icon: IconComponent, title, sub, children }) {
|
||||
return (
|
||||
<div className="k-frame" style={{ padding: '18px 18px' }}>
|
||||
<PanelHeader title={title} color="#1F2A44" accent={color} icon={(
|
||||
<div style={{
|
||||
width: 30, height: 30, borderRadius: '50%',
|
||||
background: hexA(color, 0.10), border: `1px solid ${hexA(color, 0.35)}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{React.createElement(IconComponent, { size: 15, stroke: color })}
|
||||
</div>
|
||||
)} />
|
||||
<div style={{ marginTop: -10, fontSize: 11, color: '#9A968D' }}>{sub}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buttonPrimary() {
|
||||
return {
|
||||
padding: '14px 24px', borderRadius: 999, background: '#1F2A44', color: '#F7F2E8',
|
||||
border: '1px solid rgba(212,175,55,0.4)', fontSize: 14, fontWeight: 800,
|
||||
boxShadow: '0 4px 14px rgba(31,42,68,0.25), inset 0 1px 0 rgba(212,175,55,0.3)',
|
||||
display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap',
|
||||
};
|
||||
}
|
||||
|
||||
function buttonGhost() {
|
||||
return {
|
||||
padding: '14px 24px', borderRadius: 999, background: 'transparent', color: '#1F2A44',
|
||||
border: '1px solid rgba(31,42,68,0.25)', fontSize: 14, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap',
|
||||
};
|
||||
}
|
||||
|
||||
function LeafIcon({ size = 16, stroke = '#4E6B5C' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 20V10" />
|
||||
<path d="M12 10c-4 0-7-2-8-6 5 0 8 2 8 6z" />
|
||||
<path d="M12 13c4 0 7-2 8-6-5 0-8 2-8 6z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BriefcaseIcon({ size = 16, stroke = '#3A5A8C' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="7" width="18" height="13" rx="2" />
|
||||
<path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WarnIcon({ size = 14, stroke = '#C04A4A' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 3l10 17H2z" />
|
||||
<path d="M12 10v5M12 18v.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function TodayMobile({ reading }) {
|
||||
)}
|
||||
|
||||
<div style={{ padding: '0 20px 40px' }}>
|
||||
<PrimaryButton color="#D4AF37" onClick={() => navigate(`/saju/result?rid=${reading?.id || ''}`)}>
|
||||
<PrimaryButton color="#D4AF37" onClick={() => navigate(reading?.id ? `/saju/result?rid=${reading.id}` : '/saju/result')}>
|
||||
내 사주 자세히 보기 <IconChevron dir="right" size={14} color="#1F2A44" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user