diff --git a/src/pages/saju/components/ElementBarChart.jsx b/src/pages/saju/components/ElementBarChart.jsx new file mode 100644 index 0000000..cfd3f6b --- /dev/null +++ b/src/pages/saju/components/ElementBarChart.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +const ELEMENT_ORDER = ['木', '火', '土', '金', '水']; +const ELEMENT_KR = { '木': '목', '火': '화', '土': '토', '金': '금', '水': '수' }; + +export default function ElementBarChart({ scores }) { + if (!scores) return null; + const max = Math.max(...Object.values(scores), 1); + return ( +
+ {ELEMENT_ORDER.map((e) => { + const value = scores[e] || 0; + const widthPct = (value / max) * 100; + return ( +
+
{e} ({ELEMENT_KR[e]})
+
+
+
+
{value.toFixed(1)}%
+
+ ); + })} +
+ ); +} diff --git a/src/pages/saju/components/HoryungQuote.jsx b/src/pages/saju/components/HoryungQuote.jsx new file mode 100644 index 0000000..0dd8927 --- /dev/null +++ b/src/pages/saju/components/HoryungQuote.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import HoryungMascot from './HoryungMascot'; + +export default function HoryungQuote({ pose = 'thinking', text }) { + if (!text) return null; + return ( +
+ +
{text}
+
+ ); +} diff --git a/src/pages/saju/components/InterpretAccordion.jsx b/src/pages/saju/components/InterpretAccordion.jsx new file mode 100644 index 0000000..681b97c --- /dev/null +++ b/src/pages/saju/components/InterpretAccordion.jsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; + +export default function InterpretAccordion({ items }) { + const [openKey, setOpenKey] = useState(items?.[0]?.key); + if (!items || items.length === 0) return null; + return ( +
+ {items.map((it) => { + const isOpen = openKey === it.key; + return ( +
+
setOpenKey(isOpen ? null : it.key)} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setOpenKey(isOpen ? null : it.key); }} + > + {it.title || it.key} + {isOpen ? '▾' : '▸'} +
+ {isOpen && ( +
+

{it.content}

+ {it.evidence && ( +
+ 근거: {it.evidence.saju_element}
+ 해석 논리: {it.evidence.reasoning} +
+ )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/src/pages/saju/components/MonthlyFlow.jsx b/src/pages/saju/components/MonthlyFlow.jsx new file mode 100644 index 0000000..6d90f19 --- /dev/null +++ b/src/pages/saju/components/MonthlyFlow.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +const LABEL_COLOR = { + '성장': '#4B7065', + '안정': '#D4A574', + '변동': '#6A5285', + '도전': '#C58F76', + '정체': '#888', +}; + +export default function MonthlyFlow({ flow }) { + if (!flow || flow.length === 0) return null; + return ( +
+ {flow.map((m) => ( +
+ {m.month}월 + + {m.score} + + {m.label} +
+ ))} +
+ ); +} diff --git a/src/pages/saju/components/SajuPillars.jsx b/src/pages/saju/components/SajuPillars.jsx new file mode 100644 index 0000000..b847e2a --- /dev/null +++ b/src/pages/saju/components/SajuPillars.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +const PILLAR_LABELS = { year: '년주', month: '월주', day: '일주', hour: '시주' }; + +export default function SajuPillars({ saju }) { + if (!saju) return null; + const pillars = ['year', 'month', 'day', 'hour']; + return ( +
+ {pillars.map((p) => { + const data = saju[p]; + if (!data) { + return ( +
+
{PILLAR_LABELS[p]}
+
-
+
+ ); + } + return ( +
+
{PILLAR_LABELS[p]}
+
+ {data.stem} + ({data.stem_kr}) +
+
+ {data.branch} + ({data.branch_kr}) +
+
{data.ten_god}
+
{data.fortune}
+
+ ); + })} +
+ ); +} diff --git a/src/pages/saju/hooks/useSajuReading.js b/src/pages/saju/hooks/useSajuReading.js new file mode 100644 index 0000000..cbaa6f4 --- /dev/null +++ b/src/pages/saju/hooks/useSajuReading.js @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react'; +import { sajuGetReading } from '../../../api'; + +export default function useSajuReading(readingId) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!readingId) { + setLoading(false); + return; + } + let cancelled = false; + setLoading(true); + sajuGetReading(readingId) + .then((d) => { + if (!cancelled) { + setData(d); + setLoading(false); + } + }) + .catch((e) => { + if (!cancelled) { + setError(e.message || '사주 결과를 불러올 수 없습니다.'); + setLoading(false); + } + }); + return () => { cancelled = true; }; + }, [readingId]); + + return { data, loading, error }; +}