GNU/_PAGE/chart/upbit/line/price_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;
}

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);
}

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.4);
    color: rgba(226, 232, 240, 0.5);
    padding: 8px 12px;
    border: 1px solid rgba(42, 52, 88, 0.4);
    border-radius: 8px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s ease;
}

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

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

#priceBox {
    margin-left: 16px;
    font-size: 15px;
    font-weight: bold;
    color: #4aeee3ff;
    padding: 5px 16px;
    background: var(--bg-tertiary);
    border-radius: 8px;
    border: 1px solid var(--bg-border);
    transition: all 0.3s ease;
}

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

.info-panel {
    margin-left: 8px;
    font-size: 11px;
    color: var(--text-secondary);
    padding: 4px 8px;
    background: var(--bg-tertiary);
    border-radius: 4px;
}

.control-btn {
    background: rgba(30, 39, 66, 0.4);
    color: rgba(226, 232, 240, 0.6);
    border: 1px solid rgba(42, 52, 88, 0.4);
    border-radius: 6px;
    padding: 6px 12px;
    font-size: 12px;
    cursor: pointer;
    margin-left: 8px;
    transition: all 0.2s ease;
}

.control-btn:hover {
    background: rgba(37, 43, 74, 0.6);
    border-color: rgba(59, 130, 246, 0.5);
}

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

.blink-warning {
    animation: blink 0.5s infinite;
}

#chart-wrap {
    height: 700px;
    padding: 20px;
    background: var(--bg-secondary);
}

#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);
}

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);
}

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

.manual-section {
    margin: 20px;
    background: var(--bg-card);
    border-radius: 12px;
    border: 1px solid var(--bg-border);
    box-shadow: var(--shadow-lg);
}

.manual-header {
    padding: 16px 20px;
    background: var(--bg-tertiary);
    border-bottom: 1px solid var(--bg-border);
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    user-select: none;
    transition: background 0.2s ease;
}

.manual-header:hover {
    background: var(--bg-hover);
}

.manual-header h3 {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
}

.manual-toggle {
    font-size: 20px;
    color: var(--text-secondary);
    transition: transform 0.3s ease;
}

.manual-toggle.open {
    transform: rotate(180deg);
}

.manual-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
}

.manual-content.open {
    max-height: 5000px;
}

.manual-body {
    padding: 20px;
}

.manual-item {
    margin-bottom: 24px;
    padding-bottom: 24px;
    border-bottom: 1px solid var(--bg-border);
}

.manual-item:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
}

.manual-item-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    cursor: pointer;
    padding: 8px 0;
    user-select: none;
    transition: background 0.2s ease;
    border-radius: 4px;
    margin: -8px 0 8px 0;
}

.manual-item-header:hover {
    background: rgba(59, 130, 246, 0.1);
}

.manual-item-header h4 {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    color: var(--accent-primary);
    flex: 1;
}

.manual-item-toggle {
    font-size: 14px;
    color: var(--text-secondary);
    margin-left: 12px;
    transition: transform 0.3s ease;
}

.manual-item-toggle.open {
    transform: rotate(90deg);
}

.manual-item-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
}

.manual-item-content.open {
    max-height: 2000px;
}

.manual-item p {
    margin: 8px 0;
    font-size: 13px;
    line-height: 1.6;
    color: var(--text-secondary);
}

.manual-item ul {
    margin: 8px 0;
    padding-left: 24px;
    font-size: 13px;
    line-height: 1.8;
    color: var(--text-secondary);
}

.manual-item li {
    margin: 4px 0;
}

.manual-item code {
    background: var(--bg-tertiary);
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 12px;
    color: var(--accent-primary);
    font-family: 'Courier New', monospace;
}

.manual-example {
    background: var(--bg-tertiary);
    padding: 12px;
    border-radius: 8px;
    margin: 12px 0;
    border-left: 3px solid var(--accent-primary);
}

.manual-example strong {
    color: var(--text-primary);
    display: block;
    margin-bottom: 8px;
}
</style>
</head>

<body>

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

if ($apiResponse !== false && $apiResponse !== '') {
    $decoded = json_decode($apiResponse, true);
    if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
        $data = $decoded;
    }
}

// ===== 마켓 파라미터 =====
$MARKET = $_GET['market'] ?? 'KRW-BTC';
$validMarket = 'KRW-BTC';

if (is_array($data) && count($data) > 0) {
    foreach ($data as $m) {
        if (isset($m['market']) && $m['market'] === $MARKET && strpos($m['market'], 'KRW-') === 0) {
            $validMarket = $MARKET;
            break;
        }
    }
}

// ===== 차트 시간 단위 파라미터 =====
// 0 = 실시간, 1 = 1분, 2 = 2분 ...
$chart_time = isset($_GET['time']) ? (int)$_GET['time'] : 0;
?>

<header>
<h2>Real-Time Price Line Chart</h2>
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
    <div style="display:flex; align-items:center; gap:6px; margin-right:10px;">
        <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>
    </div>

    <select id="coinSelect">
