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