GNU/_PAGE/chart/upbit/candles/candles_volume_height.php
<?php
// volume_only_alarm.php — 거래량 전용 + 알람 + 로그 + ON/OFF 스위치 풀세트
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>🔥 실시간 거래량 + 알람 PRO 차트</title>

<!-- 로컬 JS -->
<script src="lightweight-charts.standalone.production.js"></script>

<style>
    body { background:#111; color:#eee; font-family:Arial; padding:20px; }
    #chart { width:100%; height:480px; border:1px solid #333; margin-top:10px; }
    #status { margin-top:8px; font-size:13px; color:#aaa; }
    #volInfo { font-size:46px; margin-bottom:10px; font-weight:bold; }

    #top-bar { display:flex; gap:10px; align-items:center; }
    select {
        padding:5px 8px; background:#222; color:#eee;
        border:1px solid #555; border-radius:4px;
    }

    .switch { position:relative; display:inline-block; width:42px; height:22px; }
    .switch input{ display:none; }
    .slider{
        position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0;
        background:#555; transition:.3s; border-radius:22px;
    }
    .slider:before{
        position:absolute; content:""; height:16px; width:16px;
        left:3px; bottom:3px; background:white; transition:.3s;
        border-radius:50%;
    }
    input:checked + .slider { background:#28a745; }
    input:checked + .slider:before { transform:translateX(20px); }

    #toast {
        position:fixed; top:20px; right:20px;
        background:rgba(0,0,0,0.75); padding:12px 16px; border-radius:6px;
        color:#fff; font-size:14px; display:none; z-index:9999;
    }

    #logPanel {
        margin-top:20px; background:#181818; padding:10px; border-radius:6px;
        max-height:200px; overflow:auto; border:1px solid #333;
    }
    .logItem { font-size:13px; padding:3px 0; border-bottom:1px solid #333; }
</style>
</head>
<body>

<h2>🔥 실시간 거래량 PRO 차트 (스파이크 알람 + 로그 + ON/OFF)</h2>

<div id="top-bar">
    <div>
        코인:
        <select id="market">
            <option value="KRW-BTC">BTC</option>
            <option value="KRW-ETH">ETH</option>
            <option value="KRW-XRP">XRP</option>
            <option value="KRW-TRUMP">TRUMP</option>
            <option value="KRW-QTUM">QTUM</option>
        </select>
    </div>

    <label class="switch">
        <input type="checkbox" id="alarmToggle" checked>
        <span class="slider"></span>
    </label>
    <span id="alarmText">🔔 알람 ON</span>
</div>

<div id="volInfo">현재 거래량: -</div>
<div id="chart"></div>
<div id="status">상태: 초기화 중…</div>

<div id="toast"></div>

<h3 style="margin-top:20px;">📘 최근 알람 로그</h3>
<div id="logPanel"></div>

<!-- 🔥 수정된 경로 -->
<audio id="alarmSound" src="/invest/coin/upbit/chart/alarm05.wav" preload="auto"></audio>

<script>
// ======================================
// 전역
// ======================================
let chart = null, volumeSeries = null, ws = null;

let volumes = [];
let lastBucket = null;
let lastPrice  = null;

const marketSel  = document.getElementById("market");
const statusEl   = document.getElementById("status");
const volInfo    = document.getElementById("volInfo");
const toastEl    = document.getElementById("toast");
const alarmSound = document.getElementById("alarmSound");
const logPanel   = document.getElementById("logPanel");

const alarmToggle = document.getElementById("alarmToggle");
const alarmText   = document.getElementById("alarmText");

let currentMarket = marketSel.value;
let tfMin = 1;

let SPIKE_THRESHOLD = 2.0;

// 🔥 자동 스크롤 여부
let autoScroll = true;

// ======================================
// 알람 스위치
// ======================================
alarmToggle.addEventListener("change",()=>{
    alarmText.textContent = alarmToggle.checked ? "🔔 알람 ON" : "🔕 알람 OFF";
});

// ======================================
// 토스트
// ======================================
function showToast(msg){
    toastEl.textContent = msg;
    toastEl.style.display = "block";
    setTimeout(()=>{ toastEl.style.display="none"; },2000);
}

// ======================================
// 로그
// ======================================
function addLog(msg){
    const div=document.createElement("div");
    div.className="logItem";
    div.textContent = new Date().toLocaleTimeString()+" - "+msg;
    logPanel.prepend(div);

    if(logPanel.childElementCount > 20)
        logPanel.removeChild(logPanel.lastChild);
}

// ======================================
// 거래량 표시
// ======================================
function updateVolInfo(v){
    // 차트에서는 음수일 수 있으나 텍스트 정보는 절대값으로 표시
    volInfo.textContent = "현재 거래량: " + Math.abs(v.value).toLocaleString();
    volInfo.style.color = v.color;
}

// ======================================
// 차트 초기화
// ======================================
function initChart(){
    if(chart) chart.remove();

    chart = LightweightCharts.createChart(document.getElementById("chart"), {
        layout:{ background:{color:"#111"}, textColor:"#DDD" },
        rightPriceScale:{ 
            visible:true, // 음수 출력을 위해 기준선 표시 허용
            borderColor: '#333'
        },
        grid:{ vertLines:{color:"#222"}, horzLines:{color:"#222"} },
        timeScale:{ borderColor:"#555", timeVisible:true, barSpacing:10 }
    });

    volumeSeries = chart.addHistogramSeries({
        priceFormat:{ type:"volume" },
        // 매수/매도 대칭 출력을 위해 scaleMargins 조정
        priceScaleId: 'right' 
    });

    chart.priceScale("right").applyOptions({
        scaleMargins:{ top:0.1, bottom:0.1 }
    });

    chart.timeScale().subscribeVisibleLogicalRangeChange(range=>{
        if(!range || volumes.length < 2) return;
        const lastIndex = volumes.length - 1;

        if(range.to < lastIndex - 2) autoScroll = false;
        else autoScroll = true;
    });
}

// ======================================
// 시간 버킷
// ======================================
function bucketKey(t){ return Math.floor(t/(tfMin*60)); }

// ======================================
// 과거 거래량 로드 (🔥 매수/매도 방향 적용)
// ======================================
async function loadHistory(){
    statusEl.textContent="상태: 과거 거래량 로딩…";
    const url = `upbit_proxy_candles.php?market=${currentMarket}&type=minutes&unit=${tfMin}&count=200`;

    try {
        const res=await fetch(url);
        const data=await res.json();

        volumes=[];
        let prevClose=null;

        data.reverse().forEach(c=>{
            const time = Math.floor(new Date(c.candle_date_time_kst.replace("T"," ")+" +09:00").getTime()/1000);
            let color="#999";
            let signedVolume = c.candle_acc_trade_volume;

            if(prevClose!==null){
                if(c.trade_price >= prevClose) {
                    color="#26a69a"; // 매수색
                } else {
                    color="#ef5350"; // 매도색
                    signedVolume = -signedVolume; // 아래로 출력
                }
            }

            prevClose = c.trade_price;
            lastPrice = c.trade_price;

            volumes.push({ time, value: signedVolume, color });
        });

        if(volumes.length>0){
            lastBucket = bucketKey(volumes[volumes.length-1].time);
            volumeSeries.setData(volumes);
            updateVolInfo(volumes[volumes.length-1]);
            chart.timeScale().scrollToRealTime();
        }
        statusEl.textContent="상태: 과거 거래량 로드 완료";
    } catch(e) {
        statusEl.textContent="상태: 로드 실패";
    }
}

// ======================================
// 실시간 WebSocket (🔥 매수 상단, 매도 하단 적용)
// ======================================
function connectWS(){
    if(ws) ws.close();

    ws = new WebSocket("wss://api.upbit.com/websocket/v1");
    ws.binaryType="blob";

    ws.onopen = ()=>{
        ws.send(JSON.stringify([
            { ticket:"vol-pro" },
            { type:"trade", codes:[currentMarket] }
        ]));
        statusEl.textContent="상태: 실시간 연결됨";
    };

    ws.onmessage = (event)=>{
        const reader=new FileReader();
        reader.onload=()=>{
            const d=JSON.parse(reader.result);

            const ts = Math.floor(d.trade_timestamp/1000);
            let vol = d.trade_volume;
            const price = d.trade_price;

            // Upbit 실시간 체결 BID(매수), ASK(매도)
            const isBuy = (d.ask_bid === 'BID');
            let color = isBuy ? "#26a69a" : "#ef5350";
            if(!isBuy) vol = -vol; // 매도는 음수 처리

            lastPrice = price;
            const bk = bucketKey(ts);
            const candleTime = bk*tfMin*60;

            let v;
            if(lastBucket===null || bk > lastBucket){
                v = { time:candleTime, value:vol, color };
                volumes.push(v);
                lastBucket=bk;
                volumeSeries.update(v);
            } else {
                v = volumes[volumes.length-1];
                v.value += vol;
                // 봉의 누적 합계가 양수면 매수색, 음수면 매도색
                v.color = (v.value >= 0) ? "#26a69a" : "#ef5350";
                volumeSeries.update(v);
            }

            updateVolInfo(v);

            if(autoScroll){
                chart.timeScale().scrollToRealTime();
            }

            if(alarmToggle.checked){
                const prev = volumes[volumes.length-2];
                if(prev){
                    // 절대값 기준으로 스파이크 배수 계산
                    const ratio = Math.abs(v.value) / Math.abs(prev.value);

                    if(ratio >= SPIKE_THRESHOLD){
                        alarmSound.play();
                        showToast("📈 거래량 스파이크! ("+ratio.toFixed(1)+"x)");
                        addLog(`스파이크 감지: ${ratio.toFixed(1)}배`);
                    }
                }
            }
        };
        reader.readAsText(event.data);
    };

    ws.onclose = ()=>{
        statusEl.textContent="상태: 끊김 → 재접속 중…";
        setTimeout(connectWS,2000);
    };
}

// ======================================
// 초기화
// ======================================
async function initAll(){
    initChart();
    await loadHistory();
    connectWS();
}

marketSel.addEventListener("change",()=>{
    currentMarket=marketSel.value;
    initAll();
});

window.addEventListener("DOMContentLoaded",()=>initAll());
</script>

</body>
</html>