<?php
if (is_array($data) && count($data) > 0) {
    foreach ($data as $m) {
        if (!isset($m['market']) || strpos($m['market'], 'KRW-') !== 0) continue;

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

        echo "<option value=\"{$m['market']}\"{$selected}>{$symbol} : {$korean}</option>\n";
    }
}
?>
    </select>
    <div id="priceBox"><span class="loading-text">로딩 중...</span></div>
    <div id="infoPanel" class="info-panel"></div>
    <div id="wsStatus" class="info-panel">연결 중...</div>
    <button id="stateBtn" class="control-btn">상태 출력</button>
</div>
</header>

<div id="chart-wrap">
<canvas id="chartCanvas"></canvas>
</div>

<div class="manual-section">
    <div class="manual-header" id="manualHeader">
        <h3>📊 차트 분석 방법 가이드</h3>
        <span class="manual-toggle" id="manualToggle">▼</span>
    </div>
    <div class="manual-content" id="manualContent">
        <div class="manual-body">
            
            <div class="manual-item">
                <div class="manual-item-header" data-item="1">
                    <h4>1. 이동평균선(MA) 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent1">
                    <p><strong>이동평균선 교차 분석</strong></p>
                    <ul>
                        <li>골든크로스: 가격이 MA5를 상향 돌파 → 단기 상승 신호</li>
                        <li>데드크로스: 가격이 MA5를 하향 돌파 → 단기 하락 신호</li>
                        <li>MA5 > MA20 > MA60: 상승 추세</li>
                        <li>MA5 < MA20 < MA60: 하락 추세</li>
                    </ul>
                    <p><strong>이동평균 기울기 분석</strong></p>
                    <ul>
                        <li>정보 패널의 <code>MA5:↑</code> 또는 <code>MA5:↓</code> 확인</li>
                        <li><code>MA5:↑</code>: 단기 상승 추세</li>
                        <li><code>MA5:↓</code>: 단기 하락 추세</li>
                        <li>기울기 변화: 추세 전환 가능성</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 차트에서 가격 라인과 MA5 선의 위치 관계 확인<br>
                        2. 정보 패널에서 MA5 기울기 방향 확인<br>
                        3. MA5가 상승 중이고 가격이 MA5 위에 있으면 → 강세 신호<br>
                        4. MA5가 하락 중이고 가격이 MA5 아래에 있으면 → 약세 신호</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="2">
                    <h4>2. 최고가/최저가 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent2">
                    <p><strong>지지선/저항선 분석</strong></p>
                    <ul>
                        <li>최저가 포인트(빨간 점): 잠재적 지지선</li>
                        <li>최고가 포인트(녹색 점): 잠재적 저항선</li>
                        <li>가격이 최저가 근처: 지지 테스트 중</li>
                        <li>가격이 최고가 근처: 저항 테스트 중</li>
                    </ul>
                    <p><strong>가격 범위 분석</strong></p>
                    <ul>
                        <li>정보 패널의 <code>최고:XXX 최저:XXX</code> 확인</li>
                        <li>범위가 좁음: 횡보 구간</li>
                        <li>범위가 넓음: 변동성 큰 구간</li>
                        <li>현재가 위치: 범위 내 상단/중간/하단</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 차트에서 최고가/최저가 포인트 위치 확인<br>
                        2. 정보 패널에서 최고가와 최저가 수치 확인<br>
                        3. 현재가가 최저가 근처 → 반등 가능성 관찰<br>
                        4. 현재가가 최고가 근처 → 조정 가능성 관찰</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="3">
                    <h4>3. 최근 변동폭 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent3">
                    <p><strong>단기 모멘텀 분석</strong></p>
                    <ul>
                        <li><code>10s:0.25%</code>: 초단위 모멘텀</li>
                        <li><code>1m:1.50%</code>: 분단위 모멘텀</li>
                        <li>양수: 상승 모멘텀</li>
                        <li>음수: 하락 모멘텀</li>
                    </ul>
                    <p><strong>변동폭 비교 분석</strong></p>
                    <ul>
                        <li><code>10s</code>와 <code>1m</code> 비교</li>
                        <li><code>10s</code> > <code>1m</code>: 가속 상승/하락</li>
                        <li><code>10s</code> < <code>1m</code>: 모멘텀 둔화</li>
                        <li>절대값이 큼: 변동성 큼</li>
                        <li>절대값이 작음: 횡보</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 정보 패널에서 10s와 1m 변동폭 동시 확인<br>
                        2. <code>10s: +0.5%, 1m: +0.3%</code> → 가속 상승 중<br>
                        3. <code>10s: -0.2%, 1m: -0.8%</code> → 하락 모멘텀 둔화<br>
                        4. 변동폭이 지속적으로 증가 → 급등/급락 가능성</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="4">
                    <h4>4. 가격 이동 속도 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent4">
                    <p><strong>가격 변화 속도 측정</strong></p>
                    <ul>
                        <li>정보 패널의 <code>상승:123.45</code> 또는 <code>하락:123.45</code> 확인</li>
                        <li>단위: 원/초</li>
                        <li>값이 큼: 빠른 변화</li>
                        <li>값이 작음: 느린 변화</li>
                    </ul>
                    <p><strong>속도 변화 추이 분석</strong></p>
                    <ul>
                        <li>속도 증가: 가속 중</li>
                        <li>속도 감소: 둔화 중</li>
                        <li>방향 전환: 추세 전환 가능</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 정보 패널에서 가격 이동 속도 확인<br>
                        2. <code>상승:500</code> → 매우 빠른 상승 중<br>
                        3. <code>하락:50</code> → 느린 하락 중<br>
                        4. 속도가 지속적으로 증가 → 급등/급락 가능성</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="5">
                    <h4>5. 틱 수(거래량) 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent5">
                    <p><strong>거래 활동도 측정</strong></p>
                    <ul>
                        <li>정보 패널의 <code>틱:5/s</code> 확인 (실시간 모드만)</li>
                        <li>초당 체결 횟수</li>
                        <li>값이 큼: 활발한 거래</li>
                        <li>값이 작음: 거래 부진</li>
                    </ul>
                    <p><strong>거래량과 가격 관계</strong></p>
                    <ul>
                        <li>틱 수 증가 + 가격 상승: 매수 압력</li>
                        <li>틱 수 증가 + 가격 하락: 매도 압력</li>
                        <li>틱 수 감소: 관망</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 실시간 모드에서 틱 수 확인<br>
                        2. <code>틱:10/s + 가격 상승</code> → 강한 매수세<br>
                        3. <code>틱:2/s + 가격 하락</code> → 약한 매도세<br>
                        4. 틱 수가 급격히 증가 → 큰 움직임 전조</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="6">
                    <h4>6. 연속 상승/하락 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent6">
                    <p><strong>추세 강도 측정</strong></p>
                    <ul>
                        <li>정보 패널의 <code>연속↑:5</code> 또는 <code>연속↓:3</code> 확인</li>
                        <li>연속 틱 수</li>
                        <li>값이 큼: 강한 추세</li>
                        <li>값이 작음: 약한 추세</li>
                    </ul>
                    <p><strong>추세 지속성 분석</strong></p>
                    <ul>
                        <li>연속 상승/하락이 계속 증가: 추세 강화</li>
                        <li>연속 상승/하락이 감소: 추세 약화</li>
                        <li>0으로 리셋: 추세 전환</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 정보 패널에서 연속 상승/하락 카운트 확인<br>
                        2. <code>연속↑:10</code> → 강한 상승 추세<br>
                        3. <code>연속↓:2</code> → 약한 하락 추세<br>
                        4. 연속 카운트가 급격히 증가 → 추세 가속</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="7">
                    <h4>7. 변동성 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent7">
                    <p><strong>시장 안정성 측정</strong></p>
                    <ul>
                        <li>정보 패널의 <code>변동성:1234</code> 확인</li>
                        <li>표준편차 기반 수치</li>
                        <li>값이 큼: 높은 변동성</li>
                        <li>값이 작음: 낮은 변동성</li>
                    </ul>
                    <p><strong>변동성 추이 분석</strong></p>
                    <ul>
                        <li>변동성 증가: 불안정</li>
                        <li>변동성 감소: 안정화</li>
                        <li>변동성 급증: 큰 움직임 전조</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 정보 패널에서 변동성 수치 확인<br>
                        2. <code>변동성:5000</code> → 매우 불안정한 시장<br>
                        3. <code>변동성:500</code> → 안정적인 시장<br>
                        4. 변동성이 지속적으로 증가 → 큰 변동 예상</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="8">
                    <h4>8. 급변 감지 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent8">
                    <p><strong>급등/급락 감지</strong></p>
                    <ul>
                        <li>가격 박스 점멸: 10초 내 ±5% 이상 변동</li>
                        <li>급변 발생 시각 확인</li>
                        <li>급변 후 반등/추락 여부 관찰</li>
                    </ul>
                    <p><strong>급변 패턴 분석</strong></p>
                    <ul>
                        <li>급등 후 조정: 일시적 상승</li>
                        <li>급등 후 지속: 강한 상승 추세</li>
                        <li>급락 후 반등: 바닥 매수</li>
                        <li>급락 후 지속: 강한 하락 추세</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. 가격 박스 점멸 시 즉시 확인<br>
                        2. 급변 발생 시 정보 패널의 변동폭 확인<br>
                        3. 급변 후 가격 이동 속도 확인<br>
                        4. 급변 후 연속 상승/하락 카운트 확인</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="9">
                    <h4>9. 비정상 틱 감지 분석법</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent9">
                    <p><strong>이상 거래 감지</strong></p>
                    <ul>
                        <li>브라우저 콘솔(F12)에서 "비정상 틱 감지" 메시지 확인</li>
                        <li>직전 가격 대비 10% 이상 변동</li>
                        <li>오류 데이터 가능성 또는 큰 거래 발생</li>
                    </ul>
                    <p><strong>비정상 틱 대응</strong></p>
                    <ul>
                        <li>콘솔에서 가격과 직전 가격 확인</li>
                        <li>차트에서 급격한 스파이크 확인</li>
                        <li>실제 변동인지 오류인지 판단</li>
                    </ul>
                    <div class="manual-example">
                        <strong>실전 활용:</strong>
                        <p>1. F12 → Console 탭 열기<br>
                        2. "비정상 틱 감지" 메시지 확인<br>
                        3. 차트에서 해당 시점의 급격한 변화 확인<br>
                        4. 정보 패널의 변동폭과 비교</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="10">
                    <h4>10. 종합 분석 전략</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent10">
                    <p><strong>다중 지표 종합 분석</strong></p>
                    <ul>
                        <li>이동평균선 + 기울기: <code>MA5:↑ + 가격이 MA5 위</code> → 강한 상승 추세</li>
                        <li>변동폭 + 속도: <code>10s: +1% + 상승:500</code> → 급등 중</li>
                        <li>연속 카운트 + 틱 수: <code>연속↑:10 + 틱:15/s</code> → 강한 매수세</li>
                        <li>변동성 + 최고가/최저가: <code>변동성:2000 + 최고가 근처</code> → 저항 테스트 중</li>
                    </ul>
                    <div class="manual-example">
                        <strong>분석 시나리오 예시:</strong>
                        <p><strong>상승 추세 확인:</strong><br>
                        ✓ MA5:↑ (상승 기울기)<br>
                        ✓ 가격이 MA5 위에 위치<br>
                        ✓ 연속↑:8 (강한 상승 추세)<br>
                        ✓ 10s: +0.8% (가속 상승)<br>
                        ✓ 틱:12/s (활발한 거래)<br>
                        → 강한 상승 추세, 지속 가능성 높음</p>
                        
                        <p><strong>하락 추세 확인:</strong><br>
                        ✓ MA5:↓ (하락 기울기)<br>
                        ✓ 가격이 MA5 아래 위치<br>
                        ✓ 연속↓:5 (하락 추세)<br>
                        ✓ 1m: -1.2% (하락 모멘텀)<br>
                        ✓ 변동성:1500 (불안정)<br>
                        → 하락 추세, 추가 하락 가능성</p>
                    </div>
                </div>
            </div>

            <div class="manual-item">
                <div class="manual-item-header" data-item="11">
                    <h4>11. 실시간 모니터링 체크리스트</h4>
                    <span class="manual-item-toggle">▶</span>
                </div>
                <div class="manual-item-content" id="itemContent11">
                    <p><strong>매 틱마다 확인할 항목</strong></p>
                    <ul>
                        <li>가격 박스 색상: 상승(녹색)/하락(빨간색)</li>
                        <li>MA5 기울기: ↑ 또는 ↓</li>
                        <li>연속 카운트: 추세 강도</li>
                        <li>변동폭: 10s와 1m 비교</li>
                        <li>틱 수: 거래 활동도</li>
                    </ul>
                    <p><strong>주기적으로 확인할 항목</strong></p>
                    <ul>
                        <li>최고가/최저가: 지지/저항 확인</li>
                        <li>변동성: 시장 안정성</li>
                        <li>이동평균선 교차: 추세 전환 신호</li>
                        <li>가격 이동 속도: 변화 속도</li>
                    </ul>
                    <p><strong>경고 발생 시 확인할 항목</strong></p>
                    <ul>
                        <li>급변 경고: 변동폭, 속도, 연속 카운트</li>
                        <li>비정상 틱: 콘솔 메시지, 차트 스파이크</li>
                        <li>데이터 정지: 연결 상태, 재연결 대기</li>
                    </ul>
                </div>
            </div>

        </div>
    </div>
