GNU/_PAGE/chart/upbit/line/volume_line_full.php
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>실시간 업비트 거래량 차트</title>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<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;
    --warning: #ffb300;
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
}

body {
    margin: 0;
    padding: 0;
    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;
    transition: filter 0.3s ease;
}

body.night-mode {
    filter: contrast(0.95) brightness(0.98);
}

body.day-mode {
    filter: contrast(1.05) brightness(1.02);
}

header {
    padding: 16px 20px;
    background: var(--bg-card);
    border-bottom: 1px solid var(--bg-border);
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: var(--shadow-lg);
    position: relative;
}

header::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, #f87171, transparent);
    opacity: 0;
    transition: opacity 0.3s ease;
}

header.reconnecting::after {
    opacity: 1;
    animation: reconnectPulse 1.5s ease infinite;
}

@keyframes reconnectPulse {
    0%, 100% { opacity: 0.3; }
    50% { opacity: 1; }
}

h2 {
    margin: 0;
    font-size: 22px;
    font-weight: 700;
    color: var(--text-primary);
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

select {
    background: rgba(15, 21, 37, 0.5);
    color: rgba(226, 232, 240, 0.6);
    padding: 8px 12px;
    border: 1px solid rgba(42, 52, 88, 0.5);
    border-radius: 8px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s ease;
}

select:hover {
    background: rgba(18, 34, 104, 0.9);
    border-color: rgba(59, 130, 246, 1);
}

select:focus {
    outline: none;
    border-color: rgba(59, 130, 246, 0.4);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.05);
}

#volumeBox {
    margin-left: 16px;
    font-size: 15px;
    font-weight: bold;
    color: var(--warning);
    padding: 5px 16px;
    background: var(--bg-tertiary);
    border-radius: 8px;
    border: 1px solid var(--bg-border);
    transition: color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease, filter 0.3s ease;
}

#volumeBox.flash {
    background-color: rgba(255, 179, 0, 0.25);
    box-shadow: 0 0 12px rgba(255, 179, 0, 0.4);
}

#volumeBox.dim {
    color: #cc9000;
}

#volumeBox.stopped {
    color: #888;
    opacity: 0.5;
    filter: grayscale(0.8);
}

#volumeBox.alert {
    animation: borderBlink 0.4s ease infinite;
}

@keyframes borderBlink {
    0%, 100% { border-color: var(--bg-border); }
    50% { border-color: #ff4444; }
}

#volumeSpeed {
    margin-left: 8px;
    font-size: 12px;
    font-weight: normal;
}

#volumeSpeed.up {
    color: #4ade80;
}

#volumeSpeed.down {
    color: #fbbf24;
}

#volumeSpeed.neutral {
    color: #94a3b8;
}

.loading-text {
    font-size: 12px;
    color: rgba(226, 232, 240, 0.5);
}

#wsStatus {
    font-size: 11px;
    color: var(--text-muted);
    margin-left: 12px;
}

#wsStatus.connected {
    color: #4ade80;
}

#wsStatus.disconnected {
    color: #f87171;
}

#wsStatus.reconnecting {
    color: #fbbf24;
}

#tensionGauge {
    width: 100px;
    height: 6px;
    background: rgba(42, 52, 88, 0.5);
    border-radius: 3px;
    margin-left: 12px;
    position: relative;
    overflow: hidden;
}

#tensionGaugeBar {
    height: 100%;
    background: linear-gradient(90deg, #4ade80, #fbbf24, #f87171);
    width: 0%;
    transition: width 0.5s ease;
    border-radius: 3px;
}

#sensitivityControl {
    margin-left: 8px;
    display: flex;
    gap: 4px;
}

#sensitivityControl button {
    background: rgba(15, 21, 37, 0.5);
    color: rgba(148, 163, 184, 0.6);
    border: 1px solid rgba(42, 52, 88, 0.5);
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 10px;
    cursor: pointer;
    transition: all 0.2s ease;
}

#sensitivityControl button.active {
    background: rgba(59, 130, 246, 0.2);
    border-color: rgba(59, 130, 246, 0.5);
    color: var(--text-primary);
}

#chart-wrap {
    height: 700px;
    padding: 20px;
    background: var(--bg-secondary);
    position: relative;
    transition: transform 0.1s ease, filter 0.3s ease;
}

#chart-wrap.shake {
    animation: chartShake 0.3s ease;
}

@keyframes chartShake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-2px); }
    75% { transform: translateX(2px); }
}

#chart-wrap.stopped {
    filter: grayscale(0.6) contrast(0.9);
}

#chart-wrap::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(ellipse at center, rgba(255, 179, 0, 0.02) 0%, transparent 60%);
    pointer-events: none;
    z-index: 2;
}

#chartAlertOverlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(255, 68, 68, 0.08);
    pointer-events: none;
    z-index: 3;
    opacity: 0;
    transition: opacity 0.3s ease;
}

#chartAlertOverlay.active {
    opacity: 1;
}

#chartDenseBand {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(255, 179, 0, 0.03);
    pointer-events: none;
    z-index: 2;
    opacity: 0;
    transition: opacity 0.3s ease;
}

#chartDenseBand.active {
    opacity: 1;
}

#chartHeatZone {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(to bottom, rgba(255, 179, 0, 0.05) 0%, transparent 50%);
    pointer-events: none;
    z-index: 2;
    opacity: 0;
    transition: opacity 0.5s ease;
}

