<!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>
tailwind.config = {
darkMode: 'media',
theme: {
extend: {
fontFamily: { sans: ['Noto Sans KR', 'sans-serif'] },
keyframes: {
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
},
animation: {
fadeInUp: 'fadeInUp 0.8s ease-out forwards',
}
}
}
}
</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.3s ease; }
canvas { touch-action: none; transition: opacity 1s ease; }
.stat-card { transition: all 0.3s ease; }
.stat-card:hover { transform: translateY(-2px); }
</style>
</head>
<body class="bg-slate-50 dark:bg-slate-950 min-h-screen flex flex-col items-center py-4 px-4 transition-colors duration-300">
<!-- Header (Animated) -->
<header class="mb-4 text-center opacity-0 animate-fadeInUp" style="animation-delay: 0.1s">
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-1">바이오리듬 분석기</h1>
<p class="text-slate-500 dark:text-slate-400 text-xs">생년월일 기반 신체, 감성, 지성 주기 분석</p>
</header>
<!-- Main Card (Animated) -->
<main class="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-2xl p-4 md:p-6 border border-slate-200 dark:border-slate-800 opacity-0 animate-fadeInUp" style="animation-delay: 0.3s">
<!-- Controls -->
<div class="grid grid-cols-2 gap-3 mb-6 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700">
<div>
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase mb-1">생년월일</label>
<!-- 요청사항: 1972-03-02로 수정 -->
<input type="date" id="birthDate" value="1972-03-02"
class="w-full bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 text-xs rounded-md p-1.5 focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase mb-1">기준 날짜</label>
<input type="date" id="targetDate"
class="w-full bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 text-xs rounded-md p-1.5 focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<!-- Legend & Stats (Dynamic Progress) -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="stat-card bg-blue-50 dark:bg-blue-900/20 p-2 rounded-lg border-t-2 border-blue-500">
<div class="flex justify-between items-baseline">
<span class="text-[10px] font-bold text-blue-700 dark:text-blue-400">신체</span>
<span id="val-p" class="text-sm font-bold text-blue-800 dark:text-blue-300">0%</span>
</div>
<div class="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-1 mt-1 overflow-hidden">
<div id="bar-p" class="bg-blue-500 h-1 rounded-full transition-all duration-1000 ease-out" style="width: 50%"></div>
</div>
</div>
<div class="stat-card bg-red-50 dark:bg-red-900/20 p-2 rounded-lg border-t-2 border-red-500">
<div class="flex justify-between items-baseline">
<span class="text-[10px] font-bold text-red-700 dark:text-red-400">감성</span>
<span id="val-e" class="text-sm font-bold text-red-800 dark:text-red-300">0%</span>
</div>
<div class="w-full bg-red-200 dark:bg-red-800 rounded-full h-1 mt-1 overflow-hidden">
<div id="bar-e" class="bg-red-500 h-1 rounded-full transition-all duration-1000 ease-out" style="width: 50%"></div>
</div>
</div>
<div class="stat-card bg-green-50 dark:bg-green-900/20 p-2 rounded-lg border-t-2 border-green-500">
<div class="flex justify-between items-baseline">
<span class="text-[10px] font-bold text-green-700 dark:text-green-400">지성</span>
<span id="val-i" class="text-sm font-bold text-green-800 dark:text-green-300">0%</span>
</div>
<div class="w-full bg-green-200 dark:bg-green-800 rounded-full h-1 mt-1 overflow-hidden">
<div id="bar-i" class="bg-green-500 h-1 rounded-full transition-all duration-1000 ease-out" style="width: 50%"></div>
</div>
</div>
</div>
<!-- Canvas Container -->
<div class="relative w-full overflow-hidden bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg mb-4">
<canvas id="bioChart" class="w-full h-48 md:h-56 cursor-crosshair opacity-0"></canvas>
<div id="chart-overlay" class="absolute top-1 right-2 text-[10px] text-slate-400 pointer-events-none">
30일 간의 변화 추이
</div>
</div>
<!-- Interpretation -->
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
<h3 class="font-bold text-slate-800 dark:text-slate-200 text-xs mb-1 flex items-center">
<svg class="w-3.5 h-3.5 mr-1 text-yellow-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
오늘의 해석
</h3>
<p id="analysis-text" class="text-slate-600 dark:text-slate-400 text-[11px] leading-relaxed">
데이터를 분석 중입니다...
</p>
</div>
</main>
<script>
const PI = Math.PI;
const CYCLE_P = 23;
const CYCLE_E = 28;
const CYCLE_I = 33;
const birthInput = document.getElementById('birthDate');
const targetInput = document.getElementById('targetDate');
const canvas = document.getElementById('bioChart');
const ctx = canvas.getContext('2d');
const elValP = document.getElementById('val-p');
const elValE = document.getElementById('val-e');
const elValI = document.getElementById('val-i');
const elBarP = document.getElementById('bar-p');
const elBarE = document.getElementById('bar-e');
const elBarI = document.getElementById('bar-i');
const analysisText = document.getElementById('analysis-text');
const today = new Date();
targetInput.value = today.toISOString().split('T')[0];
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
draw();
canvas.style.opacity = '1'; // 로딩 시 서서히 나타남
}
window.addEventListener('resize', resizeCanvas);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', draw);
function getBiorhythm(born, target) {
const oneDay = 24 * 60 * 60 * 1000;
const diffDays = (target.setHours(12,0,0,0) - born.setHours(12,0,0,0)) / oneDay;
return {
p: Math.sin((2 * PI * diffDays) / CYCLE_P) * 100,
e: Math.sin((2 * PI * diffDays) / CYCLE_E) * 100,
i: Math.sin((2 * PI * diffDays) / CYCLE_I) * 100
};
}
function draw() {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const birthDate = new Date(birthInput.value);
const targetDateStr = targetInput.value;
if (!targetDateStr || isNaN(birthDate.getTime())) return;
const targetDate = new Date(targetDateStr);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const range = 15;
const width = canvas.width;
const height = canvas.height;
const midY = height / 2;
const colorGrid = isDark ? '#334155' : '#e2e8f0';
const colorZero = isDark ? '#64748b' : '#94a3b8';
ctx.beginPath();
ctx.strokeStyle = colorZero;
ctx.lineWidth = 1;
ctx.moveTo(0, midY);
ctx.lineTo(width, midY);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = colorGrid;
ctx.setLineDash([4, 4]);
ctx.moveTo(width / 2, 0);
ctx.lineTo(width / 2, height);
ctx.stroke();
ctx.setLineDash([]);
function drawWave(cycle, color) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (let xPixel = 0; xPixel <= width; xPixel += 2) {
const dayOffset = (xPixel / width) * (range * 2 + 1) - range - 0.5;
const oneDay = 24 * 60 * 60 * 1000;
const centerDiff = (targetDate - birthDate) / oneDay;
const currentDiff = centerDiff + dayOffset;
const val = Math.sin((2 * PI * currentDiff) / cycle);
const y = midY - (val * (height / 2 - 25));
if (xPixel === 0) ctx.moveTo(xPixel, y);
else ctx.lineTo(xPixel, y);
}
ctx.stroke();
}
drawWave(CYCLE_P, '#3b82f6');
drawWave(CYCLE_E, '#ef4444');
drawWave(CYCLE_I, '#22c55e');
updateUI(getBiorhythm(birthDate, new Date(targetDate)));
}
// 동적 카운팅 애니메이션 함수
function animateValue(obj, start, end, duration, isPercent = true) {
let startTimestamp = null;
const step = (timestamp) => {
if (!startTimestamp) startTimestamp = timestamp;
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
const currentVal = Math.floor(progress * (end - start) + start);
obj.innerText = isPercent ? `${currentVal}%` : currentVal;
if (progress < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
function updateUI(stats) {
const getWidth = (val) => `${(val + 100) / 2}%`;
// 값 애니메이션 (0에서 목표값까지)
animateValue(elValP, 0, Math.round(stats.p), 1000);
animateValue(elValE, 0, Math.round(stats.e), 1000);
animateValue(elValI, 0, Math.round(stats.i), 1000);
// 프로그레스 바 애니메이션
elBarP.style.width = getWidth(stats.p);
elBarE.style.width = getWidth(stats.e);
elBarI.style.width = getWidth(stats.i);
let comments = [];
const isCritical = (val) => Math.abs(val) < 10;
const isHigh = (val) => val > 75;
if (isCritical(stats.p)) comments.push("⚠️ <b>신체 리듬</b>이 전환기에 있습니다. 충분한 휴식이 필요합니다.");
else if (isHigh(stats.p)) comments.push("💪 <b>신체 리듬</b>이 매우 좋아 활동적인 일에 적합합니다.");
if (isCritical(stats.e)) comments.push("⚠️ <b>감성 리듬</b>이 교차점에 있어 감정 변화에 유의하세요.");
else if (isHigh(stats.e)) comments.push("🥰 <b>감성 리듬</b>이 높아 대인 관계가 원만할 것입니다.");
if (isHigh(stats.i)) comments.push("🧠 <b>지성 리듬</b>이 정점입니다. 복잡한 업무나 공부에 최적입니다.");
if (comments.length === 0) comments.push("전반적으로 안정적인 리듬입니다. 평소대로 활동하세요.");
analysisText.innerHTML = comments.slice(0, 2).join("<br>");
}
birthInput.addEventListener('change', draw);
targetInput.addEventListener('change', draw);
// 초기 로딩 애니메이션 실행
window.addEventListener('load', () => {
setTimeout(resizeCanvas, 100);
});
</script>
</body>
</html>