</div>

<script>
function toggleManual() {
    const content = document.getElementById('manualContent');
    const toggle = document.getElementById('manualToggle');
    
    if (!content || !toggle) {
        console.error("Manual elements not found");
        return;
    }
    
    if (content.classList.contains('open')) {
        content.classList.remove('open');
        toggle.classList.remove('open');
        toggle.textContent = '▼';
    } else {
        content.classList.add('open');
        toggle.classList.add('open');
        toggle.textContent = '▲';
    }
}

function toggleItem(itemNum) {
    const content = document.getElementById('itemContent' + itemNum);
    const header = document.querySelector(`[data-item="${itemNum}"]`);
    const toggle = header ? header.querySelector('.manual-item-toggle') : null;
    
    if (!content || !toggle) {
        console.error("Item elements not found for item:", itemNum);
        return;
    }
    
    if (content.classList.contains('open')) {
        content.classList.remove('open');
        toggle.classList.remove('open');
        toggle.textContent = '▶';
    } else {
        content.classList.add('open');
        toggle.classList.add('open');
        toggle.textContent = '▼';
    }
}

// DOM 로드 후 이벤트 리스너 등록
document.addEventListener('DOMContentLoaded', function() {
    const manualHeader = document.getElementById('manualHeader');
    if (manualHeader) {
        manualHeader.addEventListener('click', toggleManual);
    }
    
    // 각 항목 헤더에 클릭 이벤트 추가
    const itemHeaders = document.querySelectorAll('.manual-item-header');
    itemHeaders.forEach(header => {
        header.addEventListener('click', function() {
            const itemNum = this.getAttribute('data-item');
            if (itemNum) {
                toggleItem(itemNum);
            }
        });
    });
});