#chartHeatZone.active {
    opacity: 1;
}

#chartHoverHighlight {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.3);
    pointer-events: none;
    z-index: 4;
    opacity: 0;
    transition: opacity 0.2s ease;
}

#chartHoverHighlight.active {
    opacity: 1;
}

#chartCanvas {
    width: 100%;
    height: 100%;
    background: linear-gradient(to bottom, #000000 0%, var(--bg-primary) 100%);
    border-radius: 12px;
    border: 1px solid var(--bg-border);
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
    position: relative;
    z-index: 1;
    transition: filter 0.3s ease;
}

#sparkContainer {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    pointer-events: none;
    z-index: 5;
}

.spark {
    position: absolute;
    width: 4px;
    height: 4px;
    background: #ffb300;
    border-radius: 50%;
    pointer-events: none;
    animation: sparkFade 0.6s ease-out forwards;
}

@keyframes sparkFade {
    0% {
        opacity: 1;
        transform: scale(1);
    }
    100% {
        opacity: 0;
        transform: scale(0) translate(var(--spark-x, 0), var(--spark-y, 0));
    }
}

a {
    background: rgba(15, 21, 37, 0.5);
    color: rgba(148, 163, 184, 0.6);
    border: 1px solid rgba(42, 52, 88, 0.5);
    border-radius: 6px;
    padding: 6px 12px;
    font-size: 13px;
    text-decoration: none;
    transition: all 0.2s ease;
}

a:hover {
    background: rgba(18, 34, 104, 0.6);
    color: rgba(226, 232, 240, 0.8);
    border-color: rgba(255, 179, 0, 0.4);
}

#toggleEffects {
    background: rgba(15, 21, 37, 0.5);
    color: rgba(148, 163, 184, 0.6);
    border: 1px solid rgba(42, 52, 88, 0.5);
    border-radius: 6px;
    padding: 6px 12px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s ease;
}

#toggleEffects:hover {
    background: rgba(18, 34, 104, 0.6);
    border-color: rgba(255, 179, 0, 0.4);
}

#toggleEffects.active {
    background: rgba(59, 130, 246, 0.2);
    border-color: rgba(59, 130, 246, 0.5);
    color: var(--text-primary);
}

#eventLog {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 250px;
    max-height: 200px;
    background: rgba(10, 14, 39, 0.95);
    border: 1px solid var(--bg-border);
    border-radius: 8px;
    padding: 10px;
    font-size: 11px;
    color: var(--text-secondary);
    overflow-y: auto;
    z-index: 1000;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}

#eventLog .event {
    margin-bottom: 6px;
    padding: 4px 8px;
    border-left: 2px solid transparent;
    border-radius: 3px;
}

#eventLog .event.surge {
    border-left-color: #f87171;
    background: rgba(248, 113, 113, 0.1);
}

#eventLog .event.stop {
    border-left-color: #94a3b8;
    background: rgba(148, 163, 184, 0.1);
}

#eventLog .event.reconnect {
    border-left-color: #fbbf24;
    background: rgba(251, 191, 36, 0.1);
}

#analysisGuide {
    margin: 20px;
    padding: 20px;
    background: var(--bg-card);
    border: 1px solid var(--bg-border);
    border-radius: 12px;
    box-shadow: var(--shadow-lg);
}

#analysisGuide h3 {
    margin: 0 0 16px 0;
    font-size: 18px;
    font-weight: 700;
    color: var(--warning);
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
}

#analysisGuide h3 button {
    background: transparent;
    border: 1px solid var(--bg-border);
    color: var(--text-secondary);
    padding: 4px 12px;
    border-radius: 6px;
    font-size: 11px;
    cursor: pointer;
    transition: all 0.2s ease;
}

#analysisGuide h3 button:hover {
    border-color: var(--warning);
    color: var(--warning);
}

#analysisGuideContent {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 16px;
}

#analysisGuideContent.collapsed {
    display: none;
}

.analysis-item {
    padding: 12px;
    background: rgba(15, 21, 37, 0.3);
    border-left: 3px solid var(--warning);
    border-radius: 6px;
}

.analysis-item h4 {
    margin: 0 0 8px 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
}

.analysis-item p {
    margin: 4px 0;
    font-size: 12px;
    color: var(--text-secondary);
    line-height: 1.5;
}

.analysis-item .method {
    color: var(--warning);
    font-weight: 500;
}

.analysis-item .analysis {
    color: #4ade80;
}

.analysis-item .usage {
    color: #3b82f6;
    font-style: italic;
}

::-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);
}
</style>
</head>

<body>

<?php
// ===== 업비트 마켓 목록 =====
$api = 'https://api.upbit.com/v1/market/all';
$data = json_decode(file_get_contents($api), true);
if ($data === null) $data = [];

// ===== 파라미터 =====
$market     = $_GET['market'] ?? 'KRW-BTC';
$chart_time = isset($_GET['time']) ? (int)$_GET['time'] : 0;

// ===== 마켓 검증 =====
$validMarket = 'KRW-BTC';
foreach ($data as $m) {
    if ($m['market'] === $market && strpos($m['market'], 'KRW-') === 0) {
        $validMarket = $market;
        break;
    }
}
?>

<header>
<h2>Real-Time Volume Line Chart</h2>

