GNU/_PAGE/monitoring/upbit/daemon/daemon_trading.php
<?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>&nbsp;TOTAL</div>
      <div class="stat-chip"><span class="dot dot-bid"></span><span id="cntBid">–</span>&nbsp;BID</div>
      <div class="stat-chip"><span class="dot dot-ask"></span><span id="cntAsk">–</span>&nbsp;ASK</div>
      <div class="stat-chip"><span class="dot dot-ok"></span><span id="cntOk">–</span>&nbsp;SUCCESS</div>
      <div class="stat-chip"><span class="dot dot-fail"></span><span id="cntFail">–</span>&nbsp;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'; ?>