GNU/_PAGE/chart/upbit/ecg/volume_mini_basic.php
<?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'; ?>