<div style="display:flex; align-items:center; gap:8px;">

    <!-- 시간 버튼 (HTML 링크) -->
    <a href="?market=<?= $validMarket ?>&time=1">1분</a>
    <a href="?market=<?= $validMarket ?>&time=5">5분</a>
    <a href="?market=<?= $validMarket ?>&time=15">15분</a>
    <a href="?market=<?= $validMarket ?>&time=60">1시간</a>
    <a href="?market=<?= $validMarket ?>&time=240">4시간</a>
    <a href="?market=<?= $validMarket ?>&time=480">8시간</a>
    <a href="?market=<?= $validMarket ?>&time=720">12시간</a>
    <a href="?market=<?= $validMarket ?>&time=1440">24시간</a>

    <!-- 코인 셀렉트 -->
    <select onchange="location.href='?market='+this.value+'&time=<?= $chart_time ?>'">
        <?php
        foreach ($data as $m) {
            if (strpos($m['market'], 'KRW-') !== 0) continue;

            $symbol   = str_replace('KRW-', '', $m['market']);
            $korean   = $m['korean_name'];
            $selected = ($m['market'] === $validMarket) ? ' selected' : '';

            echo "<option value=\"{$m['market']}\"{$selected}>{$symbol} : {$korean}</option>\n";
        }
        ?>
    </select>

    <div id="volumeBox"><span class="loading-text">로딩 중...</span></div>
    <span id="volumeSpeed"></span>
    <div id="tensionGauge"><div id="tensionGaugeBar"></div></div>
    <div id="sensitivityControl">
        <button onclick="setSensitivity('low', event)">둔감</button>
        <button onclick="setSensitivity('normal', event)">보통</button>
        <button onclick="setSensitivity('high', event)">민감</button>
    </div>
    <span id="wsStatus" class="disconnected">연결 중...</span>
    <button id="toggleEffects" onclick="toggleEffects()">효과 ON</button>
</div>
</header>

<div id="chart-wrap">
<div id="chartAlertOverlay"></div>
<div id="chartDenseBand"></div>
<div id="chartHeatZone"></div>
<div id="chartHoverHighlight"></div>
<div id="sparkContainer"></div>
<canvas id="chartCanvas"></canvas>
</div>

<div id="analysisGuide">
<h3>
    차트 분석 방법 가이드
    <button onclick="toggleGuide()">접기/펼치기</button>