let MARKET = "<?= $validMarket ?>";
let UNIT_MIN = <?= $chart_time ?>; // 0=실시간, n=분단위
let ws = null;
let MAX_POINTS = 300;
let lastPrice = null;
let lastTime = null;
let priceHistory = [];
let tickTimestamps = [];
let consecutiveUp = 0;
let consecutiveDown = 0;
let lastDataTime = Date.now();
let wsConnected = false;
let noDataInterval = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;

// 최고가/최저가
let maxPrice = null;
let minPrice = null;
let maxPriceIndex = null;
let minPriceIndex = null;

// 이동평균 데이터
let ma5Data = [];
let ma20Data = [];
let ma60Data = [];

// 화면 크기 기반 MAX_POINTS 조절
function adjustMaxPoints() {
    const width = window.innerWidth;
    if (width < 768) {
        MAX_POINTS = 150;
    } else if (width < 1024) {
        MAX_POINTS = 200;
    } else if (width < 1400) {
        MAX_POINTS = 250;
    } else {
        MAX_POINTS = 300;
    }
}

adjustMaxPoints();
window.addEventListener('resize', adjustMaxPoints);

function getLabel(unitMin) {
    const d = new Date();

    if (unitMin === 0) {
        return d.toTimeString().split(" ")[0]; // HH:MM:SS
    }

    const h  = String(d.getHours()).padStart(2, '0');
    const mm = String(Math.floor(d.getMinutes() / unitMin) * unitMin).padStart(2, '0');
    return h + ':' + mm;
}

