GNU/_PAGE/asset/upbit/data_assets.php
<?php
// ======================================================
// 1. PHP 로직 (JSON 데이터 반환 및 기본 환경 - 유지)
// ======================================================
ini_set('display_errors', 0);
date_default_timezone_set('Asia/Seoul');

if (isset($_GET['ajax'])) {
    header('Content-Type: application/json');
    try {
        require_once "/home/www/DB/db_upbit.php";
        require '/home/www/DB/key_upbit_trade.php';

        function base64url_encode($data){ return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
        function jwt_hs256(array $payload, string $secret){
            $header = ['typ'=>'JWT','alg'=>'HS256'];
            $h = base64url_encode(json_encode($header));
            $p = base64url_encode(json_encode($payload));
            $s = base64url_encode(hash_hmac('sha256', "$h.$p", $secret, true));
            return "$h.$p.$s";
        }
        function upbit_accounts($access, $secret){
            $payload = ['access_key'=>$access,'nonce'=>uniqid('',true)];
            $jwt = jwt_hs256($payload, $secret);
            $ch = curl_init("https://api.upbit.com/v1/accounts");
            curl_setopt_array($ch,[
                CURLOPT_RETURNTRANSFER=>true,
                CURLOPT_HTTPHEADER=>["Authorization: Bearer {$jwt}"],
                CURLOPT_TIMEOUT=>2,
            ]);
            $res = curl_exec($ch);
            curl_close($ch);
            return json_decode($res,true);
        }

        $accounts = upbit_accounts($UPBIT_ACCESS_KEY, $UPBIT_SECRET_KEY);
        if (!is_array($accounts)) throw new Exception("API_ERROR");

        $holdings = []; $krw_balance = 0.0;
        foreach ($accounts as $a) {
            $currency = $a['currency'] ?? '';
            if ($currency === 'KRW') { $krw_balance = (float)$a['balance']; continue; }
            $qty = (float)($a['balance'] ?? 0) + (float)($a['locked'] ?? 0);
            if ($qty <= 0) continue;
            $market = "KRW-{$currency}";
            $holdings[$market] = ['market' => $market, 'qty' => $qty, 'avg' => (float)($a['avg_buy_price'] ?? 0)];
        }

        $markets = array_keys($holdings);
        $price_map = [];
        if (!empty($markets)) {
            $in = implode(',', array_fill(0, count($markets), '?'));
            $sql = "SELECT market, korean_name, trade_price, collected_at FROM daemon_upbit_Ticker WHERE market IN ($in)";
            $stmt = $pdo->prepare($sql);
            $stmt->execute($markets);
            while($r = $stmt->fetch(PDO::FETCH_ASSOC)) { $price_map[$r['market']] = $r; }
        }

        $rows = []; $total_buy = 0; $total_eval = 0;
        foreach ($holdings as $m => $h) {
            if (!isset($price_map[$m])) continue;
            $db = $price_map[$m];
            $price = (float)$db['trade_price'];
            $buy = $h['qty'] * $h['avg'];
            $eval = $h['qty'] * $price;
            if ($eval < 5000) continue;

            $pl = $eval - $buy;
            $rows[] = [
                'name' => $db['korean_name'], 'market' => $m, 'price' => $price,
                'qty' => $h['qty'], 'avg' => $h['avg'], 'buy' => $buy,
                'eval' => $eval, 'pl' => $pl, 'rate' => ($buy > 0) ? ($pl / $buy) * 100 : 0,
                'time' => date('H:i:s', strtotime($db['collected_at']))
            ];
            $total_buy += $buy; $total_eval += $eval;
        }

        echo json_encode([
            'success' => true,
            'summary' => [
                'total_asset' => $total_eval + $krw_balance,
                'krw_balance' => $krw_balance,
                'total_buy' => $total_buy,
                'total_eval' => $total_eval,
                'total_pl' => $total_eval - $total_buy,
                'total_rate' => ($total_buy > 0) ? (($total_eval - $total_buy) / $total_buy) * 100 : 0,
            ],
            'list' => $rows
        ]);
    } catch (Exception $e) { echo json_encode(['success' => false]); }
    exit;
}
// 헤더 부분 포함
require_once '/home/www/GNU/_PAGE/head.php';
?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>업비트 실시간 터미널</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Noto+Sans+KR:wght@300;500&display=swap" rel="stylesheet">
    <style>
        :root {
            --bg-color: #020617;
            --card-bg: rgba(15, 23, 42, 0.6);
            --accent-blue: #38bdf8;
            --accent-purple: #818cf8;
            --text-main: #f1f5f9;
            --text-dim: #94a3b8;
            --glass-border: rgba(255, 255, 255, 0.08);
            /* 요구사항 반영: up-color는 푸른색 */
            --up-color: #38bdf8; 
            --down-color: #fb7185; 
        }

        @keyframes fadeInUp {
            from { opacity: 0; transform: translateY(20px); }
            to { opacity: 1; transform: translateY(0); }
        }

        body {
            background: var(--bg-color);
            color: var(--text-main);
            font-family: 'Noto Sans KR', sans-serif;
            margin: 0; padding: 0 0 20px 0;
            overflow-x: hidden;
        }

        #stars-container { position: fixed; top:0; left:0; width:100%; height:100%; z-index:-1; }

        .dashboard-container { 
            width: calc(100% - 400px); 
            margin: 0 200px; 
            position: relative; 
            z-index: 1;
            animation: fadeInUp 0.8s ease-out;
        }

        header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
        .logo { font-family: 'Orbitron'; font-size: 1.5rem; font-weight: 700; color: var(--accent-blue); letter-spacing: 2px; }
        .status-tag { font-size: 0.8rem; background: rgba(16, 185, 129, 0.1); color: #10b981; padding: 5px 12px; border-radius: 20px; border: 1px solid #10b981; }

        .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
        .summary-card { 
            background: var(--card-bg); border: 1px solid var(--glass-border); padding: 25px; border-radius: 16px; 
            backdrop-filter: blur(10px); transition: 0.3s;
            animation: fadeInUp 1s ease-out;
        }
        .summary-card:hover { border-color: var(--accent-blue); transform: translateY(-5px); }
        .summary-card label { display: block; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 10px; font-weight: 500; }
        .summary-card .value { font-size: 1.8rem; font-weight: 700; font-family: 'Orbitron'; }

        .control-bar { display: flex; gap: 15px; margin-bottom: 20px; }
        .search-input { 
            flex: 1; background: var(--card-bg); border: 1px solid var(--glass-border); 
            padding: 12px 20px; border-radius: 10px; color: #fff; outline: none; 
            transition: all 0.3s ease;
        }
        
        .search-input:hover {
            border-color: var(--accent-purple);
            background: rgba(15, 23, 42, 0.8);
            box-shadow: 0 0 15px rgba(129, 140, 248, 0.2);
        }
        .search-input:focus { border-color: var(--accent-blue); }

        .table-wrap { background: var(--card-bg); border: 1px solid var(--glass-border); border-radius: 16px; overflow: hidden; backdrop-filter: blur(10px); }
        table { width: 100%; border-collapse: collapse; text-align: right; }
        th { background: rgba(255,255,255,0.05); padding: 15px; font-size: 0.8rem; color: var(--text-dim); cursor: pointer; }
        th:hover { color: var(--accent-blue); }
        td { padding: 15px; border-bottom: 1px solid var(--glass-border); font-size: 0.95rem; }
        tr:last-child td { border: none; }
        tr:hover { background: rgba(56, 189, 248, 0.05); }

        .coin-name { text-align: left; font-weight: 500; }
        .coin-name span { display: block; font-size: 0.75rem; color: var(--text-dim); font-family: 'Orbitron'; }

        .up { color: var(--up-color) !important; }
        .down { color: var(--down-color) !important; }

        @keyframes flashUp { from { background: rgba(56, 189, 248, 0.2); } to { background: transparent; } }
        @keyframes flashDown { from { background: rgba(251, 113, 133, 0.2); } to { background: transparent; } }
        .flash-up { animation: flashUp 0.5s ease-out; }
        .flash-down { animation: flashDown 0.5s ease-out; }
    </style>
</head>
<body>

<div id="stars-container"></div>

<div class="dashboard-container">
    <header>
        <div class="logo"><i class="fa-solid fa-shuttle-space"></i> UPBIT TERMINAL</div>
        <div id="connection-status" class="status-tag"><i class="fa-solid fa-bolt"></i> 실시간 연결됨</div>
    </header>

    <div class="summary-grid">
        <div class="summary-card">
            <label>총 자산</label>
            <div id="total_asset" class="value">0</div>
        </div>
        <div class="summary-card">
            <label>보유 현금</label>
            <div id="krw_balance" class="value">0</div>
        </div>
        <div class="summary-card">
            <label>총 손익</label>
            <div id="total_pl" class="value">0</div>
        </div>
        <div class="summary-card">
            <label>총 수익률</label>
            <div id="total_rate" class="value">0.00%</div>
        </div>
    </div>

    <div class="control-bar">
        <input type="text" id="searchCoin" class="search-input" placeholder="자산명 또는 심볼 검색 (예: BTC, 비트)...">
    </div>

    <div class="table-wrap">
        <table id="mainTable">
            <thead>
                <tr>
                    <th style="text-align:left;" onclick="resort('name')">자산명</th>
                    <th onclick="resort('price')">현재가</th>
                    <th onclick="resort('avg')">평균 매수가</th>
                    <th onclick="resort('eval')">평가 금액</th>
                    <th onclick="resort('pl')">손익</th>
                    <th onclick="resort('rate')">수익률</th>
                    <th onclick="resort('time')">업데이트</th>
                </tr>
            </thead>
            <tbody id="coin_list">
                <!-- JS 동적 로드 -->
            </tbody>
        </table>
    </div>
</div>

<script>
let prevData = {};
let currentSort = { key: 'eval', dir: 'desc' };
let searchTerm = "";

const nf = (v, d=0) => new Intl.NumberFormat('ko-KR', { minimumFractionDigits: d, maximumFractionDigits: d }).format(v);

function createStars() {
    const container = document.getElementById('stars-container');
    for (let i = 0; i < 100; i++) {
        const star = document.createElement('div');
        star.style.position = 'absolute';
        star.style.width = Math.random() * 2 + 'px';
        star.style.height = star.style.width;
        star.style.backgroundColor = '#fff';
        star.style.left = Math.random() * 100 + '%';
        star.style.top = Math.random() * 100 + '%';
        star.style.opacity = Math.random();
        container.appendChild(star);
    }
}

async function fetchData() {
    try {
        const res = await fetch(window.location.pathname + "?ajax=1");
        const data = await res.json();
        if (!data.success) throw new Error();
        updateSummary(data.summary);
        renderTable(data.list);
        document.getElementById('connection-status').innerHTML = '<i class="fa-solid fa-bolt pulse"></i> 실시간 연결됨';
        document.getElementById('connection-status').classList.remove('off');
    } catch (e) {
        document.getElementById('connection-status').innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> 연결 끊김';
        document.getElementById('connection-status').classList.add('off');
    }
}

function updateSummary(s) {
    document.getElementById('total_asset').textContent = nf(s.total_asset);
    document.getElementById('krw_balance').textContent = nf(s.krw_balance);
    const plEl = document.getElementById('total_pl');
    plEl.textContent = (s.total_pl > 0 ? '+' : '') + nf(s.total_pl);
    plEl.className = 'value ' + (s.total_pl >= 0 ? 'up' : 'down');
    const rateEl = document.getElementById('total_rate');
    rateEl.textContent = (s.total_rate > 0 ? '+' : '') + nf(s.total_rate, 2) + '%';
    rateEl.className = 'value ' + (s.total_rate >= 0 ? 'up' : 'down');
}

function renderTable(list) {
    const tbody = document.getElementById('coin_list');
    let filtered = list.filter(item => 
        item.name.includes(searchTerm) || item.market.toLowerCase().includes(searchTerm.toLowerCase())
    );
    filtered.sort((a, b) => {
        let v1 = a[currentSort.key];
        let v2 = b[currentSort.key];
        return currentSort.dir === 'desc' ? (v2 > v1 ? 1 : -1) : (v1 > v2 ? 1 : -1);
    });
    let html = "";
    filtered.forEach(item => {
        const prevPrice = prevData[item.market] || 0;
        let flashClass = "";
        if (prevPrice > 0 && item.price > prevPrice) flashClass = "flash-up";
        else if (prevPrice > 0 && item.price < prevPrice) flashClass = "flash-down";
        
        html += `
            <tr class="${flashClass}">
                <td class="coin-name">${item.name} <span>${item.market}</span></td>
                <!-- 수정된 부분: 현재가 수치를 항상 up 클래스(푸른색)로 표시 -->
                <td class="up font-num">${nf(item.price)}</td>
                <td>${nf(item.avg)}</td>
                <td>${nf(item.eval)}</td>
                <td class="${item.pl >= 0 ? 'up' : 'down'}">${nf(item.pl)}</td>
                <td class="${item.rate >= 0 ? 'up' : 'down'}">${nf(item.rate, 2)}%</td>
                <td style="color:var(--text-dim); font-size:0.75rem;">${item.time}</td>
            </tr>
        `;
        prevData[item.market] = item.price;
    });
    tbody.innerHTML = html;
}

function resort(key) {
    if (currentSort.key === key) currentSort.dir = currentSort.dir === 'desc' ? 'asc' : 'desc';
    else { currentSort.key = key; currentSort.dir = 'desc'; }
    fetchData();
}

document.getElementById('searchCoin').addEventListener('input', (e) => {
    searchTerm = e.target.value;
});

createStars();
setInterval(fetchData, 300);
fetchData();
</script>
</body>
</html>

<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>