</h3>
<div id="analysisGuideContent">
    <div class="analysis-item">
        <h4>1. 색상 스펙트럼 분석</h4>
        <p><span class="method">방법:</span> 차트 선 색상 관찰</p>
        <p><span class="analysis">분석:</span> 어두운색(낮음) → 노란색(중간) → 주황색(높음) → 적색(매우높음)</p>
        <p><span class="usage">활용:</span> 색상 변화로 거래량 강도 파악</p>
    </div>
    <div class="analysis-item">
        <h4>2. 선 두께 분석</h4>
        <p><span class="method">방법:</span> 차트 선 두께 관찰</p>
        <p><span class="analysis">분석:</span> 1px(최소) → 2-3px(중간) → 4px(최대)</p>
        <p><span class="usage">활용:</span> 두께 변화로 거래량 크기 추정</p>
    </div>
    <div class="analysis-item">
        <h4>3. 긴장도 게이지 분석</h4>
        <p><span class="method">방법:</span> header의 긴장도 바 확인</p>
        <p><span class="analysis">분석:</span> 0-30%(안정) → 30-60%(보통) → 60-100%(높은 변동성)</p>
        <p><span class="usage">활용:</span> 최근 10포인트 분산 기반 변동성 파악</p>
    </div>
    <div class="analysis-item">
        <h4>4. 거래량 속도 분석</h4>
        <p><span class="method">방법:</span> volumeSpeed 요소 확인</p>
        <p><span class="analysis">분석:</span> ▲X%(증가, 초록) / ▼X%(감소, 노랑)</p>
        <p><span class="usage">활용:</span> 단기 변화율 파악</p>
    </div>
    <div class="analysis-item">
        <h4>5. 연속 증가/감소 패턴</h4>
        <p><span class="method">방법:</span> 차트 선 색상 농도 관찰</p>
        <p><span class="analysis">분석:</span> 연속 3회 이상 증가 시 색상 밝아짐, 감소 시 탈색</p>
        <p><span class="usage">활용:</span> 추세 지속성 판단</p>
    </div>
    <div class="analysis-item">
        <h4>6. 급등 감지 분석</h4>
        <p><span class="method">방법:</span> 스파크 효과 및 이벤트 로그 확인</p>
        <p><span class="analysis">분석:</span> 1.5배 이상 급등 시 스파크 효과 발생</p>
        <p><span class="usage">활용:</span> 비정상적 급등 포착</p>
    </div>
    <div class="analysis-item">
        <h4>7. 거래량 급증 경고</h4>
        <p><span class="method">방법:</span> 차트 배경 오버레이 및 volumeBox 테두리 점멸 확인</p>
        <p><span class="analysis">분석:</span> 최근 10포인트 평균 대비 2.5배 이상 시 경고</p>
        <p><span class="usage">활용:</span> 비정상적 급증 알림</p>
    </div>
    <div class="analysis-item">
        <h4>8. 밀집 구간 분석</h4>
        <p><span class="method">방법:</span> chartDenseBand 배경 표시 확인</p>
        <p><span class="analysis">분석:</span> 최근 30포인트 중 상위 20% 구간이 15% 이상일 때 활성화</p>
        <p><span class="usage">활용:</span> 거래량 집중 구간 파악</p>
    </div>
    <div class="analysis-item">
        <h4>9. 히트존 분석</h4>
        <p><span class="method">방법:</span> chartHeatZone 배경 표시 확인</p>
        <p><span class="analysis">분석:</span> 현재 거래량이 최근 30포인트 상위 30% 평균의 80% 이상</p>
        <p><span class="usage">활용:</span> 고거래량 구간 식별</p>
    </div>
    <div class="analysis-item">
        <h4>10. 최대값 마커 분석</h4>
        <p><span class="method">방법:</span> 차트 상 큰 점(pointRadius: 5) 확인</p>
        <p><span class="analysis">분석:</span> 현재 화면(MAX_POINTS) 내 최대 거래량 위치 표시</p>
        <p><span class="usage">활용:</span> 최고 거래량 시점 파악</p>
    </div>
    <div class="analysis-item">
        <h4>11. 데이터 정지 감지</h4>
        <p><span class="method">방법:</span> volumeBox "정지" 표시 및 차트 채도 확인</p>
        <p><span class="analysis">분석:</span> 3초 이상 데이터 없음 시 "정지" 표시, 차트 채도 낮아짐</p>
        <p><span class="usage">활용:</span> 연결 상태 및 데이터 수신 상태 확인</p>
    </div>
    <div class="analysis-item">
        <h4>12. 이벤트 로그 분석</h4>
        <p><span class="method">방법:</span> 우측 하단 eventLog 패널 확인</p>
        <p><span class="analysis">분석:</span> 급증(빨강) / 정지(회색) / 재연결(노랑)</p>
        <p><span class="usage">활용:</span> 주요 이벤트 시간대 파악</p>
    </div>
    <div class="analysis-item">
        <h4>13. 마우스 오버 분석</h4>
        <p><span class="method">방법:</span> 차트에 마우스 오버 시 디밍 효과 확인</p>
        <p><span class="analysis">분석:</span> 특정 구간 오버 시 해당 구간 강조, 나머지 디밍</p>
        <p><span class="usage">활용:</span> 특정 시점 상세 확인</p>
    </div>
    <div class="analysis-item">
        <h4>14. 감도 조절 분석</h4>
        <p><span class="method">방법:</span> "둔감/보통/민감" 버튼으로 감도 조절</p>
        <p><span class="analysis">분석:</span> 둔감(0.5배) / 보통(1.0배) / 민감(2.0배)</p>
        <p><span class="usage">활용:</span> 분석 목적에 맞는 감도 설정</p>
    </div>
    <div class="analysis-item">
        <h4>15. 시간대별 분석</h4>
        <p><span class="method">방법:</span> 시간 버튼(1분~24시간) 선택</p>
        <p><span class="analysis">분석:</span> 단기(1-15분) / 중기(1-4시간) / 장기(8-24시간)</p>
        <p><span class="usage">활용:</span> 분석 기간에 맞는 시간 단위 선택</p>
    </div>
    <div class="analysis-item">
        <h4>16. 효과 ON/OFF 비교</h4>
        <p><span class="method">방법:</span> "효과 ON/OFF" 버튼으로 전환</p>
        <p><span class="analysis">분석:</span> ON(모든 시각 효과) / OFF(기본 차트만)</p>
        <p><span class="usage">활용:</span> 원시 데이터와 시각화 데이터 비교</p>
    </div>
    <div class="analysis-item">
        <h4>17. 연결 상태 모니터링</h4>
        <p><span class="method">방법:</span> header의 wsStatus 확인</p>
        <p><span class="analysis">분석:</span> 연결됨(초록) / 끊김(빨강) / 재연결 중(노랑)</p>
        <p><span class="usage">활용:</span> 데이터 수신 안정성 확인</p>
    </div>
</div>
</div>

<div id="eventLog"></div>

<script>
let MARKET   = "<?= $validMarket ?>";
let UNIT_MIN = <?= $chart_time ?>;
let ws = null;
const MAX_POINTS = 300;
const STOP_THRESHOLD = 3000;
const TENSION_UPDATE_INTERVAL = 500;
const SPARK_THRESHOLD = 1.5;
const ALERT_AVG_POINTS = 10;
const ALERT_MULTIPLIER = 2.5;
const DENSE_POINTS = 30;
let flashTimer = null;
let lastDataTime = Date.now();
let dataStopTimer = null;
let reconnectTimer = null;
let effectsEnabled = localStorage.getItem('volumeEffects') !== 'false';
let maxVolIndex = -1;
let alertOverlayTimer = null;
let checkDataStopInterval = null;
let timeModeInterval = null;
let sensitivity = localStorage.getItem('volumeSensitivity') || 'normal';
let consecutiveUp = 0;
let consecutiveDown = 0;
let dataUpdateTimes = [];
let stopEventLogged = false;
let lastTensionUpdate = 0;
let activeSparks = [];
let sparkTimers = [];
let guideCollapsed = localStorage.getItem('guideCollapsed') === 'true';

function toggleGuide() {
    guideCollapsed = !guideCollapsed;
    localStorage.setItem('guideCollapsed', guideCollapsed);
    const content = document.getElementById('analysisGuideContent');
    if (content) {
        content.classList.toggle('collapsed', guideCollapsed);
    }
}