const canvasEl = document.getElementById("chartCanvas");
if (!canvasEl) {
    console.error("chartCanvas element not found");
} else {
    const ctx = canvasEl.getContext("2d");

    const chartData = {
        labels: [],
        datasets: [{
            label: MARKET + " 체결가",
            data: [],
            borderColor: "#00c8ff",
            borderWidth: 2,
            pointRadius: 0,
            tension: 0.1
        }, {
            label: "MA5",
            data: [],
            borderColor: "rgba(255,255,255,0.5)",
            borderWidth: 1,
            pointRadius: 0,
            tension: 0.1
        }, {
            label: "MA20",
            data: [],
            borderColor: "rgba(255,255,255,0.4)",
            borderWidth: 1,
            pointRadius: 0,
            tension: 0.1
        }, {
            label: "MA60",
            data: [],
            borderColor: "rgba(255,255,255,0.3)",
            borderWidth: 1,
            pointRadius: 0,
            tension: 0.1
        }, {
            label: "최고가",
            data: [],
            borderColor: "#10b981",
            borderWidth: 0,
            pointRadius: 4,
            pointBackgroundColor: "#10b981",
            pointBorderColor: "#ffffff",
            pointBorderWidth: 2,
            showLine: false
        }, {
            label: "최저가",
            data: [],
            borderColor: "#ef4444",
            borderWidth: 0,
            pointRadius: 4,
            pointBackgroundColor: "#ef4444",
            pointBorderColor: "#ffffff",
            pointBorderWidth: 2,
            showLine: false
        }]
    };

    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" }, display: false },
                tooltip: {
                    callbacks: {
                        label: function(context) {
                            if (context.datasetIndex === 4) {
                                return "최고가: " + context.raw.toLocaleString() + " 원";
                            } else if (context.datasetIndex === 5) {
                                return "최저가: " + context.raw.toLocaleString() + " 원";
                            } else {
                                return context.raw.toLocaleString() + " 원";
                            }
                        }
                    }
                }
            }
        }
    });

    // 이동평균 계산
    function calculateMA(period) {
        const data = chartData.datasets[0].data;
        if (data.length < period) return null;
        const recent = data.slice(-period).filter(v => v !== null && typeof v === 'number' && !isNaN(v));
        if (recent.length < period) return null;
        const sum = recent.reduce((a, b) => a + b, 0);
        return sum / recent.length;
    }

    // 이동평균 기울기 계산
    function calculateMAGradient(maData) {
        if (maData.length < 2) return 0;
        const recent = maData.slice(-2);
        if (recent[0] === null || recent[1] === null) return 0;
        return recent[1] - recent[0];
    }

    // 표준편차 계산
    function calculateStdDev() {
        const data = chartData.datasets[0].data.filter(v => v !== null && typeof v === 'number' && !isNaN(v));
        if (data.length < 2) return 0;
        const mean = data.reduce((a, b) => a + b, 0) / data.length;
        const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
        return Math.sqrt(variance);
    }

    // 최근 변동폭 계산
    function calculateRecentChange(seconds) {
        const now = Date.now();
        const filtered = priceHistory.filter(p => now - p.ts <= seconds * 1000);
        if (filtered.length < 2) return null;
        const oldest = filtered[0].price;
        const newest = filtered[filtered.length - 1].price;
        if (oldest === 0) return null;
        return ((newest - oldest) / oldest) * 100;
    }

    // 가격 이동 속도 계산
    function calculatePriceSpeed(currentPrice, currentTime) {
        if (lastPrice === null || lastTime === null) return { speed: 0, direction: "" };
        const timeDiff = (currentTime - lastTime) / 1000;
        if (timeDiff <= 0) return { speed: 0, direction: "" };
        const priceDiff = Math.abs(currentPrice - lastPrice);
        const speed = priceDiff / timeDiff;
        const direction = currentPrice > lastPrice ? "상승" : "하락";
        return { speed, direction };
    }

    // 틱 수 카운트 (초당)
    function countTicksPerSecond() {
        const now = Date.now();
        tickTimestamps = tickTimestamps.filter(ts => now - ts <= 1000);
        return tickTimestamps.length;
    }

    // 스프레드 계산
    function calculateSpread(bidPrice, askPrice) {
        if (!bidPrice || !askPrice || bidPrice <= 0 || askPrice <= 0) return null;
        const spread = askPrice - bidPrice;
        const spreadPercent = (spread / bidPrice) * 100;
        return { spread, spreadPercent };
    }

    // 급변 감지
    function detectSuddenChange(currentPrice, changeRate) {
        const change10s = calculateRecentChange(10);
        if (change10s === null) return false;
        return Math.abs(change10s) > 5;
    }

    // 비정상 틱 감지
    function detectAbnormalTick(currentPrice) {
        if (lastPrice === null) return false;
        if (lastPrice === 0) return false;
        const changePercent = Math.abs((currentPrice - lastPrice) / lastPrice) * 100;
        return changePercent > 10;
    }

    // 상태 객체 업데이트
    function updateState(price, rate, speed, volatility, tickCount) {
        window.STATE = {
            price: price,
            rate: rate,
            speed: speed,
            volatility: volatility,
            tickCount: tickCount,
            timestamp: Date.now()
        };
    }

    // 최고가/최저가 차트 업데이트 (최적화)
    function updateHighLowPoints() {
        const dataLength = chartData.labels.length;
        if (dataLength === 0) return;
        
        const priceData = chartData.datasets[0].data;
        if (priceData.length === 0) return;
        
        // 기존 최고가/최저가 데이터 초기화
        chartData.datasets[4].data = new Array(dataLength).fill(null);
        chartData.datasets[5].data = new Array(dataLength).fill(null);
        
        let currentMax = null;
        let currentMaxIdx = null;
        let currentMin = null;
        let currentMinIdx = null;
        
        priceData.forEach((price, idx) => {
            if (price !== null && typeof price === 'number' && !isNaN(price)) {
                if (currentMax === null || price > currentMax) {
                    currentMax = price;
                    currentMaxIdx = idx;
                }
                if (currentMin === null || price < currentMin) {
                    currentMin = price;
                    currentMinIdx = idx;
                }
            }
        });
        
        if (currentMaxIdx !== null && currentMaxIdx < dataLength) {
            chartData.datasets[4].data[currentMaxIdx] = currentMax;
            maxPrice = currentMax;
            maxPriceIndex = currentMaxIdx;
        }
        if (currentMinIdx !== null && currentMinIdx < dataLength) {
            chartData.datasets[5].data[currentMinIdx] = currentMin;
            minPrice = currentMin;
            minPriceIndex = currentMinIdx;
        }
    }

    // 정보 패널 업데이트
    function updateInfoPanel(data) {
        if (!data || typeof data !== 'object') return;
        
        const price = data.trade_price;
        if (typeof price !== 'number' || isNaN(price)) return;
        
        const changeRate = (typeof data.signed_change_rate === 'number') ? data.signed_change_rate : 0;
        const bidPrice = (typeof data.bid_price === 'number') ? data.bid_price : null;
        const askPrice = (typeof data.ask_price === 'number') ? data.ask_price : null;
        
        // 변동폭 계산
        const change10s = calculateRecentChange(10);
        const change1m = calculateRecentChange(60);
        
        // 틱 수
        const ticksPerSec = UNIT_MIN === 0 ? countTicksPerSecond() : 0;
        
        // 가격 이동 속도
        const speedInfo = calculatePriceSpeed(price, Date.now());
        
        // 스프레드
        const spreadInfo = calculateSpread(bidPrice, askPrice);
        
        // 이동평균 기울기
        const ma5Gradient = calculateMAGradient(ma5Data);
        const ma20Gradient = calculateMAGradient(ma20Data);
        const ma60Gradient = calculateMAGradient(ma60Data);
        
        // 표준편차
        const stdDev = calculateStdDev();
        
        // 마지막 체결 시각
        const lastTradeTime = new Date().toTimeString().split(" ")[0];
        
        // 정보 텍스트 구성
        let infoText = [];
        if (maxPrice !== null && minPrice !== null) {
            infoText.push(`최고:${maxPrice.toLocaleString()} 최저:${minPrice.toLocaleString()}`);
        }
        if (change10s !== null) {
            infoText.push(`10s:${change10s.toFixed(2)}%`);
        }
        if (change1m !== null) {
            infoText.push(`1m:${change1m.toFixed(2)}%`);
        }
        if (ticksPerSec > 0) {
            infoText.push(`틱:${ticksPerSec}/s`);
        }
        if (speedInfo.speed > 0) {
            infoText.push(`${speedInfo.direction}:${speedInfo.speed.toFixed(2)}`);
        }
        if (spreadInfo) {
            infoText.push(`스프레드:${spreadInfo.spreadPercent.toFixed(3)}%`);
        }
        infoText.push(`시각:${lastTradeTime}`);
        if (consecutiveUp > 0) {
            infoText.push(`연속↑:${consecutiveUp}`);
        }
        if (consecutiveDown > 0) {
            infoText.push(`연속↓:${consecutiveDown}`);
        }
        if (ma5Gradient !== 0) {
            infoText.push(`MA5:${ma5Gradient > 0 ? "↑" : "↓"}`);
        }
        if (stdDev > 0) {
            infoText.push(`변동성:${stdDev.toFixed(0)}`);
        }
        
        const infoPanel = document.getElementById("infoPanel");
        if (infoPanel) {
            infoPanel.innerText = infoText.join(" | ");
        }
        
        // 상태 객체 업데이트
        updateState(price, changeRate, speedInfo.speed, stdDev, ticksPerSec);
        
        // 급변 경고
        if (detectSuddenChange(price, changeRate)) {
            const priceBox = document.getElementById("priceBox");
            if (priceBox) {
                priceBox.classList.add("blink-warning");
                setTimeout(() => {
                    if (priceBox) {
                        priceBox.classList.remove("blink-warning");
                    }
                }, 2000);
            }
        }
        
        // 비정상 틱 감지
        if (detectAbnormalTick(price)) {
            console.warn("비정상 틱 감지:", price, "직전:", lastPrice);
        }
        
        // 스프레드 급확대 경고
        const priceBox = document.getElementById("priceBox");
        if (priceBox) {
            if (spreadInfo && spreadInfo.spreadPercent > 1) {
                priceBox.style.borderColor = "#ef4444";
            } else {
                priceBox.style.borderColor = "";
            }
        }
    }

    function connectWS() {
        if (ws) {
            try {
                ws.close();
            } catch(e) {
                console.error("WebSocket close error:", e);
            }
        }

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

            ws.onopen = () => {
                wsConnected = true;
                reconnectAttempts = 0;
                const wsStatus = document.getElementById("wsStatus");
                if (wsStatus) {
                    wsStatus.innerText = "연결됨";
                    wsStatus.style.color = "#10b981";
                }
                try {
                    ws.send(JSON.stringify([
                        { ticket: "trade-price" },
                        { type: "ticker", codes: [MARKET] }
                    ]));
                } catch(e) {
                    console.error("WebSocket send error:", e);
                }
            };

            ws.onmessage = e => {
                try {
                    lastDataTime = Date.now();
                    
                    const decoder = new TextDecoder("utf-8");
                    const text = decoder.decode(e.data);
                    let d;
                    try {
                        d = JSON.parse(text);
                    } catch(parseError) {
                        console.error("JSON parse error:", parseError);
                        return;
                    }
                    
                    if (!d || typeof d !== 'object' || typeof d.trade_price !== 'number' || isNaN(d.trade_price)) {
                        return;
                    }
                    
                    const price = d.trade_price;
                    const time = getLabel(UNIT_MIN);
                    const currentTime = Date.now();

                    // 틱 타임스탬프 기록 (메모리 관리)
                    if (UNIT_MIN === 0) {
                        tickTimestamps.push(currentTime);
                        // 2초 이상 된 데이터 제거
                        tickTimestamps = tickTimestamps.filter(ts => currentTime - ts <= 2000);
                    }

                    // 가격 히스토리
                    priceHistory.push({ price, ts: currentTime });
                    if (priceHistory.length > 100) {
                        priceHistory.shift();
                    }

                    // 연속 상승/하락 카운트
                    if (lastPrice !== null) {
                        if (price > lastPrice) {
                            consecutiveUp++;
                            consecutiveDown = 0;
                        } else if (price < lastPrice) {
                            consecutiveDown++;
                            consecutiveUp = 0;
                        } else {
                            consecutiveUp = 0;
                            consecutiveDown = 0;
                        }
                    }

                    // 색상 결정
                    let color = "#00c8ff";
                    if (lastPrice !== null) {
                        if (price > lastPrice) {
                            color = "#10b981";
                        } else if (price < lastPrice) {
                            color = "#ef4444";
                        }
                    }

                    const priceBoxEl = document.getElementById("priceBox");
                    if (priceBoxEl) {
                        priceBoxEl.style.color = color;
                        priceBoxEl.innerText = price.toLocaleString() + " 원";
                    }

                    chartData.labels.push(time);
                    chartData.datasets[0].data.push(price);
                    chartData.datasets[0].borderColor = color;

                    // 이동평균 업데이트
                    const ma5 = calculateMA(5);
                    const ma20 = calculateMA(20);
                    const ma60 = calculateMA(60);
                    
                    chartData.datasets[1].data.push(ma5 !== null ? ma5 : null);
                    chartData.datasets[2].data.push(ma20 !== null ? ma20 : null);
                    chartData.datasets[3].data.push(ma60 !== null ? ma60 : null);
                    
                    // 이동평균 데이터 배열에도 push (기울기 계산용) - null도 push하여 길이 동기화
                    ma5Data.push(ma5);
                    ma20Data.push(ma20);
                    ma60Data.push(ma60);

                    if (chartData.labels.length > MAX_POINTS) {
                        chartData.labels.shift();
                        chartData.datasets[0].data.shift();
                        chartData.datasets[1].data.shift();
                        chartData.datasets[2].data.shift();
                        chartData.datasets[3].data.shift();
                        chartData.datasets[4].data.shift();
                        chartData.datasets[5].data.shift();
                        
                        // 이동평균 데이터 배열도 shift (길이 동기화)
                        if (ma5Data.length > 0) ma5Data.shift();
                        if (ma20Data.length > 0) ma20Data.shift();
                        if (ma60Data.length > 0) ma60Data.shift();
                        
                        // 최고가/최저가 인덱스 감소
                        if (maxPriceIndex !== null) {
                            maxPriceIndex--;
                            if (maxPriceIndex < 0) {
                                maxPriceIndex = null;
                                maxPrice = null;
                            }
                        }
                        if (minPriceIndex !== null) {
                            minPriceIndex--;
                            if (minPriceIndex < 0) {
                                minPriceIndex = null;
                                minPrice = null;
                            }
                        }
                    }

                    // 최고가/최저가 차트 업데이트
                    updateHighLowPoints();

                    chart.update("none");
                    lastPrice = price;
                    lastTime = currentTime;

                    // 정보 패널 업데이트
                    updateInfoPanel(d);
                } catch(error) {
                    console.error("WebSocket message processing error:", error);
                }
            };

            ws.onclose = () => {
                wsConnected = false;
                const wsStatus = document.getElementById("wsStatus");
                if (wsStatus) {
                    wsStatus.innerText = "끊김";
                    wsStatus.style.color = "#ef4444";
                }
                
                if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
                    reconnectAttempts++;
                    setTimeout(() => {
                        connectWS();
                    }, 1000);
                } else {
                    if (wsStatus) {
                        wsStatus.innerText = "재연결 실패";
                        wsStatus.style.color = "#ef4444";
                    }
                }
            };

            ws.onerror = (error) => {
                wsConnected = false;
                const wsStatus = document.getElementById("wsStatus");
                if (wsStatus) {
                    wsStatus.innerText = "오류";
                    wsStatus.style.color = "#ef4444";
                }
                console.error("WebSocket error:", error);
            };
        } catch(error) {
            console.error("WebSocket connection error:", error);
            wsConnected = false;
            const wsStatus = document.getElementById("wsStatus");
            if (wsStatus) {
                wsStatus.innerText = "연결 실패";
                wsStatus.style.color = "#ef4444";
            }
        }
    }

    // 데이터 무응답 감지
    function checkNoData() {
        const elapsed = (Date.now() - lastDataTime) / 1000;
        if (elapsed > 5 && wsConnected) {
            const priceBox = document.getElementById("priceBox");
            if (priceBox) {
                priceBox.innerText = "데이터 정지";
                priceBox.style.color = "#ef4444";
            }
        }
    }

    noDataInterval = setInterval(checkNoData, 1000);

    // 페이지 이탈 시 정리
    window.addEventListener('beforeunload', () => {
        if (noDataInterval) {
            clearInterval(noDataInterval);
        }
        if (ws) {
            try {
                ws.close();
            } catch(e) {
                // 무시
            }
        }
    });

    const coinSelect = document.getElementById("coinSelect");
    if (coinSelect) {
        coinSelect.addEventListener("change", function(){
            location.href = "?market=" + this.value + "&time=<?= $chart_time ?>";
        });
    }

    const stateBtn = document.getElementById("stateBtn");
    if (stateBtn) {
        stateBtn.addEventListener("click", function(){
            if (window.STATE) {
                console.log(JSON.stringify(window.STATE, null, 2));
                alert(JSON.stringify(window.STATE, null, 2));
            } else {
                alert("상태 데이터가 없습니다.");
            }
        });
    }

    connectWS();
}
</script>

</body>
</html>