<?php
// ── AJAX 요청 처리 ──────────────────────────────────────────
if (isset($_GET['ajax'])) {
header('Content-Type: application/json; charset=utf-8');
require_once '/home/www/DB/db_upbit.php';
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$f_side = isset($_GET['side']) ? trim($_GET['side']) : '';
$f_trigger = isset($_GET['trigger_type']) ? trim($_GET['trigger_type']) : '';
$f_ord = isset($_GET['ord_type']) ? trim($_GET['ord_type']) : '';
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$per = isset($_GET['per']) ? (int)$_GET['per'] : 20;
if (!in_array($per, [20, 50, 100])) $per = 20;
$offset = ($page - 1) * $per;
$where = [];
$params = [];
if ($search !== '') {
$where[] = '(cron_id LIKE :s OR market LIKE :s2)';
$like = '%' . $search . '%';
$params[':s'] = $like;
$params[':s2'] = $like;
}
if ($f_side) { $where[] = 'side = :side'; $params[':side'] = $f_side; }
if ($f_trigger) { $where[] = 'trigger_type = :trigger'; $params[':trigger'] = $f_trigger; }
if ($f_ord) { $where[] = 'ord_type = :ord'; $params[':ord'] = $f_ord; }
$wsql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
try {
// 전체 건수
$cstmt = $db_upbit->prepare("SELECT COUNT(*) FROM daemon_trading" . $wsql);
foreach ($params as $k => $v) $cstmt->bindValue($k, $v);
$cstmt->execute();
$total = (int)$cstmt->fetchColumn();
// 페이지 데이터
$stmt = $db_upbit->prepare("SELECT * FROM daemon_trading" . $wsql . " ORDER BY timestamp DESC LIMIT :lim OFFSET :off");
foreach ($params as $k => $v) $stmt->bindValue($k, $v);
$stmt->bindValue(':lim', $per, PDO::PARAM_INT);
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'data' => $rows,
'total' => $total,
'page' => $page,
'per' => $per,
'pages' => (int)ceil($total / $per),
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
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>TRADING MONITOR</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #020617;
--card: #111827;
--border: #1e293b;
--panel: #0c1224;
--surface: #1a2436;
--mid: #334155;
--text: #e2e8f0;
--sub: #94a3b8;
--dim: #475569;
--green: #22d3a5;
--red: #f87171;
--blue: #38bdf8;
--yellow: #fbbf24;
--accent: #6366f1;
--mono: 'Space Mono', monospace;
--sans: 'Noto Sans KR', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--bg); color: var(--text); font-family: var(--sans); min-height: 100vh; overflow-x: hidden; }
body::before {
content: ''; position: fixed; inset: 0;
background-image: linear-gradient(rgba(99,102,241,.04) 1px, transparent 1px), linear-gradient(90deg, rgba(99,102,241,.04) 1px, transparent 1px);
background-size: 40px 40px; pointer-events: none; z-index: 0;
}
body::after {
content: ''; position: fixed; top: -120px; left: 50%; transform: translateX(-50%);
width: 700px; height: 300px;
background: radial-gradient(ellipse, rgba(99,102,241,.18) 0%, transparent 70%);
pointer-events: none; z-index: 0;
}
.wrap { position: relative; z-index: 1; max-width: 100%; margin: 0 auto; padding: 0 24px 60px; }
header {
padding: 36px 0 28px;
display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap;
border-bottom: 1px solid var(--border); margin-bottom: 28px;
opacity: 0; transform: translateY(-18px);
animation: fadeDown .55s cubic-bezier(.22,1,.36,1) .1s forwards;
}
.logo { display: flex; align-items: center; gap: 14px; }
.logo-icon {
width: 42px; height: 42px; border: 1.5px solid var(--accent); border-radius: 8px;
display: grid; place-items: center; background: rgba(99,102,241,.1); box-shadow: 0 0 18px rgba(99,102,241,.25);
}
.logo-text h1 { font-family: var(--mono); font-size: 1.35rem; letter-spacing: .12em; }
.logo-text p { font-size: .72rem; color: var(--dim); letter-spacing: .08em; margin-top: 2px; }
.stat-bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; opacity: 0; animation: fadeIn .5s ease .35s forwards; }
.stat-chip {
display: flex; align-items: center; gap: 7px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 20px; padding: 5px 14px; font-family: var(--mono); font-size: .72rem; letter-spacing: .05em;
}
.dot { width: 7px; height: 7px; border-radius: 50%; }
.dot-all { background: var(--blue); }
.dot-bid { background: var(--green); box-shadow: 0 0 6px var(--green); }
.dot-ask { background: var(--red); box-shadow: 0 0 6px var(--red); }
.dot-ok { background: var(--green); }
.dot-fail { background: var(--red); }
/* 필터 */
.filter-area {
display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px;
opacity: 0; animation: fadeUp .5s cubic-bezier(.22,1,.36,1) .3s forwards;
}
.filter-group { display: flex; gap: 6px; align-items: center; }
.filter-label { font-family: var(--mono); font-size: .65rem; color: var(--dim); letter-spacing: .08em; margin-right: 2px; }
.fbtn {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
padding: 5px 13px; font-family: var(--mono); font-size: .72rem; color: var(--sub);
cursor: pointer; transition: all .15s; letter-spacing: .05em;
}
.fbtn:hover { border-color: var(--mid); color: var(--text); }
.fbtn.active { border-color: var(--accent); color: var(--accent); background: rgba(99,102,241,.1); box-shadow: 0 0 8px rgba(99,102,241,.2); }
.fbtn.active-bid { border-color: var(--green); color: var(--green); background: rgba(34,211,165,.08); }
.fbtn.active-ask { border-color: var(--red); color: var(--red); background: rgba(248,113,113,.08); }
/* 검색 */
.search-bar {
display: flex; align-items: center; gap: 10px;
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 10px 18px; margin-bottom: 20px; transition: border-color .2s;
opacity: 0; animation: fadeUp .5s cubic-bezier(.22,1,.36,1) .4s forwards;
}
.search-bar:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(99,102,241,.12); }
.search-bar svg { color: var(--dim); flex-shrink: 0; }
.search-bar input { flex: 1; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--sans); font-size: .9rem; }
.search-bar input::placeholder { color: var(--dim); }
.search-hint { font-size: .7rem; color: var(--dim); white-space: nowrap; font-family: var(--mono); }
.clear-btn { background: none; border: none; color: var(--dim); cursor: pointer; padding: 2px; display: none; transition: color .2s; }
.clear-btn:hover { color: var(--text); }
.clear-btn.show { display: flex; }
/* 실시간 / 페이지 정보 */
.live-row {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;
margin-bottom: 14px; font-size: .75rem; color: var(--sub);
opacity: 0; animation: fadeIn .4s ease .5s forwards;
}
.live-badge { display: flex; align-items: center; gap: 6px; font-family: var(--mono); }
.pulse-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 6px var(--green);
animation: pulse 1.8s ease-in-out infinite;
}
.page-info { font-family: var(--mono); font-size: .7rem; color: var(--dim); }
.last-update { font-family: var(--mono); font-size: .68rem; color: var(--dim); }
/* per-page 선택 */
.per-select {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
color: var(--sub); font-family: var(--mono); font-size: .72rem;
padding: 4px 8px; cursor: pointer; outline: none;
}
.per-select:focus { border-color: var(--accent); }
/* 테이블 */
.table-wrap {
background: var(--card); border: 1px solid var(--border); border-radius: 12px 12px 0 0; overflow: auto;
opacity: 0; animation: fadeUp .55s cubic-bezier(.22,1,.36,1) .55s forwards;
}
table { width: 100%; border-collapse: collapse; font-size: .82rem; min-width: 1400px; }
thead { background: var(--panel); position: sticky; top: 0; z-index: 10; }
thead th {
padding: 13px 14px; text-align: left;
font-family: var(--mono); font-size: .68rem; letter-spacing: .1em;
color: var(--dim); font-weight: 400; border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody tr { border-bottom: 1px solid rgba(30,41,59,.7); transition: background .15s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: rgba(51,65,85,.25); }
tbody tr.new-row { animation: rowSlide .5s cubic-bezier(.22,1,.36,1) forwards; }
td { padding: 11px 14px; vertical-align: middle; color: var(--sub); }
td.td-mono { font-family: var(--mono); font-size: .75rem; }
td.td-uuid { font-family: var(--mono); font-size: .68rem; color: var(--dim); max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.badge { display: inline-flex; align-items: center; gap: 5px; border-radius: 5px; padding: 3px 9px; font-family: var(--mono); font-size: .68rem; letter-spacing: .05em; font-weight: 700; white-space: nowrap; }
.badge-bid { background: rgba(34,211,165,.1); color: var(--green); border: 1px solid rgba(34,211,165,.25); }
.badge-ask { background: rgba(248,113,113,.1); color: var(--red); border: 1px solid rgba(248,113,113,.25); }
.badge-success { background: rgba(34,211,165,.08); color: var(--green); border: 1px solid rgba(34,211,165,.2); }
.badge-fail { background: rgba(248,113,113,.1); color: var(--red); border: 1px solid rgba(248,113,113,.25); }
.badge-wait { background: rgba(251,191,36,.08); color: var(--yellow); border: 1px solid rgba(251,191,36,.2); }
.badge-done { background: rgba(56,189,248,.08); color: var(--blue); border: 1px solid rgba(56,189,248,.2); }
.badge-cancel { background: rgba(71,85,105,.2); color: var(--mid); border: 1px solid rgba(71,85,105,.35); }
.badge-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.market-tag { background: rgba(99,102,241,.1); color: var(--accent); border: 1px solid rgba(99,102,241,.2); border-radius: 4px; padding: 2px 8px; font-family: var(--mono); font-size: .72rem; font-weight: 700; }
.step-tag { background: rgba(56,189,248,.08); color: var(--blue); border: 1px solid rgba(56,189,248,.18); border-radius: 4px; padding: 2px 8px; font-family: var(--mono); font-size: .68rem; }
.ord-tag { background: rgba(251,191,36,.07); color: var(--yellow); border: 1px solid rgba(251,191,36,.18); border-radius: 4px; padding: 2px 8px; font-family: var(--mono); font-size: .68rem; }
/* 페이지네이션 */
.pagination-wrap {
background: var(--panel); border: 1px solid var(--border); border-top: none;
border-radius: 0 0 12px 12px; padding: 14px 18px;
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;
opacity: 0; animation: fadeUp .55s cubic-bezier(.22,1,.36,1) .65s forwards;
}
.page-total { font-family: var(--mono); font-size: .7rem; color: var(--dim); }
.page-btns { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.pbtn {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
padding: 5px 10px; font-family: var(--mono); font-size: .72rem; color: var(--sub);
cursor: pointer; transition: all .15s; min-width: 34px; text-align: center;
}
.pbtn:hover:not(:disabled) { border-color: var(--mid); color: var(--text); }
.pbtn.active { border-color: var(--accent); color: var(--accent); background: rgba(99,102,241,.12); }
.pbtn:disabled { opacity: .3; cursor: not-allowed; }
.pbtn-nav { padding: 5px 12px; }
.page-ellipsis { color: var(--dim); font-family: var(--mono); font-size: .72rem; padding: 0 4px; }
.empty-state { padding: 60px 20px; text-align: center; color: var(--dim); font-size: .85rem; }
.db-error { padding: 30px; text-align: center; color: var(--red); font-family: var(--mono); font-size: .8rem; }
.spin-wrap { display: flex; align-items: center; justify-content: center; padding: 60px; gap: 10px; color: var(--dim); font-family: var(--mono); font-size: .78rem; }
.spin { width: 18px; height: 18px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
mark { background: rgba(99,102,241,.3); color: var(--text); border-radius: 2px; padding: 0 2px; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.7)} }
@keyframes fadeDown { to{opacity:1;transform:translateY(0)} }
@keyframes fadeUp { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
@keyframes fadeIn { to{opacity:1} }
@keyframes rowSlide { from{opacity:0;background:rgba(99,102,241,.12);transform:translateX(-12px)} to{opacity:1;background:transparent;transform:translateX(0)} }
@keyframes spin { to{transform:rotate(360deg)} }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--panel); }
::-webkit-scrollbar-thumb { background: var(--mid); border-radius: 3px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo">
<div class="logo-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/>
<polyline points="16 7 22 7 22 13"/>
</svg>
</div>
<div class="logo-text">
<h1>TRADING MONITOR</h1>
<p>UPBIT_DATA · daemon_trading · REALTIME</p>
</div>
</div>
<div class="stat-bar">
<div class="stat-chip"><span class="dot dot-all"></span><span id="cntAll">–</span> TOTAL</div>
<div class="stat-chip"><span class="dot dot-bid"></span><span id="cntBid">–</span> BID</div>
<div class="stat-chip"><span class="dot dot-ask"></span><span id="cntAsk">–</span> ASK</div>
<div class="stat-chip"><span class="dot dot-ok"></span><span id="cntOk">–</span> SUCCESS</div>
<div class="stat-chip"><span class="dot dot-fail"></span><span id="cntFail">–</span> FAIL</div>
</div>
</header>
<div class="filter-area">
<div class="filter-group">
<span class="filter-label">SIDE</span>
<button class="fbtn active" data-filter="side" data-val="">ALL</button>
<button class="fbtn" data-filter="side" data-val="bid">BID 매수</button>
<button class="fbtn" data-filter="side" data-val="ask">ASK 매도</button>
</div>
<div class="filter-group" style="margin-left:10px">
<span class="filter-label">TRIGGER</span>
<button class="fbtn active" data-filter="trigger_type" data-val="">ALL</button>
<button class="fbtn" data-filter="trigger_type" data-val="daily_rate">DAILY RATE</button>
<button class="fbtn" data-filter="trigger_type" data-val="fixed_price">FIXED PRICE</button>
</div>
<div class="filter-group" style="margin-left:10px">
<span class="filter-label">ORD TYPE</span>
<button class="fbtn active" data-filter="ord_type" data-val="">ALL</button>
<button class="fbtn" data-filter="ord_type" data-val="price">PRICE</button>
<button class="fbtn" data-filter="ord_type" data-val="market">MARKET</button>
<button class="fbtn" data-filter="ord_type" data-val="limit">LIMIT</button>
</div>
</div>
<div class="search-bar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" id="searchInput" placeholder="거래 검색…" autocomplete="off">
<span class="search-hint">cron_id · market</span>
<button class="clear-btn" id="clearBtn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="live-row">
<div class="live-badge"><span class="pulse-dot"></span>LIVE · 3s AUTO REFRESH</div>
<div style="display:flex;align-items:center;gap:12px">
<span class="page-info" id="pageInfo">–</span>
<select class="per-select" id="perSelect">
<option value="20" selected>20개</option>
<option value="50">50개</option>
<option value="100">100개</option>
</select>
<span class="last-update" id="lastUpdate">–</span>
</div>
</div>
<div class="table-wrap" id="tableWrap">
<div class="spin-wrap"><div class="spin"></div> 데이터 로딩 중…</div>
</div>
<div class="pagination-wrap" id="paginationWrap">
<span class="page-total" id="pageTotal">–</span>
<div class="page-btns" id="pageBtns"></div>
</div>
</div>
<script>
const INTERVAL = 3000;
let searchVal = '';
let searchTimer = null;
let filters = { side: '', trigger_type: '', ord_type: '' };
let curPage = 1;
let totalPages = 1;
let perPage = 20;
const tableWrap = document.getElementById('tableWrap');
const searchInput = document.getElementById('searchInput');
const clearBtn = document.getElementById('clearBtn');
const lastUpdate = document.getElementById('lastUpdate');
const pageInfo = document.getElementById('pageInfo');
const pageTotal = document.getElementById('pageTotal');
const pageBtns = document.getElementById('pageBtns');
const perSelect = document.getElementById('perSelect');
/* ── 유틸 ── */
function esc(s) {
if (s == null) return '';
const d = document.createElement('div');
d.textContent = String(s);
return d.innerHTML;
}
function highlight(s, q) {
if (!q || !s) return esc(s);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return esc(s).replace(re, '<mark>$1</mark>');
}
function dec(v, d = 8) { return (v != null && v !== '') ? parseFloat(v).toFixed(d) : '–'; }
function num(v) { return (v != null && v !== '') ? Number(v).toLocaleString() : '–'; }
function sideBadge(s) {
if (s === 'bid') return `<span class="badge badge-bid"><span class="badge-dot"></span>BID</span>`;
if (s === 'ask') return `<span class="badge badge-ask"><span class="badge-dot"></span>ASK</span>`;
return esc(s);
}
function resultBadge(r) {
if (r === 'success') return `<span class="badge badge-success">✓ SUCCESS</span>`;
if (r === 'fail') return `<span class="badge badge-fail">✗ FAIL</span>`;
return esc(r);
}
function stateBadge(s) {
if (!s) return '–';
const cls = { wait: 'badge-wait', done: 'badge-done', cancel: 'badge-cancel' }[s] || '';
return `<span class="badge ${cls}">${esc(s).toUpperCase()}</span>`;
}
/* ── 통계 (전체 기준) ── */
function updateStats(total, data) {
document.getElementById('cntAll').textContent = total;
document.getElementById('cntBid').textContent = data.filter(r => r.side === 'bid').length;
document.getElementById('cntAsk').textContent = data.filter(r => r.side === 'ask').length;
document.getElementById('cntOk').textContent = data.filter(r => r.result === 'success').length;
document.getElementById('cntFail').textContent = data.filter(r => r.result === 'fail').length;
}
/* ── 행 생성 ── */
function buildRow(r, idx, q) {
const tr = document.createElement('tr');
tr.dataset.id = r.id;
const stepHtml = r.step_code ? `<span class="step-tag">${esc(r.step_code)}</span> <span style="color:var(--dim);font-size:.7rem">${esc(r.step_code_val)}</span>` : '–';
const ordHtml = r.ord_type ? `<span class="ord-tag">${esc(r.ord_type).toUpperCase()}</span>` : '–';
const mktHtml = r.market ? `<span class="market-tag">${highlight(r.market, q)}</span>` : '–';
tr.innerHTML = `
<td class="td-mono" style="color:var(--dim);font-size:.7rem">${idx + 1}</td>
<td class="td-mono" style="color:var(--blue)">${esc(r.id)}</td>
<td style="font-size:.75rem;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.cron_id)}">${highlight(r.cron_id, q)}</td>
<td>${mktHtml}</td>
<td>${sideBadge(r.side)}</td>
<td>${stepHtml}</td>
<td style="font-size:.72rem;color:var(--sub)">${esc(r.trigger_type) || '–'}</td>
<td class="td-mono" style="font-size:.72rem">${dec(r.trigger_value, 2)}</td>
<td>${ordHtml}</td>
<td class="td-mono" style="color:var(--yellow)">${num(r.req_price)}</td>
<td class="td-mono">${dec(r.req_volume)}</td>
<td class="td-uuid" title="${esc(r.upbit_uuid)}">${esc(r.upbit_uuid) || '–'}</td>
<td>${stateBadge(r.state)}</td>
<td class="td-mono">${dec(r.executed_volume)}</td>
<td class="td-mono" style="color:var(--green)">${dec(r.avg_price, 2)}</td>
<td class="td-mono" style="color:var(--dim)">${dec(r.paid_fee)}</td>
<td>${resultBadge(r.result)}</td>
<td style="font-size:.72rem;color:var(--dim);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.message)}">${esc(r.message) || '–'}</td>
<td class="td-mono" style="font-size:.7rem;white-space:nowrap">${esc(r.timestamp) || '–'}</td>`;
return tr;
}
/* ── 테이블 전체 렌더 ── */
function renderTable(rows, q) {
if (!rows.length) {
tableWrap.innerHTML = '<div class="empty-state">검색 결과가 없습니다</div>';
return;
}
const table = document.createElement('table');
table.innerHTML = `<thead><tr>
<th>#</th><th>ID</th><th>CRON_ID</th><th>MARKET</th><th>SIDE</th>
<th>STEP</th><th>TRIGGER TYPE</th><th>TRIGGER VAL</th><th>ORD TYPE</th>
<th>REQ PRICE</th><th>REQ VOL</th><th>UPBIT UUID</th><th>STATE</th>
<th>EXEC VOL</th><th>AVG PRICE</th><th>FEE</th><th>RESULT</th>
<th>MESSAGE</th><th>TIMESTAMP</th>
</tr></thead>`;
const tbody = document.createElement('tbody');
tbody.id = 'tbody';
const offset = (curPage - 1) * perPage;
rows.forEach((r, i) => tbody.appendChild(buildRow(r, offset + i, q)));
table.appendChild(tbody);
tableWrap.innerHTML = '';
tableWrap.appendChild(table);
}
/* ── 페이지네이션 렌더 ── */
function renderPagination(total, pages, page) {
pageTotal.textContent = `총 ${total.toLocaleString()}건 · ${pages}페이지`;
pageInfo.textContent = `${page} / ${pages} 페이지`;
pageBtns.innerHTML = '';
const addBtn = (label, targetPage, disabled = false, active = false, isNav = false) => {
const btn = document.createElement('button');
btn.className = 'pbtn' + (isNav ? ' pbtn-nav' : '') + (active ? ' active' : '');
btn.textContent = label;
btn.disabled = disabled;
if (!disabled) btn.addEventListener('click', () => goPage(targetPage));
pageBtns.appendChild(btn);
};
addBtn('«', 1, page <= 1, false, true);
addBtn('‹', page - 1, page <= 1, false, true);
// 페이지 번호 (최대 7개 표시)
const range = 3;
let start = Math.max(1, page - range);
let end = Math.min(pages, page + range);
if (page - range <= 1) end = Math.min(pages, 1 + range * 2);
if (page + range >= pages) start = Math.max(1, pages - range * 2);
if (start > 1) {
addBtn('1', 1);
if (start > 2) { const el = document.createElement('span'); el.className = 'page-ellipsis'; el.textContent = '…'; pageBtns.appendChild(el); }
}
for (let i = start; i <= end; i++) addBtn(String(i), i, false, i === page);
if (end < pages) {
if (end < pages - 1) { const el = document.createElement('span'); el.className = 'page-ellipsis'; el.textContent = '…'; pageBtns.appendChild(el); }
addBtn(String(pages), pages);
}
addBtn('›', page + 1, page >= pages, false, true);
addBtn('»', pages, page >= pages, false, true);
}
/* ── 페이지 이동 ── */
function goPage(p) {
curPage = p;
fetchData();
}
/* ── 쿼리 빌드 ── */
function buildQuery() {
let q = `?ajax=1&page=${curPage}&per=${perPage}`;
if (searchVal) q += `&search=${encodeURIComponent(searchVal)}`;
if (filters.side) q += `&side=${encodeURIComponent(filters.side)}`;
if (filters.trigger_type) q += `&trigger_type=${encodeURIComponent(filters.trigger_type)}`;
if (filters.ord_type) q += `&ord_type=${encodeURIComponent(filters.ord_type)}`;
return q;
}
/* ── fetch ── */
async function fetchData() {
try {
const res = await fetch(buildQuery(), { cache: 'no-store' });
const json = await res.json();
if (!json.success) {
tableWrap.innerHTML = `<div class="db-error">DB 오류: ${esc(json.error)}</div>`;
return;
}
totalPages = json.pages;
// 현재 페이지가 범위 초과 시 보정
if (curPage > totalPages && totalPages > 0) { curPage = totalPages; fetchData(); return; }
updateStats(json.total, json.data);
renderTable(json.data, searchVal);
renderPagination(json.total, json.pages, json.page);
lastUpdate.textContent = 'UPDATED ' + new Date().toLocaleTimeString('ko-KR');
} catch(e) { console.error(e); }
}
/* ── 리셋 & 재조회 ── */
function resetAndFetch() {
curPage = 1;
tableWrap.innerHTML = '<div class="spin-wrap"><div class="spin"></div> 로딩 중…</div>';
fetchData();
}
/* ── 필터 버튼 ── */
document.querySelectorAll('.fbtn').forEach(btn => {
btn.addEventListener('click', () => {
const group = btn.dataset.filter;
const val = btn.dataset.val;
document.querySelectorAll(`.fbtn[data-filter="${group}"]`).forEach(b => b.classList.remove('active', 'active-bid', 'active-ask'));
btn.classList.add('active');
if (val === 'bid') btn.classList.add('active-bid');
if (val === 'ask') btn.classList.add('active-ask');
filters[group] = val;
resetAndFetch();
});
});
/* ── 검색 ── */
searchInput.addEventListener('input', () => {
searchVal = searchInput.value.trim();
clearBtn.classList.toggle('show', searchVal.length > 0);
clearTimeout(searchTimer);
searchTimer = setTimeout(resetAndFetch, 300);
});
clearBtn.addEventListener('click', () => {
searchInput.value = '';
searchVal = '';
clearBtn.classList.remove('show');
resetAndFetch();
});
/* ── 페이지당 수량 변경 ── */
perSelect.addEventListener('change', () => {
perPage = parseInt(perSelect.value);
resetAndFetch();
});
/* ── 시작 ── */
fetchData();
setInterval(fetchData, INTERVAL);
</script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>