function getLabel(unitMin) {
    const d = new Date();
    if (unitMin === 0) {
        return d.toTimeString().split(" ")[0]; // 실시간
    }
    const h  = String(d.getHours()).padStart(2, '0');
    const mm = String(Math.floor(d.getMinutes() / unitMin) * unitMin).padStart(2, '0');
    return h + ':' + mm;
}

function updateTimeMode() {
    if (!document.body) return;
    const hour = new Date().getHours();
    document.body.classList.remove('night-mode', 'day-mode');
    if (hour >= 6 && hour < 18) {
        document.body.classList.add('day-mode');
    } else {
        document.body.classList.add('night-mode');
    }
}

function setSensitivity(level, evt) {
    sensitivity = level;
    localStorage.setItem('volumeSensitivity', level);
    const buttons = document.querySelectorAll('#sensitivityControl button');
    buttons.forEach(btn => {
        btn.classList.remove('active');
    });
    if (evt && evt.target) {
        evt.target.classList.add('active');
    } else {
        buttons.forEach(btn => {
            if ((level === 'low' && btn.textContent === '둔감') ||
                (level === 'normal' && btn.textContent === '보통') ||
                (level === 'high' && btn.textContent === '민감')) {
                btn.classList.add('active');
            }
        });
    }
}

function getSensitivityMultiplier() {
    if (sensitivity === 'low') return 0.5;
    if (sensitivity === 'high') return 2.0;
    return 1.0;
}

function addEventLog(type, message) {
    const log = document.getElementById('eventLog');
    if (!log) return;
    const event = document.createElement('div');
    event.className = `event ${type}`;
    const time = new Date().toLocaleTimeString();
    event.textContent = `[${time}] ${message}`;
    log.insertBefore(event, log.firstChild);
    while (log.children.length > 10) {
        log.removeChild(log.lastChild);
    }
    log.scrollTop = 0;
}

function createSpark(x, y) {
    if (!effectsEnabled) return;
    const container = document.getElementById('sparkContainer');
    if (!container) return;
    
    const scrollX = window.scrollX || window.pageXOffset;
    const scrollY = window.scrollY || window.pageYOffset;
    const rect = container.getBoundingClientRect();
    const sparkX = x - rect.left - scrollX;
    const sparkY = y - rect.top - scrollY;
    
    for (let i = 0; i < 8; i++) {
        const spark = document.createElement('div');
        spark.className = 'spark';
        const angle = (Math.PI * 2 * i) / 8;
        const distance = 20 + Math.random() * 30;
        spark.style.left = sparkX + 'px';
        spark.style.top = sparkY + 'px';
        spark.style.setProperty('--spark-x', Math.cos(angle) * distance + 'px');
        spark.style.setProperty('--spark-y', Math.sin(angle) * distance + 'px');
        container.appendChild(spark);
        activeSparks.push(spark);
        const timer = setTimeout(() => {
            if (spark.parentNode) {
                spark.remove();
            }
            const index = activeSparks.indexOf(spark);
            if (index > -1) {
                activeSparks.splice(index, 1);
            }
            const timerIndex = sparkTimers.indexOf(timer);
            if (timerIndex > -1) {
                sparkTimers.splice(timerIndex, 1);
            }
        }, 600);
        sparkTimers.push(timer);
    }
}

function getVolumeColor(vol, minVol, maxVol) {
    if (!effectsEnabled || maxVol === minVol) return "#ffb300";
    const ratio = (vol - minVol) / (maxVol - minVol);
    const sensMult = getSensitivityMultiplier();
    const adjRatio = Math.min(1, ratio * sensMult);
    if (adjRatio < 0.33) return "#cc9000";
    if (adjRatio < 0.66) return "#ffb300";
    if (adjRatio < 0.85) return "#ff8800";
    return "#ff4444";
}

function getVolumeWidth(vol, minVol, maxVol) {
    if (!effectsEnabled || maxVol === minVol) return 2;
    const ratio = (vol - minVol) / (maxVol - minVol);
    const sensMult = getSensitivityMultiplier();
    const adjRatio = Math.min(1, ratio * sensMult);
    return 1 + (adjRatio * 3);
}

function toggleEffects() {
    effectsEnabled = !effectsEnabled;
    localStorage.setItem('volumeEffects', effectsEnabled);
    const btn = document.getElementById('toggleEffects');
    if (btn) {
        btn.textContent = effectsEnabled ? '효과 ON' : '효과 OFF';
        btn.classList.toggle('active', effectsEnabled);
    }
    if (!effectsEnabled) {
        chartData.datasets[0].borderColor = "#ffb300";
        chartData.datasets[0].borderWidth = 2;
        const chartWrap = document.getElementById('chart-wrap');
        if (chartWrap) chartWrap.classList.remove('shake', 'stopped');
    }
}

function updateWSStatus(status) {
    const statusEl = document.getElementById('wsStatus');
    if (!statusEl) return;
    statusEl.className = status;
    if (status === 'connected') statusEl.textContent = '연결됨';
    else if (status === 'disconnected') statusEl.textContent = '끊김';
    else if (status === 'reconnecting') {
        statusEl.textContent = '재연결 중...';
        const header = document.querySelector('header');
        if (header) header.classList.add('reconnecting');
        addEventLog('reconnect', '재연결 시도 중...');
    }
    if (status === 'connected') {
        const header = document.querySelector('header');
        if (header) header.classList.remove('reconnecting');
    }
}

