<?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'; ?>