<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>오늘의 주역 운세</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
body { font-family: 'Noto Sans KR', sans-serif; transition: background-color 0.5s ease; }
/* 페이지 로딩 및 전환 애니메이션 */
.fade-in-up { animation: fadeInUp 0.8s ease-out forwards; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* 주사위 흔들기 애니메이션 */
.dice-shake { animation: shake 0.5s infinite; }
@keyframes shake {
0% { transform: rotate(0deg) scale(1); }
25% { transform: rotate(10deg) scale(1.1); }
50% { transform: rotate(0deg) scale(1); }
75% { transform: rotate(-10deg) scale(1.1); }
100% { transform: rotate(0deg) scale(1); }
}
/* 괘(爻) 드로잉 애니메이션 */
.line-draw { animation: lineDraw 1s ease-out forwards; opacity: 0; }
@keyframes lineDraw {
from { opacity: 0; transform: scaleX(0); }
to { opacity: 1; transform: scaleX(1); }
}
</style>
</head>
<body class="bg-[#FDFBF7] dark:bg-slate-950 transition-colors duration-500">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// --- 고해상도 아이콘 컴포넌트 ---
const IconBook = () => <svg className="w-7 h-7 text-amber-500" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" /><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" /></svg>;
const IconDice = ({ className }) => (
<svg className={className || "w-12 h-12"} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="8.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg>
);
const IconKeyboard = () => <svg className="w-8 h-8 text-slate-700 dark:text-slate-300" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><rect width="20" height="16" x="2" y="4" rx="2" ry="2" /><path d="M6 8h.001M10 8h.001M14 8h.001M18 8h.001M6 12h.001M10 12h.001M14 12h.001M18 12h.001" /></svg>;
const IconSparkles = () => <svg className="w-5 h-5 text-amber-400 mr-2" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" /></svg>;
const IconRefresh = () => <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5" /></svg>;
const IconMoon = () => <svg className="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>;
const IconSun = () => <svg className="w-6 h-6 text-yellow-500" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>;
const IconLightbulb = () => <svg className="w-5 h-5 text-yellow-500 mr-2" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A5 5 0 0 0 8 8c0 1.3.5 2.6 1.5 3.5.8.8 1.3 1.5 1.5 2.5M9 18h6M10 22h4" /></svg>;
const TRIGRAMS = {
1: { name: '건(乾)', nature: '하늘', binary: [1, 1, 1] },
2: { name: '태(兌)', nature: '연못', binary: [0, 1, 1] },
3: { name: '이(離)', nature: '불', binary: [1, 0, 1] },
4: { name: '진(震)', nature: '우레', binary: [0, 0, 1] },
5: { name: '손(巽)', nature: '바람', binary: [1, 1, 0] },
6: { name: '감(坎)', nature: '물', binary: [0, 1, 0] },
7: { name: '간(艮)', nature: '산', binary: [1, 0, 0] },
8: { name: '곤(坤)', nature: '땅', binary: [0, 0, 0] },
};
const getHexagramInterpretation = (upper, lower) => {
const descriptions = {
'1-1': { title: '중천건 (重天乾)', desc: '용이 하늘로 승천하는 형상입니다. 강한 운세와 리더십이 필요한 시기입니다.', luck: '매우 길함', tip: '자신감을 가지고 추진하되, 독단을 경계하고 주변의 조언을 경청하십시오.' },
'8-8': { title: '중지곤 (重地坤)', desc: '모든 것을 포용하는 땅의 형상입니다. 나서기보다 포용하고 따르는 것이 이롭습니다.', luck: '길함 (안정)', tip: '순리에 맡기고 기다리는 미덕이 필요한 때입니다. 서두르면 일을 그르칩니다.' },
'1-8': { title: '천지비 (天地否)', desc: '하늘과 땅이 막혀 소통이 안 되는 형상입니다. 답답할 수 있으나 내실을 다져야 합니다.', luck: '흉함 (주의)', tip: '억지로 풀려 하지 말고 잠시 멈추어 자신을 돌아보는 시간을 가지십시오.' },
'8-1': { title: '지천태 (地天泰)', desc: '하늘과 땅이 교류하여 만물이 화합하는 형상입니다. 모든 일이 순조롭게 풀립니다.', luck: '매우 길함', tip: '좋은 시기일수록 겸손함을 유지하여 다가올 변화에 대비하십시오.' },
'3-6': { title: '화수미제 (火수미제)', desc: '아직 일이 완성되지 않은 상태입니다. 끝까지 방심하지 말고 마무리에 집중하세요.', luck: '보통 (노력 필요)', tip: '마지막 한 걸음이 성패를 결정합니다. 신중하게 마무리를 지으십시오.' },
'6-3': { title: '수화기제 (수화기제)', desc: '물과 불이 균형을 이루어 이미 완성된 상태입니다. 현상 유지에 힘써야 합니다.', luck: '길함', tip: '성공 뒤의 나태함을 경계하고 현재의 안정을 지키는 데 주력하십시오.' },
};
const key = `${upper}-${lower}`;
if (descriptions[key]) return descriptions[key];
const u = TRIGRAMS[upper] || TRIGRAMS[1];
const l = TRIGRAMS[lower] || TRIGRAMS[1];
return {
title: `${u.nature}${l.nature}괘`,
desc: `위에는 ${u.name}, 아래에는 ${l.name}이 위치했습니다. 변화의 흐름을 읽고 신중하게 행동하세요.`,
luck: '보통',
tip: '상황의 변화를 예의주시하며 유연하게 대처하는 것이 필요합니다.'
};
};
function JuyeokFortune() {
const [step, setStep] = useState('intro');
const [loading, setLoading] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [num1, setNum1] = useState('');
const [num2, setNum2] = useState('');
const [num3, setNum3] = useState('');
const [result, setResult] = useState(null);
const userInfo = "음력 1972년 1월 17일 오전 5시생";
// 다크모드 토글 및 시스템 설정 감지
useEffect(() => {
if (darkMode) document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
}, [darkMode]);
const reset = () => {
setStep('intro');
setNum1(''); setNum2(''); setNum3('');
setResult(null); setLoading(false);
};
const calculateHexagram = (n1, n2, n3) => {
const cleanN1 = Math.floor(Math.abs(Number(n1) || 0));
const cleanN2 = Math.floor(Math.abs(Number(n2) || 0));
const cleanN3 = Math.floor(Math.abs(Number(n3) || 0));
return { upper: cleanN1 % 8 || 8, lower: cleanN2 % 8 || 8, change: cleanN3 % 6 || 6 };
};
const handleDiceRoll = () => {
setLoading(true);
setTimeout(() => {
const r1 = Math.floor(Math.random() * 64) + 1;
const r2 = Math.floor(Math.random() * 64) + 1;
const r3 = Math.floor(Math.random() * 6) + 1;
setResult(calculateHexagram(r1, r2, r3));
setLoading(false);
setStep('result');
}, 2000);
};
const handleManualInput = () => {
if (!num1 || !num2 || !num3) return;
setLoading(true);
setTimeout(() => {
setResult(calculateHexagram(num1, num2, num3));
setLoading(false);
setStep('result');
}, 1000);
};
const HexagramVisual = ({ upper, lower, activeLine }) => {
const renderTrigram = (trigramNum, isUpper) => {
const trigram = TRIGRAMS[trigramNum] || TRIGRAMS[1];
return trigram.binary.map((val, idx) => {
const lineNum = isUpper ? (6 - idx) : (3 - idx);
const isActive = lineNum === activeLine;
const delay = (6 - lineNum) * 0.1;
return (
<div key={`${isUpper ? 'u' : 'l'}-${idx}`} className="flex items-center justify-center my-1.5 relative h-8 w-48">
<div className={`w-full flex justify-between items-center line-draw ${isActive ? 'animate-pulse' : ''}`} style={{animationDelay: `${delay}s`}}>
{val === 1 ? (
<div className={`h-6 w-full rounded-sm ${isActive ? 'bg-red-600 shadow-[0_0_15px_rgba(220,38,38,0.8)]' : 'bg-slate-800 dark:bg-slate-400'}`}></div>
) : (
<div className="w-full flex justify-between">
<div className={`h-6 w-[46%] rounded-sm ${isActive ? 'bg-red-600 shadow-[0_0_15px_rgba(220,38,38,0.8)]' : 'bg-slate-800 dark:bg-slate-400'}`}></div>
<div className={`h-6 w-[46%] rounded-sm ${isActive ? 'bg-red-600 shadow-[0_0_15px_rgba(220,38,38,0.8)]' : 'bg-slate-800 dark:bg-slate-400'}`}></div>
</div>
)}
</div>
{isActive && <span className="absolute -right-12 text-red-600 font-bold text-xs whitespace-nowrap">동효(動)</span>}
</div>
);
});
};
return (
<div className="flex flex-col items-center bg-slate-50 dark:bg-slate-900/50 p-6 rounded-2xl border-2 border-slate-200 dark:border-slate-800 shadow-inner">
<div className="mb-1">{renderTrigram(upper, true)}</div>
<div>{renderTrigram(lower, false)}</div>
</div>
);
};
return (
<div className="min-h-screen transition-colors duration-500">
<header className="bg-slate-900 text-amber-50 p-6 shadow-2xl text-center relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-600 via-amber-400 to-red-600"></div>
{/* 다크모드 토글 버튼 */}
<button onClick={() => setDarkMode(!darkMode)} className="absolute right-4 top-6 p-2 rounded-full bg-slate-800 hover:bg-slate-700 transition-all active:scale-90 shadow-lg">
{darkMode ? <IconSun /> : <IconMoon />}
</button>
<h1 className="text-3xl font-bold mb-2 flex items-center justify-center gap-2 fade-in-up">
<IconBook /> 오늘의 주역(周易)
</h1>
<div className="text-sm text-slate-400 flex items-center justify-center gap-2 mt-2 opacity-80">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/></svg>
<span>의뢰인: {userInfo}</span>
</div>
</header>
<main className="max-w-2xl mx-auto p-6">
{step === 'intro' && (
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-2xl p-10 text-center border border-stone-100 dark:border-slate-800 fade-in-up">
<div className="mb-6 inline-block p-4 bg-amber-50 dark:bg-amber-900/20 rounded-full">
<IconSparkles />
</div>
<h2 className="text-2xl font-bold mb-4 dark:text-white">마음을 비우고 운세를 묻습니다</h2>
<p className="text-slate-500 dark:text-slate-400 mb-10 leading-relaxed">주역은 우연을 통해 필연을 읽어내는 학문입니다.<br/>고민을 생각한 뒤 아래 버튼을 눌러주세요.</p>
<button onClick={() => setStep('method')} className="w-full bg-slate-900 dark:bg-slate-100 dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-white text-white font-bold py-5 px-6 rounded-2xl transition-all flex items-center justify-center shadow-xl active:scale-95">
운세 확인하기
</button>
</div>
)}
{step === 'method' && (
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-2xl p-10 border border-stone-100 dark:border-slate-800 fade-in-up">
<h2 className="text-xl font-bold mb-8 text-center dark:text-white">점괘를 뽑을 방법을 선택하세요</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<button onClick={() => setStep('dice')} className="flex flex-col items-center justify-center p-8 bg-stone-50 dark:bg-slate-800 hover:bg-red-50 dark:hover:bg-red-900/20 border-2 border-stone-200 dark:border-slate-700 rounded-2xl transition-all group active:scale-95">
<IconDice className="w-14 h-14 text-slate-700 dark:text-slate-300 group-hover:text-red-500 transition-colors" />
<span className="font-bold text-lg mt-4 dark:text-white">주사위 던지기</span>
</button>
<button onClick={() => setStep('input')} className="flex flex-col items-center justify-center p-8 bg-stone-50 dark:bg-slate-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border-2 border-stone-200 dark:border-slate-700 rounded-2xl transition-all group active:scale-95">
<IconKeyboard />
<span className="font-bold text-lg mt-4 dark:text-white">숫자 직접 입력</span>
</button>
</div>
<button onClick={() => setStep('intro')} className="mt-8 text-sm text-slate-400 underline w-full text-center hover:text-slate-600">뒤로 가기</button>
</div>
)}
{loading && (
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-2xl p-16 text-center border dark:border-slate-800 fade-in-up">
<div className="animate-spin inline-block mb-6 dark:text-white"><IconRefresh /></div>
<p className="text-xl font-medium text-slate-600 dark:text-slate-400 italic">하늘의 뜻을 읽는 중...</p>
</div>
)}
{step === 'dice' && !loading && (
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-2xl p-12 text-center border dark:border-slate-800 fade-in-up">
<div className="mb-10 flex justify-center dice-shake">
<IconDice className="w-24 h-24 text-amber-600 dark:text-amber-500 drop-shadow-xl" />
</div>
<h3 className="text-2xl font-bold mb-4 dark:text-white">주사위 점</h3>
<p className="mb-10 text-slate-500 dark:text-slate-400">버튼을 누르면 세 번의 주사위가 던져집니다.</p>
<button onClick={handleDiceRoll} className="bg-red-600 hover:bg-red-700 text-white font-bold py-5 px-16 rounded-2xl shadow-lg transition-all active:scale-95 hover:shadow-red-500/30">
주사위 던지기
</button>
<button onClick={() => setStep('method')} className="block mt-10 text-slate-400 text-sm mx-auto hover:text-slate-600">이전으로</button>
</div>
)}
{step === 'input' && !loading && (
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-2xl p-10 border dark:border-slate-800 fade-in-up">
<h3 className="text-xl font-bold mb-8 text-center dark:text-white">숫자 3개를 입력하세요</h3>
<div className="space-y-6 max-w-xs mx-auto">
<input type="number" value={num1} onChange={(e)=>setNum1(e.target.value)} placeholder="상괘 숫자" className="w-full p-4 border-2 dark:bg-slate-800 dark:border-slate-700 dark:text-white rounded-xl text-center outline-none focus:border-amber-500 transition-all text-xl font-bold" />
<input type="number" value={num2} onChange={(e)=>setNum2(e.target.value)} placeholder="하괘 숫자" className="w-full p-4 border-2 dark:bg-slate-800 dark:border-slate-700 dark:text-white rounded-xl text-center outline-none focus:border-amber-500 transition-all text-xl font-bold" />
<input type="number" value={num3} onChange={(e)=>setNum3(e.target.value)} placeholder="동효 숫자" className="w-full p-4 border-2 dark:bg-slate-800 dark:border-slate-700 dark:text-white rounded-xl text-center outline-none focus:border-amber-500 transition-all text-xl font-bold" />
<button onClick={handleManualInput} className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl shadow-lg active:scale-95 transition-all">결과 보기</button>
</div>
</div>
)}
{step === 'result' && result && !loading && (
<div className="bg-white dark:bg-slate-900 rounded-[2.5rem] shadow-2xl overflow-hidden border border-stone-100 dark:border-slate-800 fade-in-up">
<div className="bg-slate-50 dark:bg-slate-800/50 p-8 border-b dark:border-slate-700 flex justify-between items-center">
<h2 className="text-3xl font-bold dark:text-white flex items-center gap-3">
<IconBook /> {getHexagramInterpretation(result.upper, result.lower).title}
</h2>
<span className="font-bold text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 px-4 py-1 rounded-full text-sm">
{getHexagramInterpretation(result.upper, result.lower).luck}
</span>
</div>
<div className="p-10">
<div className="flex flex-col md:flex-row gap-10 items-center justify-center mb-10">
<HexagramVisual upper={result.upper} lower={result.lower} activeLine={result.change} />
<div className="flex-1 space-y-4 dark:text-slate-300 w-full">
<div className="p-6 bg-stone-50 dark:bg-slate-800/50 rounded-2xl border dark:border-slate-700">
<p className="text-sm font-bold text-slate-400 mb-2">괘 구성</p>
<p className="text-lg font-bold">상괘: {TRIGRAMS[result.upper].name} ({TRIGRAMS[result.upper].nature})</p>
<p className="text-lg font-bold mt-2">하괘: {TRIGRAMS[result.lower].name} ({TRIGRAMS[result.lower].nature})</p>
<div className="mt-4 pt-4 border-t dark:border-slate-700">
<p className="text-sm font-bold text-red-600 dark:text-red-400">동효: 제 {result.change}효 변동</p>
</div>
</div>
</div>
</div>
<div className="border-t dark:border-slate-700 pt-10 space-y-8">
<div>
<h4 className="font-bold text-slate-800 dark:text-slate-200 mb-3 text-xl">괘의 풀이</h4>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed text-lg">{getHexagramInterpretation(result.upper, result.lower).desc}</p>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 p-6 rounded-2xl border border-amber-100 dark:border-amber-900/30 shadow-sm">
<h4 className="font-bold text-amber-800 dark:text-amber-400 flex items-center mb-3 text-lg"><IconLightbulb /> 상세 조언</h4>
<p className="text-amber-900 dark:text-amber-200 leading-relaxed">{getHexagramInterpretation(result.upper, result.lower).tip}</p>
</div>
<button onClick={reset} className="mt-10 flex items-center justify-center w-full py-4 text-slate-400 dark:text-slate-500 hover:text-slate-800 dark:hover:text-white border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-2xl transition-all">
<IconRefresh /> 다시 점치기
</button>
</div>
</div>
</div>
)}
</main>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<JuyeokFortune />);
</script>
</body>
</html>