const ctx = document.getElementById("chartCanvas").getContext("2d");

const bgGradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.height || 700);
bgGradient.addColorStop(0, 'rgba(255, 179, 0, 0.12)');
bgGradient.addColorStop(0.5, 'rgba(255, 179, 0, 0.04)');
bgGradient.addColorStop(1, 'rgba(255, 179, 0, 0)');

const chartData = {
    labels: [],
    datasets: [{
        label: MARKET + " 거래량",
        data: [],
        borderColor: "#ffb300",
        backgroundColor: bgGradient,
        borderWidth: 2,
        pointRadius: 0,
        tension: 0.1,
        fill: true,
        maxIndex: -1
    }]
};

const chart = new Chart(ctx, {
    type: "line",
    data: chartData,
    options: {
        animation:false,
        responsive:true,
        maintainAspectRatio:false,
        scales:{
            x:{ ticks:{color:"#888"}, grid:{color:"rgba(255,255,255,0.07)"} },
            y:{ ticks:{color:"#ccc"}, grid:{color:"rgba(255,255,255,0.07)"} }
        },
        plugins:{
            legend:{ labels:{color:"#ddd"} },
            tooltip:{
                backgroundColor: 'rgba(0, 0, 0, 0.9)',
                titleColor: '#ffb300',
                bodyColor: '#fff',
                borderColor: '#ffb300',
                borderWidth: 1,
                padding: 10,
                displayColors: false,
                callbacks:{
                    label: c => c.raw.toLocaleString() + " 수량"
                }
            }
        },
        elements: {
            point: {
                radius: function(context) {
                    if (!effectsEnabled) return 0;
                    const index = context.dataIndex;
                    const dataset = context.dataset;
                    if (dataset.maxIndex === index) return 5;
                    return 0;
                }
            },
            line: {
                borderWidth: function(context) {
                    if (!effectsEnabled) return 2;
                    const dataset = context.dataset;
                    const data = dataset.data;
                    if (!data || data.length === 0) return 2;
                    const min = Math.min(...data);
                    const max = Math.max(...data);
                    if (min === max) return 2;
                    const val = data[context.dataIndex];
                    if (val === undefined) return 2;
                    return getVolumeWidth(val, min, max);
                },
                borderColor: function(context) {
                    if (!effectsEnabled) return "#ffb300";
                    const dataset = context.dataset;
                    const data = dataset.data;
                    if (!data || data.length === 0) return "#ffb300";
                    const min = Math.min(...data);
                    const max = Math.max(...data);
                    if (min === max) return "#ffb300";
                    const val = data[context.dataIndex];
                    if (val === undefined) return "#ffb300";
                    return getVolumeColor(val, min, max);
                }
            }
        },
        onHover: (event, activeElements) => {
            const highlight = document.getElementById('chartHoverHighlight');
            if (!highlight) return;
            if (activeElements && activeElements.length > 0) {
                highlight.classList.add('active');
            } else {
                highlight.classList.remove('active');
            }
        }
    }
});

function checkDataStop() {
    const elapsed = Date.now() - lastDataTime;
    const volBox = document.getElementById("volumeBox");
    const chartWrap = document.getElementById("chart-wrap");
    if (!volBox) return;
    
    if (elapsed > STOP_THRESHOLD) {
        volBox.classList.add("stopped");
        if (chartWrap) chartWrap.classList.add("stopped");
        const currentText = volBox.textContent || '';
        if (currentText.indexOf("정지") === -1 && currentText.indexOf("수량") !== -1) {
            volBox.textContent = currentText.replace(" 수량", " 정지");
            if (!stopEventLogged) {
                addEventLog('stop', '데이터 수신 정지');
                stopEventLogged = true;
            }
        }
    } else {
        volBox.classList.remove("stopped");
        if (chartWrap) chartWrap.classList.remove("stopped");
        const currentText = volBox.textContent || '';
        if (currentText.indexOf("정지") !== -1) {
            volBox.textContent = currentText.replace(" 정지", " 수량");
            stopEventLogged = false;
        }
    }
}

function updateTensionGauge() {
    if (!effectsEnabled) return;
    const now = Date.now();
    if (now - lastTensionUpdate < TENSION_UPDATE_INTERVAL) return;
    lastTensionUpdate = now;
    
    const dataArr = chartData.datasets[0].data;
    if (dataArr.length < 10) return;
    const recent = dataArr.slice(-10);
    let variance = 0;
    const avg = recent.reduce((a, b) => a + b, 0) / recent.length;
    recent.forEach(v => {
        variance += Math.pow(v - avg, 2);
    });
    variance = variance / recent.length;
    const maxVar = Math.pow(avg, 2) * 0.1;
    const tension = Math.min(100, Math.max(0, (variance / maxVar) * 100));
    const gaugeBar = document.getElementById('tensionGaugeBar');
    if (gaugeBar) gaugeBar.style.width = tension + '%';
}

