GNU/_PAGE/chart/upbit/whale/short-term_price.php
<?php require_once '/home/www/GNU/_PAGE/head.php'; ?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>관측 코어 - 거래대금 특화형 (V11-Final-Score)</title>
    <!-- 웹 폰트 및 아이콘 로드 -->
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
    
    <style>
        body { 
            background-color: #020617; 
            color: #f8fafc; 
            margin: 0; 
            padding: 0; 
            font-family: 'Inter', 'Pretendard', sans-serif; 
            overflow-y: auto; 
            user-select: none; 
        }
        
        #chart-container { position: relative; width: 100%; height: 450px; border-bottom: 2px solid #1e293b; background: #020617; }
        canvas { image-rendering: crispedges; display: block; cursor: grab; width: 100%; height: 100%; }
        canvas:active { cursor: grabbing; }
        
        .info-overlay { 
            position: absolute; top: 15px; left: 15px; z-index: 50; 
            background: rgba(15, 23, 42, 0.9); padding: 18px; border-radius: 4px;
            border: 1px solid #334155; pointer-events: auto; backdrop-filter: blur(12px);
            cursor: move; width: 340px; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.5);
        }
        
        .status-bar { position: absolute; bottom: 10px; right: 55px; font-size: 10px; color: #475569; display: flex; gap: 15px; z-index: 20; }
        .btn-tool { background: #1e293b; color: #94a3b8; padding: 2px 8px; border-radius: 2px; font-size: 10px; border: 1px solid #475569; transition: all 0.2s; cursor: pointer; }
        .btn-tool:hover { background: #334155; color: #f8fafc; }
        
        #depth-bar { position: absolute; right: 0; top: 0; width: 45px; height: 450px; z-index: 10; display: flex; flex-direction: column; border-left: 1px solid #1e293b; background: #020617; }
        .depth-label { position: absolute; width: 100%; text-align: center; font-size: 9px; font-weight: bold; color: white; text-shadow: 1px 1px 2px black; z-index: 11; }
        
        .briefing-card { background: #0f172a; border: 1px solid #1e293b; border-radius: 4px; padding: 12px; display: flex; flex-direction: column; height: 100%; }
        .momentum-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; transition: all 0.2s; }
        .alert-blink { animation: alert-flash 0.5s infinite; }
        @keyframes alert-flash { 0%, 100% { opacity: 1; color: #fbbf24; } 50% { opacity: 0.3; color: #f43f5e; } }
        
        #briefing-content { height: 150px; overflow-y: auto; scroll-behavior: smooth; }
        #briefing-content::-webkit-scrollbar { width: 4px; }
        #briefing-content::-webkit-scrollbar-thumb { background-color: #334155; border-radius: 10px; }
        #briefing-content::-webkit-scrollbar-thumb:hover { background-color: #475569; }
        #briefing-content::-webkit-scrollbar-track { background: transparent; }

        #ratio-monitor { width: 100%; height: 25px; background: #010409; display: flex; align-items: center; padding: 0 10px; border-bottom: 1px solid #1e293b; }
        .ratio-bar-container { flex: 1; height: 4px; background: #111827; border-radius: 10px; overflow: hidden; position: relative; display: flex; border: 1px solid #1e293b; margin: 0 10px; }
        .ratio-bar-center { position: absolute; left: 50%; top: 0; width: 2px; height: 100%; background: rgba(255,255,255,0.4); z-index: 5; }
        #buy-ratio-fill { height: 100%; background: #f43f5e; transition: width 0.5s ease-out; }
        #sell-ratio-fill { height: 100%; background: #3b82f6; transition: width 0.5s ease-out; }

        #score-monitor { width: 100%; height: 25px; background: #010409; display: flex; align-items: center; padding: 0 10px; border-bottom: 1px solid #1e293b; }
        .score-bar-container { flex: 1; height: 6px; background: #111827; border-radius: 10px; overflow: hidden; position: relative; border: 1px solid #1e293b; margin: 0 10px; }
        #score-fill { position: absolute; left: 50%; height: 100%; width: 0%; transition: all 0.4s ease-out; }

        #log-container { padding: 1.5rem; display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; background: #010409; max-height: 250px; overflow: hidden; }

        #manual-container { 
            height: 130px; background: #020617; border-top: 1px solid #1e293b; padding: 12px; 
            display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; 
            overflow: hidden; 
        }
        .manual-box { background: #0f172a; border: 1px solid #334155; border-radius: 4px; padding: 10px; }
        .manual-title { font-size: 13px; font-weight: bold; color: #38bdf8; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
        .manual-item { font-size: 14px; color: #94a3b8; line-height: 1.6; }
        .key-badge { background: #1e293b; border: 1px solid #475569; padding: 1px 4px; border-radius: 3px; color: #f8fafc; margin-right: 4px; font-family: 'JetBrains Mono', monospace; font-size: 11px; }

        .val-label { font-size: 9px; color: #64748b; font-weight: bold; text-transform: uppercase; }
        .val-data { font-family: 'JetBrains Mono'; font-size: 11px; font-weight: bold; color: #cbd5e1; }

        .score-text-block { font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.4; border-bottom: 1px solid #1e293b; padding-bottom: 8px; margin-bottom: 8px; }
    </style>
</head>
<body>

    <div id="chart-container">
        <div id="depth-bar">
            <span id="bid-label" style="top: 10px;" class="depth-label">0%</span>
            <div id="bid-wall" style="flex: 50; background: #f43f5e; opacity: 0.6; transition: flex 0.5s;"></div>
            <div id="ask-wall" style="flex: 50; background: #3b82f6; opacity: 0.6; transition: flex 0.5s;"></div>
            <span id="ask-label" style="bottom: 10px;" class="depth-label">0%</span>
        </div>

        <div id="draggable-info" class="info-overlay shadow-2xl">
            <div id="score-text-display" class="score-text-block">
                <div class="text-white">종합 점수 : <span id="txt-global-score">+0.00</span></div>
                <div class="text-slate-400">거래대금 강도 : <span id="txt-vol-strength">+0.00</span></div>
                <div class="text-slate-400">비율 강도 : <span id="txt-ratio-strength">+0.00</span></div>
            </div>

            <div class="flex justify-between items-start gap-6 mb-2">
                <h1 class="text-[10px] font-bold text-blue-400 uppercase tracking-widest opacity-80"><i class="fa-solid fa-crosshairs mr-1"></i> 관측 코어 V11-스코어</h1>
                <div class="flex gap-1">
                    <button onclick="exportData()" class="btn-tool" title="데이터 복사"><i class="fa-solid fa-copy"></i></button>
                    <button onclick="captureCanvas()" class="btn-tool" title="차트 캡처"><i class="fa-solid fa-camera"></i></button>
                    <button onclick="location.reload()" class="btn-tool" title="새로고침"><i class="fa-solid fa-rotate"></i></button>
                </div>
            </div>
            <div class="flex justify-between items-center mb-1">
                <div class="flex items-baseline gap-3">
                    <span id="price" class="text-4xl font-mono font-bold text-emerald-400 tracking-tighter leading-none">0</span>
                    <span id="percent" class="text-base text-slate-500 font-mono">0.00%</span>
                </div>
                <div class="text-right">
                    <div class="text-[9px] text-slate-500 font-bold uppercase">종합 스코어</div>
                    <div id="global-score-text" class="text-2xl font-mono font-bold text-slate-400">0.00</div>
                </div>
            </div>
            <div id="stats" class="grid grid-cols-2 gap-x-6 gap-y-2 mt-4 text-[10px] text-slate-500 uppercase font-medium border-t border-slate-800 pt-4">
                <div class="flex justify-between"><span>실시간 거래대금</span><span id="v-val" class="text-amber-400 font-bold">-</span></div>
                <div class="flex justify-between"><span>체결 점유율</span><span id="val-val" class="text-slate-300">-</span></div>
                <div class="flex justify-between"><span>체결 강도</span><span id="speed-val" class="text-emerald-500 font-bold">-</span></div>
                <div class="flex justify-between"><span>상하 변동폭</span><span class="text-slate-300"><span id="h-val" class="text-rose-500">-</span>/<span id="l-val" class="text-blue-500">-</span></span></div>
            </div>
        </div>

        <div class="status-bar">
            <span id="whale-alert" class="hidden font-bold text-amber-500"><i class="fa-solid fa-whale mr-1"></i>대량 자금 체결</span>
            <span id="drag-mode" class="text-amber-500 font-bold hidden"><i class="fa-solid fa-hand-back-point-left mr-1"></i>과거 탐색 중</span>
            <span id="alert-text" class="hidden font-bold alert-blink"><i class="fa-solid fa-triangle-exclamation mr-1"></i>대금 스파이크</span>
            <span id="zoom-stat">확대: 120봉</span>
            <span id="conn-stat"><span class="inline-block w-2 h-2 bg-emerald-500 rounded-full mr-1"></span>관측 엔진 가동 중</span>
        </div>
        <canvas id="mainChart"></canvas>
    </div>

    <div id="ratio-monitor">
        <div class="text-[9px] font-bold text-slate-500 uppercase w-32">실시간 대금 점유율</div>
        <div class="ratio-bar-container">
            <div class="ratio-bar-center"></div>
            <div id="buy-ratio-fill" style="width: 50%;"></div>
            <div id="sell-ratio-fill" style="width: 50%;"></div>
        </div>
        <div id="ratio-text" class="text-[10px] font-mono text-slate-400 w-32 text-right">50% : 50%</div>
    </div>

    <div id="score-monitor">
        <div class="text-[9px] font-bold text-slate-500 uppercase w-32">종합 모멘텀 지수</div>
        <div class="score-bar-container">
            <div class="ratio-bar-center"></div>
            <div id="score-fill"></div>
        </div>
        <div id="score-val-label" class="text-[10px] font-mono text-slate-400 w-32 text-right">0.00</div>
    </div>

    <div id="log-container">
        <div class="briefing-card">
            <div class="text-[10px] text-slate-500 mb-1 font-bold uppercase tracking-widest">체결 모멘텀</div>
            <div class="flex items-center gap-3 mb-4">
                <div id="m-dot" class="momentum-dot bg-slate-700"></div>
                <div id="m-text" class="text-xs font-bold text-slate-400">데이터 로드...</div>
            </div>
            <div class="text-[10px] text-slate-500 mb-1 font-bold uppercase tracking-widest">거래 밀도</div>
            <div id="vix-gauge" class="text-xl font-mono font-bold text-emerald-400">0.00</div>
        </div>
        
        <div class="briefing-card md:col-span-2 flex flex-col overflow-hidden">
            <div class="text-[10px] text-slate-500 mb-2 font-bold uppercase flex justify-between shrink-0">
                <span><i class="fa-solid fa-list-ul mr-1"></i> 거래대금 변동 브리핑 로그</span>
                <span id="last-update" class="text-slate-700">-</span>
            </div>
            <div id="briefing-content" class="text-sm font-mono space-y-1"></div>
        </div>

        <div class="briefing-card md:col-span-2">
            <div class="text-[10px] text-slate-500 mb-3 font-bold uppercase tracking-widest flex justify-between border-b border-slate-800 pb-2">
                <span>실시간 대금 주도권 분석</span>
                <span class="text-blue-400 animate-pulse"><i class="fa-solid fa-microchip"></i> 스코어 AI</span>
            </div>
            <div class="flex-1 flex flex-col justify-between">
                <div id="market-sentiment" class="text-base font-bold text-slate-100 leading-tight">거래 데이터 대기 중...</div>
                <div class="grid grid-cols-3 gap-2 mt-4 bg-black/30 p-3 rounded">
                    <div class="flex flex-col"><span class="val-label">대금비율</span><span id="val-ratio" class="val-data">-</span></div>
                    <div class="flex flex-col"><span class="val-label">유입속도</span><span id="val-delta" class="val-data">-</span></div>
                    <div class="flex flex-col"><span class="val-label">체결강도</span><span id="val-speed" class="val-data">-</span></div>
                </div>
            </div>
        </div>
    </div>

    <div id="manual-container">
        <div class="manual-box">
            <span class="manual-title"><i class="fa-solid fa-gamepad"></i> 인터랙션 제어</span>
            <div class="manual-item">
                <p><span class="key-badge">창 이동</span> 상단 정보창 드래그 이동</p>
                <p><span class="key-badge">드래그</span> 차트 가로 탐색</p>
                <p><span class="key-badge">휠 조작</span> 캔들 확대/축소 (대금 데이터 연동)</p>
            </div>
        </div>
        <div class="manual-box">
            <span class="manual-title"><i class="fa-solid fa-chart-line"></i> 거래대금 데이터 독해</span>
            <div class="manual-item">
                <p>■ <span class="text-rose-500 font-bold">비율 바</span>: 현재 구간 내 매수 체결대금 비중</p>
                <p>■ <span class="text-amber-400 font-bold">대형 자금</span>: 평균 대비 320% 대금 발생 시 경보</p>
                <p>■ <span class="text-emerald-500 font-bold">종합 점수</span>: 거래대금 편차와 체결비율 가중 합산</p>
            </div>
        </div>
        <div class="manual-box">
            <span class="manual-title"><i class="fa-solid fa-shield-halved"></i> 시스템 시그널 안내</span>
            <div class="manual-item">
                <p>■ <span class="text-violet-400 font-bold">관성 이탈</span>: 대금 급감과 함께 추세 이탈 감지</p>
                <p>■ <span class="text-amber-500 font-bold">스파이크</span>: 실시간 거래대금 폭증 로그 기록</p>
                <p>■ <span class="text-emerald-500 font-bold">AI 분석</span>: 체결대금 기반 주도 세력 추정</p>
            </div>
        </div>
    </div>

    <script>
        const canvas = document.getElementById('mainChart');
        const ctx = canvas.getContext('2d');
        const chartData = [];
        let zoomPoints = 120, offset = 0, isDragging = false, lastX = 0;
        let basePrice = 100000000, sessionStartPrice = 100000000, lastPriceTime = Date.now(), audioCtx = null;
        let lastBuyRatio = 50;

        const dragInfo = document.getElementById('draggable-info');
        let isMoving = false, moveStartX, moveStartY;

        // 초기화: 부모 컨테이너 너비를 정확히 참조하여 왼쪽 쏠림 해결
        function init() {
            const dpr = window.devicePixelRatio || 1;
            const container = document.getElementById('chart-container');
            canvas.width = container.clientWidth * dpr;
            canvas.height = 450 * dpr;
            ctx.setTransform(1, 0, 0, 1, 0, 0); 
            ctx.scale(dpr, dpr);
            draw();
        }

        // 브라우저 리사이즈 시 대응
        window.addEventListener('resize', init);

        function handleUserInteraction() {
            if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
        }

        // 사용자 첫 클릭 시 오디오 컨텍스트 활성화 (브라우저 제한 해결)
        window.addEventListener('click', handleUserInteraction, { once: true });

        dragInfo.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON' || e.target.parentElement.tagName === 'BUTTON') return;
            handleUserInteraction();
            isMoving = true;
            moveStartX = e.clientX - dragInfo.offsetLeft;
            moveStartY = e.clientY - dragInfo.offsetTop;
            dragInfo.style.opacity = "0.8";
        });

        window.addEventListener('mousemove', (e) => {
            if (!isMoving) return;
            dragInfo.style.left = (e.clientX - moveStartX) + 'px';
            dragInfo.style.top = (e.clientY - moveStartY) + 'px';
        });

        window.addEventListener('mouseup', () => { isMoving = false; dragInfo.style.opacity = "0.9"; });

        function playSound(up, intense = false) {
            try {
                if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                if(audioCtx.state === 'suspended') return;
                const osc = audioCtx.createOscillator(), g = audioCtx.createGain();
                osc.frequency.setValueAtTime(up ? (intense ? 1200 : 900) : (intense ? 200 : 350), audioCtx.currentTime);
                g.gain.setValueAtTime(intense ? 0.03 : 0.015, audioCtx.currentTime);
                g.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.05);
                osc.connect(g); g.connect(audioCtx.destination);
                osc.start(); osc.stop(audioCtx.currentTime + 0.05);
            } catch(e) {}
        }

        function addBriefing(msg, type = "normal") {
            const content = document.getElementById('briefing-content');
            const color = type === "spike" ? "text-amber-500 font-bold" : (type === "breakout" ? "text-violet-400 font-bold" : (type === "whale" ? "text-yellow-400 font-bold" : "text-slate-400"));
            const logEntry = document.createElement('div');
            logEntry.className = color;
            logEntry.innerText = `[${new Date().toLocaleTimeString('ko-KR',{hour12:false})}] ${msg}`;
            content.insertBefore(logEntry, content.firstChild);
            // 로그 50개 제한
            if(content.children.length > 50) content.removeChild(content.lastChild);
        }

        function update() {
            const last = chartData.length > 0 ? chartData[chartData.length-1].p : basePrice;
            const now = Date.now();
            const next = last + (Math.random()*600000 - 300000);
            const speed = (Math.abs(next - last) / (now - lastPriceTime) * 100).toFixed(2);
            lastPriceTime = now;
            
            const vol = Math.random()*45+5;
            const tradeValue = vol * next; 
            const currentSide = Math.random()>0.48 ? 'B' : 'S';
            
            const isSpike = chartData.length > 0 && tradeValue > chartData[chartData.length-1].val * 3.2;
            const isWhale = tradeValue > 4000000000; 
            
            const viewForB = chartData.slice(-35);
            const bMax = viewForB.length ? Math.max(...viewForB.map(d=>d.p)) : next;
            const bMin = viewForB.length ? Math.min(...viewForB.map(d=>d.p)) : next;
            let isBreakout = (next > bMax + 95000 || next < bMin - 95000);

            // 실시간 데이터 추가 시 과거 탐색 중이면 offset 증가시켜 화면 유지
            if (offset > 0) offset++;
            
            chartData.push({ p: next, v: vol, val: tradeValue, s: currentSide, spd: speed, spike: isSpike, breakout: isBreakout, whale: isWhale });
            if(chartData.length > 5000) chartData.shift();

            document.getElementById('price').innerText = Math.floor(next).toLocaleString();
            document.getElementById('percent').innerText = `${(((next/sessionStartPrice)-1)*100).toFixed(2)}%`;
            document.getElementById('v-val').innerText = (tradeValue / 100000000).toFixed(2) + "억";
            document.getElementById('val-val').innerText = Math.round(lastBuyRatio) + "%";
            document.getElementById('speed-val').innerText = speed;
            document.getElementById('vix-gauge').innerText = (speed / 100).toFixed(2);
            
            if(isSpike) addBriefing(`거래대금 급증 감지`, "spike");
            if(isBreakout) { addBriefing(`관성 이탈 시그널`, "breakout"); playSound(next > last); }
            if(isWhale) { addBriefing(`대형 자금 체결 포착: ${(tradeValue/100000000).toFixed(1)}억`, "whale"); playSound(next > last, true); }
            
            document.getElementById('whale-alert').classList.toggle('hidden', !isWhale);
            document.getElementById('alert-text').classList.toggle('hidden', !isSpike && !isBreakout);

            const viewRange = chartData.slice(-(zoomPoints + offset), offset === 0 ? undefined : -offset);
            if(viewRange.length > 0) {
                const buyVal = viewRange.filter(x => x.s === 'B').reduce((a, b) => a + b.val, 0);
                const sellVal = viewRange.filter(x => x.s === 'S').reduce((a, b) => a + b.val, 0);
                const totalVal = buyVal + sellVal;
                const buyRatio = totalVal > 0 ? (buyVal / totalVal * 100) : 50;

                document.getElementById('buy-ratio-fill').style.width = buyRatio + "%";
                document.getElementById('sell-ratio-fill').style.width = (100 - buyRatio) + "%";
                document.getElementById('ratio-text').innerText = `${Math.round(buyRatio)}% : ${Math.round(100 - buyRatio)}%`;
                
                const avgVal = viewRange.reduce((a, b) => a + b.val, 0) / viewRange.length || 1;
                const strength_v = Math.min(1, Math.max(-1, (tradeValue - avgVal) / avgVal));
                const strength_r = Math.min(1, Math.max(-1, (buyRatio - 50) / 50));
                const finalScore = Math.min(1, Math.max(-1, (strength_v * 0.6) + (strength_r * 0.4)));

                updateScoreUI(finalScore, strength_v, strength_r);
                updateMarketSentiment(buyRatio, last, next, speed);
                lastBuyRatio = buyRatio;

                document.getElementById('bid-wall').style.flex = buyRatio; 
                document.getElementById('ask-wall').style.flex = 100 - buyRatio;
                document.getElementById('bid-label').innerText = Math.round(buyRatio) + "%"; 
                document.getElementById('ask-label').innerText = Math.round(100 - buyRatio) + "%";
            }
            draw();
        }

        // 실시간 업데이트 인터벌 가동 (1초)
        setInterval(update, 1000);

        function updateScoreUI(score, sv, sr) {
            const scoreTextEl = document.getElementById('global-score-text');
            const scoreFillEl = document.getElementById('score-fill');
            const scoreLabelEl = document.getElementById('score-val-label');
            const txtGlobal = document.getElementById('txt-global-score');
            const txtVol = document.getElementById('txt-vol-strength');
            const txtRatio = document.getElementById('txt-ratio-strength');

            const fmt = (v) => (v >= 0 ? "+" : "") + v.toFixed(2);
            
            txtGlobal.innerText = fmt(score);
            txtVol.innerText = fmt(sv);
            txtRatio.innerText = fmt(sr);

            scoreTextEl.innerText = score.toFixed(2);
            scoreLabelEl.innerText = fmt(score);
            
            // 극단값 및 색상 처리
            if(score > 0.1) {
                scoreTextEl.className = "text-2xl font-mono font-bold text-rose-500";
                scoreFillEl.style.background = "#f43f5e";
                txtGlobal.className = "text-rose-500 font-bold";
            } else if(score < -0.1) {
                scoreTextEl.className = "text-2xl font-mono font-bold text-blue-500";
                scoreFillEl.style.background = "#3b82f6";
                txtGlobal.className = "text-blue-500 font-bold";
            } else {
                scoreTextEl.className = "text-2xl font-mono font-bold text-slate-400";
                scoreFillEl.style.background = "#94a3b8";
                txtGlobal.className = "text-white";
            }

            if(score >= 0) {
                scoreFillEl.style.left = "50%";
                scoreFillEl.style.width = Math.min(50, score * 50) + "%";
            } else {
                const w = Math.min(50, Math.abs(score) * 50);
                scoreFillEl.style.left = (50 - w) + "%";
                scoreFillEl.style.width = w + "%";
            }
        }

        function updateMarketSentiment(ratio, prevPrice, curPrice, speed) {
            const sentimentEl = document.getElementById('market-sentiment');
            const delta = ratio - lastBuyRatio;
            let dominance = ratio > 58 ? "자금 매수 압도" : (ratio > 52 ? "자금 매수 우위" : (ratio < 42 ? "자금 매도 압도" : (ratio < 48 ? "자금 매도 우위" : "수급 중립")));
            sentimentEl.innerText = `${Math.abs(delta) > 2.5 ? "폭발적" : "활발한"} ${dominance}`;
            sentimentEl.style.color = ratio > 52 ? "#f43f5e" : (ratio < 48 ? "#3b82f6" : "#f1f5f9");
            document.getElementById('val-ratio').innerText = Math.round(ratio) + "%";
            document.getElementById('val-delta').innerText = (delta > 0 ? "+" : "") + delta.toFixed(1);
            document.getElementById('val-speed').innerText = speed;
        }

        function draw() {
            const dpr = window.devicePixelRatio || 1;
            const w = canvas.width / dpr, h = 450; ctx.clearRect(0, 0, w, h);
            // depth-bar 너비(45)를 제외한 실제 드로잉 영역 계산
            const drawWidth = w - 45;
            const startIndex = Math.max(0, chartData.length - zoomPoints - offset);
            const view = chartData.slice(startIndex, startIndex + zoomPoints);
            if(view.length < 2) return;

            const prices = view.map(d => d.p);
            const maxP = Math.max(...prices);
            const minP = Math.min(...prices);
            const range = (maxP - minP) || 1;
            
            const maxVal = Math.max(...view.map(d => d.val));
            const getX = (i) => i * (drawWidth / (zoomPoints - 1));
            // 가격 범위 스케일링 (상하 10% 여유)
            const getY = (p) => h - ((p - (minP - range*0.1)) / (range * 1.2) * (h * 0.8)) - (h * 0.1);

            ctx.strokeStyle = '#0f172a';
            for(let i=1; i<6; i++){ ctx.beginPath(); ctx.moveTo(0,h*(i/6)); ctx.lineTo(drawWidth,h*(i/6)); ctx.stroke(); }

            const curP = view[view.length-1].p, curY = getY(curP);
            ctx.setLineDash([2, 4]); ctx.strokeStyle = '#64748b'; ctx.beginPath(); ctx.moveTo(0, curY); ctx.lineTo(drawWidth, curY); ctx.stroke();
            ctx.setLineDash([]); ctx.font = "11px 'JetBrains Mono'"; ctx.fillStyle = '#10b981'; ctx.fillText(Math.floor(curP).toLocaleString(), drawWidth - 100, curY-6);

            view.forEach((d, i) => {
                const x = getX(i), bw = Math.max(1, (drawWidth / zoomPoints) - 1);
                const bh = (Math.log10(1 + d.val) / (Math.log10(1 + (maxVal||1)) * 1.5)) * h * 0.4;
                ctx.fillStyle = d.spike ? '#fbbf24' : (d.s === 'B' ? '#f43f5e' : '#3b82f6');
                ctx.globalAlpha = 0.25; ctx.fillRect(x - (bw/2), h - bh, bw, bh);
                ctx.globalAlpha = 1.0;
            });

            ctx.beginPath(); ctx.strokeStyle = '#10b981'; ctx.lineWidth = 2;
            view.forEach((d, i) => { if(i===0) ctx.moveTo(getX(i), getY(d.p)); else ctx.lineTo(getX(i), getY(d.p)); });
            ctx.stroke(); 
        }

        // 드래그 로직 개선: 과거 탐색 시 X 좌표 계산 불일치 해결
        canvas.addEventListener('mousedown', (e) => { 
            handleUserInteraction(); 
            isDragging = true; 
            lastX = e.clientX; 
        });
        window.addEventListener('mouseup', () => { isDragging = false; });
        window.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - lastX;
            if (Math.abs(dx) > 1) {
                // 확대 수준에 따른 이동 감도 조절
                const sensitivity = zoomPoints / 400;
                offset = Math.max(0, Math.min(chartData.length - zoomPoints, offset - Math.round(dx * sensitivity)));
                lastX = e.clientX; 
                document.getElementById('drag-mode').classList.toggle('hidden', offset === 0); 
                draw();
            }
        });

        canvas.addEventListener('wheel', (e) => {
            e.preventDefault(); 
            handleUserInteraction();
            zoomPoints = Math.min(2000, Math.max(10, zoomPoints + (e.deltaY > 0 ? 20 : -20)));
            document.getElementById('zoom-stat').innerText = `확대: ${zoomPoints}봉`; 
            draw();
        }, { passive: false });

        // 더미 데이터 초기 생성
        for(let i=0; i<1500; i++) { 
            basePrice += (Math.random()*200000-100000); 
            const vol = Math.random()*15+5;
            chartData.push({p:basePrice, v:vol, val: vol * basePrice, s:Math.random()>0.48?'B':'S', spike:false, breakout:false, whale:false}); 
        }
        
        init();
    </script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>