<?php
/**
* db_data.php
* - 기존 로직 및 구조 유지
* - 컬럼 설정 및 고도화된 JS 기능 추가
*/
date_default_timezone_set('Asia/Seoul');
// 1. 설정 및 파라미터 처리
$DEFAULT_TABLE = 'daemon_upbit_Ticker';
$LIMIT = 7; // 표시 행 수
$table = $_GET['table'] ?? $DEFAULT_TABLE;
$mode = $_GET['mode'] ?? 'html';
// 2. DB 연결
require_once '/home/www/DB/db_upbit.php';
if (!isset($db_upbit)) {
$error_msg = 'DB 연결 설정 파일을 찾을 수 없습니다.';
if ($mode === 'json') {
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => $error_msg]); exit;
}
die($error_msg);
}
$pdo = $db_upbit;
// 3. 보안 및 테이블 유효성 검사
try {
$stmt = $pdo->query("SHOW TABLES");
$all_tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!in_array($table, $all_tables, true)) {
throw new Exception("존재하지 않는 테이블: " . htmlspecialchars($table));
}
$stmt = $pdo->query("SHOW COLUMNS FROM `{$table}`");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
} catch (Exception $e) {
if ($mode === 'json') {
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
}
die($e->getMessage());
}
// 4. 쿼리 생성
$orderBy = "";
$priorityCols = ['timestamp_ms', 'timestamp', 'collected_ms', 'created_at', 'id'];
foreach ($priorityCols as $c) {
if (in_array($c, $columns, true)) {
$orderBy = " ORDER BY `{$c}` DESC ";
break;
}
}
if (!$orderBy && !empty($columns)) $orderBy = " ORDER BY `{$columns[0]}` DESC ";
$whereClause = "";
if (in_array('market', $columns, true)) {
$TARGET_MARKETS = ['KRW-BTC', 'KRW-ETH', 'KRW-XRP', 'KRW-SOL', 'KRW-ADA', 'KRW-BCH', 'KRW-DOGE'];
$whereClause = " WHERE `market` IN ('" . implode("','", $TARGET_MARKETS) . "') ";
}
$sql = "SELECT * FROM `{$table}` {$whereClause} {$orderBy} LIMIT {$LIMIT}";
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($mode === 'json') {
header('Content-Type: application/json');
echo json_encode([
'ok' => true,
'table' => $table,
'columns' => $columns,
'rows' => $rows,
'server_time' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE);
exit;
}
// 헤더 부분 포함
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title><?= htmlspecialchars($table) ?> PRO MONITOR</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg-main: #020408;
--bg-card: #0b101a;
--bg-header: #151c2c;
--border: #1e293b;
--accent: #00f2ff;
--text-p: #f8fafc;
--text-s: #94a3b8;
--row-h: 70px;
--success: #22c55e;
--danger: #ef4444;
}
body {
font-family: 'Pretendard', -apple-system, sans-serif;
background: var(--bg-main);
color: var(--text-p);
margin: 0;
padding: 0 0 40px 0;
overflow-x: hidden;
}
.container {
width: calc(100% - 100px);
margin: 0 50px;
animation: riseUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes riseUp {
0% { opacity: 0; transform: translateY(50px); }
100% { opacity: 1; transform: translateY(0); }
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 15px;
flex-wrap: wrap;
}
.table-nav-select {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--accent);
padding: 10px 15px;
border-radius: 8px;
outline: none;
cursor: pointer;
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
transition: 0.3s;
}
.table-nav-select:hover { border-color: var(--accent); }
.search-box {
flex: 1;
min-width: 300px;
position: relative;
}
.search-box input {
float:right;
width: 700px;
background: var(--bg-card);
border: 1px solid var(--border);
color: #fff;
padding: 12px 20px;
border-radius: 8px;
outline: none;
transition: 0.3s;
}
.search-box input:focus { border-color: var(--accent); box-shadow: 0 0 10px rgba(0,242,255,0.1); }
.btn-group { display: flex; gap: 8px; }
button {
background: var(--bg-header);
border: 1px solid var(--border);
color: var(--text-p);
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: 0.2s;
}
button:hover { background: var(--border); border-color: var(--text-s); }
button.active { background: var(--accent); color: var(--bg-main); border-color: var(--accent); }
#colPanel {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
display: none;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.table-wrap {
background: var(--bg-card);
border-radius: 12px;
border: 1px solid var(--border);
overflow: hidden;
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
}
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
height: var(--row-h);
background: var(--bg-header);
color: var(--text-s);
text-align: left;
padding: 0 20px;
font-weight: 600;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
th:hover { color: var(--accent); }
th.sort-asc::after { content: ' ↑'; }
th.sort-desc::after { content: ' ↓'; }
td {
height: var(--row-h);
padding: 0 20px;
border-bottom: 1px solid rgba(255,255,255,0.03);
white-space: nowrap;
}
tr:hover td { background: rgba(255,255,255,0.02); }
.inc { color: var(--success); animation: flash-g 0.8s; font-weight: bold; }
.dec { color: var(--danger); animation: flash-r 0.8s; font-weight: bold; }
@keyframes flash-g { from { background: rgba(34,197,94,0.2); } to { background: transparent; } }
@keyframes flash-r { from { background: rgba(239,68,68,0.2); } to { background: transparent; } }
.pinned { position: sticky; left: 0; background: var(--bg-card) !important; z-index: 2; border-right: 1px solid var(--border); }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
#refreshCounter { font-family: monospace; color: var(--accent); }
</style>
</head>
<body>
<div class="container">
<div class="toolbar">
<div>
<select class="table-nav-select" onchange="location.href='?table='+this.value">
<?php
$nav_tables = [
'daemon_upbit_Ticker' => 'Ticker (실시간)',
'daemon_upbit_coin_1m' => '1분봉 (1m)',
'daemon_upbit_coin_5m' => '5분봉 (5m)',
'daemon_upbit_coin_15m' => '15분봉 (15m)',
'daemon_upbit_coin_30m' => '30분봉 (30m)',
'daemon_upbit_coin_1h' => '1시간봉 (1h)',
'daemon_upbit_coin_4h' => '4시간봉 (4h)',
'daemon_upbit_coin_8h' => '8시간봉 (8h)',
'daemon_upbit_coin_12h' => '12시간봉 (12h)',
'daemon_upbit_coin_24h' => '24시간봉 (24h)'
];
foreach ($nav_tables as $val => $name) {
$selected = ($table === $val) ? 'selected' : '';
echo "<option value='{$val}' {$selected}>{$name}</option>";
}
?>
</select>
<h1 style="margin:0; font-size:28px;"><?= htmlspecialchars($table) ?> <span id="refreshCounter"></span></h1>
<p style="margin:5px 0 0; color:var(--text-s); font-size:12px;">Real-time AJAX Data Processing System</p>
</div>
<div class="search-box">
<input type="text" id="globalSearch" placeholder="모든 컬럼에서 검색...">
</div>
<div class="btn-group">
<button id="toggleRefresh">Pause</button>
<button id="toggleColBtn">Columns</button>
<button onclick="exportToCSV()">Export CSV</button>
</div>
</div>
<div id="colPanel"></div>
<div class="table-wrap">
<div style="overflow-x: auto;">
<table id="mainTable">
<thead id="thead"></thead>
<tbody id="tbody"></tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
// 컬럼 한글 매핑 데이터
const colMap = {
"id": "기본키",
"market": "업비트 마켓",
"trade_date": "체결 날짜(UTC)",
"trade_time": "체결 시각(UTC)",
"trade_date_kst": "체결 날짜(KST)",
"trade_time_kst": "체결 시각(KST)",
"opening_price": "시가",
"high_price": "고가",
"low_price": "저가",
"trade_price": "현재가",
"prev_closing_price": "전일 종가",
"change": "전일 대비 방향",
"change_price": "가격 변화",
"change_rate": "변화율",
"signed_change_price": "부호 포함 가격 변화",
"signed_change_rate": "부호 포함 변화율",
"trade_volume": "최근 거래량",
"acc_trade_volume": "누적 거래량",
"acc_trade_volume_24h": "24시간 누적 거래량",
"acc_trade_price": "누적 거래대금",
"acc_trade_price_24h": "24시간 누적 거래대금",
"highest_52_week_price": "52주 최고가",
"highest_52_week_date": "52주 최고가 날짜",
"lowest_52_week_price": "52주 최저가",
"lowest_52_week_date": "52주 최저가 날짜",
"collected_at": "서버 수집 시각",
"collected_ms": "서버 수집 시각(ms)",
"ob_timestamp": "오더북 타임스탬프(ms)",
"ob_total_ask_size": "전체 매도 잔량",
"ob_total_bid_size": "전체 매수 잔량",
"ob_units": "호가 묶음",
"ob_collected_at": "오더북 수집 시각",
"ob_collected_ms": "오더북 수집 ms",
"tr_trade_timestamp": "체결 시간 타임스탬프(ms)",
"tr_trade_price": "체결 가격",
"tr_trade_volume": "체결 수량",
"tr_ask_bid": "매수/매도 구분",
"tr_trade_date_utc": "UTC 체결일",
"tr_trade_time_utc": "UTC 체결시간",
"tr_trade_date_kst": "KST 체결일",
"tr_trade_time_kst": "KST 체결시간",
"tr_collected_at": "체결 수집 시각",
"tr_collected_ms": "체결 수집 ms",
"day_of_week": "요일",
"korean_name": "마켓 한글명",
"daemon_id": "데몬 ID",
"daemon_pid": "데몬 PID",
"daemon_heartbeat": "데몬 하트비트"
};
const state = {
currentTable: "<?= $table ?>",
allColumns: <?= json_encode($columns) ?>,
hiddenCols: JSON.parse(localStorage.getItem('hide_cols_<?= $table ?>') || '["ob_units"]'),
data: [],
prevData: {},
isPaused: false,
searchTerm: '',
sortCol: '',
sortDir: 'desc',
countdown: 10
};
function initColPanel() {
const panel = document.getElementById('colPanel');
panel.innerHTML = state.allColumns.map(col => `
<label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
<input type="checkbox" ${!state.hiddenCols.includes(col) ? 'checked' : ''}
onchange="window.toggleColumn('${col}')">
<span style="font-size:13px;">${colMap[col] || col}</span>
</label>
`).join('');
}
window.toggleColumn = (col) => {
if (state.hiddenCols.includes(col)) {
state.hiddenCols = state.hiddenCols.filter(c => c !== col);
} else {
state.hiddenCols.push(col);
}
localStorage.setItem('hide_cols_<?= $table ?>', JSON.stringify(state.hiddenCols));
renderTable();
};
async function fetchData() {
if (state.isPaused) return;
try {
const res = await fetch(`?mode=json&table=${state.currentTable}&_=${Date.now()}`);
const json = await res.json();
if (json.ok) {
state.data = json.rows;
renderTable();
state.countdown = 10;
}
} catch (e) {
console.error("Fetch Error:", e);
}
}
function renderTable() {
let displayData = state.data.filter(row => {
return Object.values(row).some(val =>
String(val).toLowerCase().includes(state.searchTerm.toLowerCase())
);
});
if (state.sortCol) {
displayData.sort((a, b) => {
let v1 = a[state.sortCol], v2 = b[state.sortCol];
if (!isNaN(v1) && !isNaN(v2)) { v1 = parseFloat(v1); v2 = parseFloat(v2); }
if (v1 < v2) return state.sortDir === 'asc' ? -1 : 1;
if (v1 > v2) return state.sortDir === 'asc' ? 1 : -1;
return 0;
});
}
const activeCols = state.allColumns.filter(c => !state.hiddenCols.includes(c));
document.getElementById('thead').innerHTML = `<tr>${activeCols.map(col => `
<th class="${(col==='market'||col==='coin')?'pinned':''} ${state.sortCol===col? 'sort-'+state.sortDir : ''}"
onclick="window.setSort('${col}')">${colMap[col] || col}</th>
`).join('')}</tr>`;
document.getElementById('tbody').innerHTML = displayData.map((row, idx) => `
<tr>${activeCols.map(col => {
let val = row[col] ?? '';
let key = `${idx}_${col}`;
let diffClass = '';
if (state.prevData[key] !== undefined && !isNaN(val) && val !== '') {
if (parseFloat(val) > parseFloat(state.prevData[key])) diffClass = 'inc';
else if (parseFloat(val) < parseFloat(state.prevData[key])) diffClass = 'dec';
}
state.prevData[key] = val;
return `<td class="${(col==='market'||col==='coin')?'pinned':''} ${diffClass}">${val}</td>`;
}).join('')}</tr>
`).join('');
}
window.setSort = (col) => {
if (state.sortCol === col) {
state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
} else {
state.sortCol = col;
state.sortDir = 'desc';
}
renderTable();
};
document.getElementById('globalSearch').oninput = (e) => {
state.searchTerm = e.target.value;
renderTable();
};
document.getElementById('toggleRefresh').onclick = function() {
state.isPaused = !state.isPaused;
this.innerText = state.isPaused ? 'Resume' : 'Pause';
this.classList.toggle('active', state.isPaused);
};
document.getElementById('toggleColBtn').onclick = () => {
const p = document.getElementById('colPanel');
p.style.display = p.style.display === 'none' ? 'grid' : 'none';
};
window.exportToCSV = () => {
const activeCols = state.allColumns.filter(c => !state.hiddenCols.includes(c));
let csv = "\uFEFF"; // 한글 깨짐 방지 BOM
csv += activeCols.map(col => colMap[col] || col).join(',') + '\n';
state.data.forEach(row => {
csv += activeCols.map(col => `"${row[col] ?? ''}"`).join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `data_${state.currentTable}.csv`;
a.click();
};
setInterval(() => {
if(!state.isPaused) {
state.countdown--;
if(state.countdown <= 0) {
fetchData();
state.countdown = 10;
}
document.getElementById('refreshCounter').innerText = `(${state.countdown}s)`;
} else {
document.getElementById('refreshCounter').innerText = `(Paused)`;
}
}, 100);
initColPanel();
fetchData();
})();
</script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>