function processVolumeData(vol) {
    if (!effectsEnabled) return;
    
    const dataArr = chartData.datasets[0].data;
    const len = dataArr.length;
    
    if (len === 0) return;
    
    const prevVol = dataArr[len - 1];
    const speedEl = document.getElementById("volumeSpeed");
    
    if (speedEl && prevVol > 0 && typeof vol === 'number' && vol > 0) {
        const change = ((vol - prevVol) / prevVol) * 100;
        if (change > 0.1) {
            speedEl.textContent = `▲${change.toFixed(1)}%`;
            speedEl.className = "up";
            consecutiveUp++;
            consecutiveDown = 0;
        } else if (change < -0.1) {
            speedEl.textContent = `▼${Math.abs(change).toFixed(1)}%`;
            speedEl.className = "down";
            consecutiveDown++;
            consecutiveUp = 0;
        } else {
            speedEl.textContent = "";
            speedEl.className = "neutral";
            consecutiveUp = 0;
            consecutiveDown = 0;
        }
    }
    
    if (prevVol > 0 && typeof vol === 'number' && vol > 0 && vol / prevVol > SPARK_THRESHOLD) {
        const chartCanvas = document.getElementById("chartCanvas");
        const chartWrap = document.getElementById("chart-wrap");
        if (chartCanvas && chartWrap) {
            const canvasRect = chartCanvas.getBoundingClientRect();
            const wrapRect = chartWrap.getBoundingClientRect();
            const x = wrapRect.left + wrapRect.width * 0.9;
            const y = wrapRect.top + wrapRect.height * 0.3;
            createSpark(x, y);
            addEventLog('surge', '급등 감지');
        }
    }
    
    if (len >= ALERT_AVG_POINTS) {
        const recent = dataArr.slice(-ALERT_AVG_POINTS);
        const avg = recent.reduce((a, b) => a + b, 0) / ALERT_AVG_POINTS;
        if (typeof vol === 'number' && vol > avg * ALERT_MULTIPLIER) {
            const overlay = document.getElementById("chartAlertOverlay");
            if (overlay) {
                overlay.classList.add("active");
                const volBox = document.getElementById("volumeBox");
                if (volBox) volBox.classList.add("alert");
                if (alertOverlayTimer) clearTimeout(alertOverlayTimer);
                alertOverlayTimer = setTimeout(() => {
                    if (overlay) overlay.classList.remove("active");
                    if (volBox) volBox.classList.remove("alert");
                }, 2000);
            }
            const chartWrap = document.getElementById("chart-wrap");
            if (chartWrap && effectsEnabled) {
                chartWrap.classList.add("shake");
                setTimeout(() => chartWrap.classList.remove("shake"), 300);
            }
        }
    }
    
    if (len >= DENSE_POINTS) {
        const recent = dataArr.slice(-DENSE_POINTS);
        const sorted = [...recent].sort((a, b) => b - a);
        const threshold = sorted[Math.floor(DENSE_POINTS * 0.2)];
        let denseCount = 0;
        for (let i = len - DENSE_POINTS; i < len; i++) {
            if (dataArr[i] >= threshold) denseCount++;
        }
        const denseBand = document.getElementById("chartDenseBand");
        if (denseBand) {
            if (denseCount >= DENSE_POINTS * 0.15) {
                denseBand.classList.add("active");
            } else {
                denseBand.classList.remove("active");
            }
        }
        
        const heatZone = document.getElementById("chartHeatZone");
        if (heatZone) {
            const topValues = sorted.slice(0, Math.floor(DENSE_POINTS * 0.3));
            const avgTop = topValues.reduce((a, b) => a + b, 0) / topValues.length;
            if (typeof vol === 'number' && vol >= avgTop * 0.8) {
                heatZone.classList.add("active");
            } else {
                heatZone.classList.remove("active");
            }
        }
    }
    
    if (len > 0) {
        let maxVal = dataArr[0];
        let maxIdx = 0;
        for (let i = 1; i < len; i++) {
            if (dataArr[i] > maxVal) {
                maxVal = dataArr[i];
                maxIdx = i;
            }
        }
        maxVolIndex = maxIdx;
        chartData.datasets[0].maxIndex = maxIdx;
    }
    
    updateTensionGauge();
    
    const minVol = Math.min(...dataArr);
    const maxVol = Math.max(...dataArr);
    let currentColor = getVolumeColor(vol, minVol, maxVol);
    
    if (consecutiveUp >= 3) {
        const intensity = Math.min(consecutiveUp / 10, 0.3);
        const g = Math.min(255, Math.floor(179 + intensity * 76));
        currentColor = `rgba(255, ${g}, 0, 1)`;
    }
    if (consecutiveDown >= 3) {
        const intensity = Math.min(consecutiveDown / 10, 0.5);
        const g = Math.max(100, Math.floor(144 - intensity * 44));
        const a = Math.max(0.5, 1 - intensity);
        currentColor = `rgba(204, ${g}, 0, ${a})`;
    }
    
    chartData.datasets[0].borderColor = currentColor;
}

