<?php require_once '/home/www/GNU/_PAGE/head.php'; ?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Volume Heartbeat</title>
<style>
:root {
--bg-primary: #0a0e27;
--bg-secondary: #151932;
--bg-tertiary: #1e2742;
--bg-card: #1a1f3a;
--bg-hover: #252b4a;
--bg-border: #2a3458;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-primary: #3b82f6;
--accent-secondary: #8b5cf6;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--border-color: #2a3458;
--heartbeat-color: #10b981;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, var(--bg-primary) 0%, #0f172a 100%);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Malgun Gothic', 'Roboto', sans-serif;
padding: 0px;
min-height: 100vh;
line-height: 1.6;
}
h2 {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 8px;
margin: 40px 40px 0 40px;
}
#heartbeat-container {
padding: 16px;
background: var(--bg-card);
border-radius: 7px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-lg);
transition: all 0.3s ease;
margin: 15px 40px 0 40px;
}
#heartbeat-container:hover {
border-color: var(--accent-primary);
box-shadow: 0 12px 24px rgba(59, 130, 246, 0.15);
}
#heartbeat-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
color: var(--text-primary);
}
#heartbeat-title > span {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
}
#heartbeat-info {
font-size: 12px;
color: var(--text-secondary);
padding: 6px 12px;
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-color);
font-family: 'Courier New', monospace;
font-weight: 500;
margin-left: 6px;
}
#heartbeat-controls {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
font-size: 13px;
}
#heartbeat-controls > label {
color: var(--text-secondary);
font-weight: 500;
}
#heartbeat-market {
padding: 8px 12px;
font-size: 13px;
background: var(--bg-tertiary);
color: var(--text-primary);
border-radius: 8px;
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
outline: none;
}
#heartbeat-market:hover {
background: var(--bg-hover);
border-color: var(--accent-primary);
}
#heartbeat-market:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
#heartbeat-market option {
background: var(--bg-card);
color: var(--text-primary);
padding: 8px;
}
#heartbeat {
width: 100%;
height: 70px;
background: linear-gradient(to bottom, #000000 0%, #0a0e27 100%);
border: 1px solid var(--border-color);
border-radius: 8px;
display: block;
margin-top: 12px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* 스크롤바 스타일링 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
border: 2px solid var(--bg-secondary);
}
::-webkit-scrollbar-thumb:hover {
background: var(--bg-hover);
}
/* 반응형 디자인 */
@media (max-width: 768px) {
body {
padding: 16px;
}
h2 {
font-size: 20px;
}
#heartbeat-title {
flex-direction: column;
align-items: flex-start;
}
#heartbeat-controls {
width: 100%;
flex-direction: column;
align-items: flex-start;
}
#heartbeat-market {
width: 100%;
}
}
</style>
</head>
<body>
<h2>📈 거래량 심전도 - 미니 베이직 버전</h2>
<div id="heartbeat-container">
<div id="heartbeat-title">
<span>거래량 심전도 (Volume Heartbeat) Mini Basic</span>
<div id="heartbeat-controls">
<label for="heartbeat-market">감시 코인:</label>
<select id="heartbeat-market">
<option value="KRW-BTC">BTC</option>
<option value="KRW-ETH">ETH</option>
<option value="KRW-XRP">XRP</option>
<option value="KRW-QTUM">QTUM</option>
<option value="KRW-TRUMP">TRUMP</option>
</select>
<span id="heartbeat-info">데이터 대기 중...</span>
</div>
</div>
<canvas id="heartbeat" width="800" height="70"></canvas>
</div>
<script>
const HEARTBEAT_POINTS = 120;
let heartbeatMarket = "KRW-BTC";
let heartbeatData = [];
const heartbeatCanvas = document.getElementById('heartbeat');
const hbCtx = heartbeatCanvas.getContext('2d');
const heartbeatInfoEl = document.getElementById('heartbeat-info');
const heartbeatSelect = document.getElementById('heartbeat-market');
// Canvas 크기 조정 (반응형)
function resizeCanvas() {
const container = heartbeatCanvas.parentElement;
const containerWidth = container.clientWidth;
heartbeatCanvas.width = containerWidth;
heartbeatCanvas.height = 70;
drawHeartbeat();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
heartbeatSelect.addEventListener('change', () => {
heartbeatMarket = heartbeatSelect.value;
heartbeatData = [];
drawHeartbeat();
});
function updateHeartbeat(ts, volume) {
heartbeatData.push({ ts, volume });
while (heartbeatData.length > HEARTBEAT_POINTS) {
heartbeatData.shift();
}
drawHeartbeat();
}
function drawHeartbeat() {
const ctx = hbCtx;
const w = heartbeatCanvas.width;
const h = heartbeatCanvas.height;
ctx.clearRect(0, 0, w, h);
if (heartbeatData.length < 2) {
ctx.fillStyle = "#64748b";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText("데이터 수집 중...", w / 2, h / 2);
heartbeatInfoEl.textContent = "심전도: 데이터 수집 중";
return;
}
let minV = heartbeatData[0].volume;
let maxV = heartbeatData[0].volume;
for (let i = 1; i < heartbeatData.length; i++) {
const v = heartbeatData[i].volume;
if (v < minV) minV = v;
if (v > maxV) maxV = v;
}
if (maxV <= 0) maxV = 1;
const range = maxV - minV || 1;
let sum = 0;
let sumSq = 0;
for (let i = 0; i < heartbeatData.length; i++) {
const v = heartbeatData[i].volume;
sum += v;
sumSq += v * v;
}
const n = heartbeatData.length;
const mean = sum / n;
const variance = Math.max(0, sumSq / n - mean * mean);
const std = Math.sqrt(variance);
heartbeatInfoEl.textContent = `최근 ${heartbeatData.length}틱 | 평균≈${mean.toFixed(4)} | σ≈${std.toFixed(4)}`;
// 그라데이션 생성 (밝은 초록에서 어두운 초록으로)
const gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, "#10b981");
gradient.addColorStop(0.5, "#059669");
gradient.addColorStop(1, "#047857");
ctx.strokeStyle = gradient;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.shadowBlur = 4;
ctx.shadowColor = "rgba(16, 185, 129, 0.5)";
ctx.beginPath();
for (let i = 0; i < heartbeatData.length; i++) {
const x = (i / (heartbeatData.length - 1)) * (w - 2) + 1;
const v = heartbeatData[i].volume;
const yRatio = (v - minV) / range;
const y = h - 1 - yRatio * (h - 2);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// 그림자 초기화
ctx.shadowBlur = 0;
}
// WebSocket
let ws = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
function connectWS() {
try {
ws = new WebSocket("wss://api.upbit.com/websocket/v1");
ws.onopen = () => {
reconnectAttempts = 0;
ws.send(JSON.stringify([
{ ticket: "volume_only" },
{ type: "ticker", codes: ["KRW-BTC", "KRW-ETH", "KRW-XRP", "KRW-QTUM", "KRW-TRUMP"] }
]));
heartbeatInfoEl.textContent = "연결됨: 데이터 수집 중...";
};
ws.onmessage = (event) => {
event.data.arrayBuffer().then(buffer => {
const decoder = new TextDecoder("utf-8");
const json = decoder.decode(buffer);
try {
const data = JSON.parse(json);
const market = data.cd || data.code;
const volume = Number(data.tv || data.trade_volume || 0);
const ts = Date.now();
if (market === heartbeatMarket && volume > 0) {
updateHeartbeat(ts, volume);
}
} catch (e) {
console.error("데이터 파싱 오류:", e);
}
});
};
ws.onerror = (error) => {
console.error("WebSocket 오류:", error);
heartbeatInfoEl.textContent = "연결 오류 발생";
};
ws.onclose = () => {
heartbeatInfoEl.textContent = "연결 끊김: 재연결 시도 중...";
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(() => {
connectWS();
}, 3000 * reconnectAttempts);
} else {
heartbeatInfoEl.textContent = "연결 실패: 페이지를 새로고침하세요";
}
};
} catch (e) {
console.error("WebSocket 연결 오류:", e);
heartbeatInfoEl.textContent = "연결 실패";
}
}
connectWS();
</script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>