function connectWS() {
    if (ws && ws.readyState === WebSocket.OPEN) return;
    
    if (ws && ws.readyState !== WebSocket.CLOSED) {
        try { ws.close(); } catch(e) {}
    }
    
    updateWSStatus('reconnecting');
    ws = new WebSocket("wss://api.upbit.com/websocket/v1");

    ws.onopen = () => {
        updateWSStatus('connected');
        if (reconnectTimer) {
            clearTimeout(reconnectTimer);
            reconnectTimer = null;
        }
        ws.send(JSON.stringify([
            { ticket:"volume" },
            { type:"ticker", codes:[MARKET] }
        ]));
    };

    ws.onclose = () => {
        updateWSStatus('disconnected');
        if (reconnectTimer) clearTimeout(reconnectTimer);
        reconnectTimer = setTimeout(() => connectWS(), 3000);
    };

    ws.onerror = () => {
        updateWSStatus('disconnected');
        if (reconnectTimer) clearTimeout(reconnectTimer);
        reconnectTimer = setTimeout(() => connectWS(), 3000);
    };

    ws.onmessage = e => {
        const reader = new FileReader();
        reader.onload = () => {
            try {
                const d = JSON.parse(reader.result);
                if (!d || typeof d.acc_trade_volume_24h === 'undefined') return;
                
                const vol  = d.acc_trade_volume_24h;
                if (typeof vol !== 'number' || vol < 0) return;
                
                const time = getLabel(UNIT_MIN);
                const now = Date.now();

                lastDataTime = now;
                dataUpdateTimes.push(now);
                if (dataUpdateTimes.length > 10) dataUpdateTimes.shift();

                if (dataStopTimer) clearTimeout(dataStopTimer);
                dataStopTimer = setTimeout(checkDataStop, STOP_THRESHOLD);

                const volBox = document.getElementById("volumeBox");
                if (volBox) {
                    volBox.textContent = vol.toLocaleString() + " 수량";
                    volBox.classList.remove("stopped");
                }

                const dataArr = chartData.datasets[0].data;
                if (dataArr.length > 0) {
                    const prevVol = dataArr[dataArr.length - 1];
                    if (vol > prevVol) {
                        if (volBox) {
                            volBox.classList.remove("dim", "stopped");
                            volBox.classList.add("flash");
                        }
                        if (flashTimer) clearTimeout(flashTimer);
                        flashTimer = setTimeout(() => {
                            if (volBox) volBox.classList.remove("flash");
                        }, 300);
                        const minVol = Math.min(...dataArr);
                        const maxVol = Math.max(...dataArr);
                        if (minVol !== maxVol) {
                            chartData.datasets[0].borderColor = getVolumeColor(vol, minVol, maxVol);
                        }
                    } else if (vol < prevVol) {
                        if (volBox) {
                            volBox.classList.remove("flash", "stopped");
                            volBox.classList.add("dim");
                        }
                        if (flashTimer) clearTimeout(flashTimer);
                        const minVol = Math.min(...dataArr);
                        const maxVol = Math.max(...dataArr);
                        if (minVol !== maxVol) {
                            chartData.datasets[0].borderColor = getVolumeColor(vol, minVol, maxVol);
                        }
                    } else {
                        if (volBox) {
                            volBox.classList.remove("flash", "dim", "stopped");
                        }
                        if (flashTimer) clearTimeout(flashTimer);
                        const minVol = Math.min(...dataArr);
                        const maxVol = Math.max(...dataArr);
                        if (minVol !== maxVol) {
                            chartData.datasets[0].borderColor = getVolumeColor(vol, minVol, maxVol);
                        }
                    }
                }

                chartData.labels.push(time);
                chartData.datasets[0].data.push(vol);

                if (chartData.labels.length > MAX_POINTS) {
                    chartData.labels.shift();
                    chartData.datasets[0].data.shift();
                    maxVolIndex = -1;
                    chartData.datasets[0].maxIndex = -1;
                }

                processVolumeData(vol);

                chart.update("none");
            } catch(err) {
                console.error('Data parse error:', err);
            }
        };
        reader.readAsText(e.data);
    };
}

if (document.body) {
    updateTimeMode();
} else {
    document.addEventListener('DOMContentLoaded', updateTimeMode);
}
timeModeInterval = setInterval(updateTimeMode, 3600000);

const toggleBtn = document.getElementById('toggleEffects');
if (toggleBtn) {
    toggleBtn.textContent = effectsEnabled ? '효과 ON' : '효과 OFF';
    toggleBtn.classList.toggle('active', effectsEnabled);
}

document.querySelectorAll('#sensitivityControl button').forEach(btn => {
    if ((sensitivity === 'low' && btn.textContent === '둔감') ||
        (sensitivity === 'normal' && btn.textContent === '보통') ||
        (sensitivity === 'high' && btn.textContent === '민감')) {
        btn.classList.add('active');
    }
});

const guideContent = document.getElementById('analysisGuideContent');
if (guideContent) {
    guideContent.classList.toggle('collapsed', guideCollapsed);
}

connectWS();
checkDataStopInterval = setInterval(checkDataStop, 1000);

window.addEventListener('beforeunload', () => {
    if (checkDataStopInterval) clearInterval(checkDataStopInterval);
    if (timeModeInterval) clearInterval(timeModeInterval);
    if (flashTimer) clearTimeout(flashTimer);
    if (dataStopTimer) clearTimeout(dataStopTimer);
    if (reconnectTimer) clearTimeout(reconnectTimer);
    if (alertOverlayTimer) clearTimeout(alertOverlayTimer);
    sparkTimers.forEach(timer => clearTimeout(timer));
    activeSparks.forEach(spark => {
        if (spark.parentNode) spark.remove();
    });
    if (ws) {
        try { ws.close(); } catch(e) {}
    }
});
</script>

</body>
</html>