<?php
// ==================================================================
// 설정 및 DB 연결
// ==================================================================
error_reporting(E_ALL);
ini_set('display_errors', 1);
try {
require '/home/www/DB/db_upbit.php';
$pdo = $db_upbit;
} catch (Exception $e) {
die("데이터베이스 연결 실패: " . $e->getMessage());
}
/** @var PDO $pdo */
$pdo_gnu = null;
try {
require '/home/www/DB/db_gnu.php';
if (isset($db_gnu) && $db_gnu instanceof PDO) {
$pdo_gnu = $db_gnu;
} elseif (isset($pdo_gnu) && $pdo_gnu instanceof PDO) {
$pdo_gnu = $pdo_gnu;
} elseif (isset($pdo) && $pdo instanceof PDO) {
$pdo_gnu = $pdo;
}
} catch (Exception $e) {
$pdo_gnu = null;
}
/** @var PDO|null $pdo_gnu */
// ==================================================================
// 헬퍼 함수 (기존 코드 유지)
// ==================================================================
function getTimeDiff($datetime) {
if (!$datetime) return 'N/A';
try {
$now = new DateTime();
$ago = new DateTime($datetime);
$diff = $now->diff($ago);
if ($diff->d > 0) return $diff->d . '일 전';
if ($diff->h > 0) return $diff->h . '시간 전';
if ($diff->i > 0) return $diff->i . '분 전';
return $diff->s . '초 전';
} catch (Exception $e) { return '형식 오류'; }
}
function isHeartbeatDead($datetime) {
if (!$datetime) return true;
$now = new DateTime();
$ago = new DateTime($datetime);
$diff = $now->getTimestamp() - $ago->getTimestamp();
return $diff > 300;
}
function getBybitBestSymbols($pdo_gnu) {
static $symbols = null;
if ($symbols !== null) {
return $symbols;
}
$symbols = [];
if (!($pdo_gnu instanceof PDO)) {
return $symbols;
}
try {
$sql = "SELECT wr_subject FROM g5_write_daemon_best_bybit WHERE (x2_run = 1 OR x2_run = '1')";
$stmt = $pdo_gnu->query($sql);
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($rows as $symbol) {
$symbol = strtoupper(trim((string)$symbol));
if ($symbol === '') {
continue;
}
if (strpos($symbol, 'USDT') === false && strpos($symbol, '-') === false) {
$symbol .= 'USDT';
}
if (!in_array($symbol, $symbols, true)) {
$symbols[] = $symbol;
}
}
} catch (Exception $e) {
return [];
}
return $symbols;
}
function getDaemonMetrics($pdo, $pdo_gnu, $daemon_id) {
$default = [
'total_count_raw' => 0,
'total_count_str' => '-',
'last_trade' => '데이터 없음',
'trade_diff' => ''
];
$config_map = [
'daemon_upbit_Ticker' => [
'table' => 'daemon_upbit_Ticker',
'date_column' => 'collected_at',
'where_sql' => "WHERE market = ?",
'params' => ['KRW-BCH']
],
'daemon_upbit_Best' => [
'table' => 'daemon_upbit_Ticker',
'date_column' => 'collected_at',
'where_sql' => "WHERE market = ?",
'params' => ['KRW-BTC']
],
'daemon_upbit_Ticker_user' => [
'table' => 'daemon_upbit_Ticker_user',
'date_column' => 'collected_at',
'where_sql' => '',
'params' => []
],
'daemon_bybit_Ticker' => [
'table' => 'daemon_bybit_Ticker',
'date_column' => 'updated_at',
'where_sql' => '',
'params' => []
]
];
if ($daemon_id === 'daemon_bybit_Best') {
$symbols = getBybitBestSymbols($pdo_gnu);
if (!$symbols) {
return $default;
}
$placeholders = implode(', ', array_fill(0, count($symbols), '?'));
$config = [
'table' => 'daemon_bybit_Ticker',
'date_column' => 'updated_at',
'where_sql' => "WHERE symbol IN ($placeholders)",
'params' => $symbols
];
} else {
if (!isset($config_map[$daemon_id])) {
return $default;
}
$config = $config_map[$daemon_id];
}
try {
$sql = "SELECT COUNT(*) AS total_cnt, MAX({$config['date_column']}) AS last_trade FROM {$config['table']} {$config['where_sql']}";
$stmt = $pdo->prepare($sql);
$stmt->execute($config['params']);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return $default;
}
$total_count_raw = (int)($row['total_cnt'] ?? 0);
$last_trade = $row['last_trade'] ?: '데이터 없음';
return [
'total_count_raw' => $total_count_raw,
'total_count_str' => $total_count_raw > 0 ? number_format($total_count_raw) : '0',
'last_trade' => $last_trade,
'trade_diff' => $row['last_trade'] ? getTimeDiff($row['last_trade']) : ''
];
} catch (Exception $e) {
return $default;
}
}
// ==================================================================
// 실시간 데이터를 위한 API 모드 (기존 로직 유지)
// ==================================================================
if (isset($_GET['api'])) {
header('Content-Type: application/json');
$target_ids = ['daemon_upbit_Ticker', 'daemon_upbit_Ticker_user', 'daemon_upbit_Best', 'daemon_bybit_Ticker', 'daemon_bybit_Best'];
$in_clause = "'" . implode("','", $target_ids) . "'";
$sql = "SELECT * FROM daemon_record WHERE d_id IN ($in_clause) ORDER BY FIELD(d_id, $in_clause)";
$stmt = $pdo->query($sql);
$daemons = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach ($daemons as $daemon) {
$is_dead = isHeartbeatDead($daemon['d_heartbeat']);
$metrics = getDaemonMetrics($pdo, $pdo_gnu, $daemon['d_id']);
$result[] = [
'd_id' => $daemon['d_id'],
'd_status' => $daemon['d_status'],
'is_dead' => $is_dead,
'total_count' => $metrics['total_count_str'],
'total_count_raw' => $metrics['total_count_raw'],
'last_trade' => $metrics['last_trade'],
'trade_diff' => $metrics['trade_diff'],
'heartbeat_diff' => getTimeDiff($daemon['d_heartbeat']),
'd_pid' => $daemon['d_pid'] ?: '-',
'd_kill_flag' => (bool)$daemon['d_kill_flag']
];
}
echo json_encode($result);
exit;
}
// 초기 데이터 로드
$target_ids = ['daemon_upbit_Ticker', 'daemon_upbit_Ticker_user', 'daemon_upbit_Best', 'daemon_bybit_Ticker', 'daemon_bybit_Best'];
$in_clause = "'" . implode("','", $target_ids) . "'";
$sql = "SELECT * FROM daemon_record WHERE d_id IN ($in_clause) ORDER BY FIELD(d_id, $in_clause)";
$stmt = $pdo->query($sql);
$daemons = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 헤더 부분 포함
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>핵심 데몬 가동 현황</title>
<!-- 아이콘과 폰트만 외부 호출 허용 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
/* CSS 변수 설정 */
:root {
--bg-body: #f1f5f9;
--bg-card: #ffffff;
--bg-metric: #f8fafc;
--text-main: #1e293b;
--text-sub: #64748b;
--indigo: #6366f1;
--border-card: #e2e8f0;
}
.dark {
--bg-body: #0a0f1d;
--bg-card: #161b2c;
--bg-metric: rgba(13, 17, 29, 0.5);
--text-main: #f1f5f9;
--text-sub: #94a3b8;
--indigo: #818cf8;
--border-card: rgba(30, 41, 59, 0.5);
}
body {
font-family: 'Pretendard', sans-serif;
font-style: normal !important;
background-color: var(--bg-body);
margin: 0;
padding: 0px 0px 40px 0px; /* px-[60px] 요구사항 반영 */
overflow-x: hidden;
transition: background-color 0.3s;
}
* { font-style: normal !important; box-sizing: border-box; }
.max-w-full { width: 100vw; padding: 30px 50px 0px 50px; }
.page-enter { animation: enter 0.6s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes enter { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } }
/* Header Styles */
header { margin-bottom: 20px; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; gap: 1.5rem; }
@media (min-width: 768px) { header { flex-direction: row; } }
header h1 { font-size: 2.25rem; font-weight: 900; color: var(--text-main); margin: 0; letter-spacing: -0.05em; }
header h1 span { color: #6366f1; }
.header-desc { font-size: 1.125rem; color: var(--text-sub); margin-top: 0.75rem; font-weight: 500; display: flex; align-items: center; }
#update-timer { margin-left: 0.75rem; font-size: 0.875rem; background: #e2e8f0; color: #64748b; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-family: monospace; }
.dark #update-timer { background: #1e293b; color: #94a3b8; }
/* Grid System */
.grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }
/* Card Styles */
.daemon-card {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--bg-card);
border-radius: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
border: 1px solid var(--border-card);
overflow: hidden;
}
.daemon-card:hover { transform: translateY(-5px); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); }
.kill-flag-overlay {
position: absolute; top: 0; right: 0; background: #dc2626; color: white;
font-size: 0.75rem; font-weight: 700; padding: 0.5rem 1rem;
border-bottom-left-radius: 1rem; z-index: 10;
}
.card-body { padding: 2.5rem; flex-grow: 1; }
.card-head-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; }
.card-info-group { display: flex; align-items: center; gap: 1.5rem; }
.icon-box {
width: 4rem; height: 4rem; border-radius: 1rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; background: #1e293b; color: #94a3b8; border: 1px solid #334155;
}
.card-title { font-size: 1.5rem; font-weight: 800; color: var(--text-main); margin: 0; line-height: 1.2; }
.card-id { font-size: 0.875rem; color: var(--text-sub); font-family: monospace; margin-top: 0.25rem; }
.status-badge { padding: 0.5rem 1.25rem; border-radius: 0.75rem; font-size: 0.875rem; font-weight: 900; }
/* Metric Box */
.metric-box { background-color: var(--bg-metric); border-radius: 1rem; padding: 1.5rem; border: 1px solid var(--border-card); margin-bottom: 1.5rem; }
.metric-label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.metric-label { font-size: 0.875rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; }
.mode-badge { font-size: 0.75rem; font-weight: 900; padding: 0.25rem 0.75rem; border-radius: 0.5rem; border: 1px solid transparent; }
.total-cnt { font-size: 2.25rem; font-weight: 900; color: var(--indigo); letter-spacing: -0.05em; }
.cnt-unit { font-size: 1.125rem; font-weight: 700; color: var(--text-sub); margin-left: 0.5rem; }
/* Details */
.details-container { display: flex; flex-direction: column; gap: 1.5rem; }
.detail-label-row { display: flex; justify-content: space-between; align-items: end; margin-bottom: 0.5rem; }
.detail-label { font-size: 0.75rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; }
.detail-diff { font-size: 0.75rem; font-weight: 900; color: var(--indigo); }
.detail-value-box { font-family: monospace; font-size: 1rem; font-weight: 600; color: var(--text-main); background: var(--bg-metric); padding: 0.75rem 1rem; border-radius: 0.75rem; border: 1px solid var(--border-card); }
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 0.5rem; }
.meta-label { font-size: 0.75rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; display: block; margin-bottom: 0.25rem; }
.meta-value { font-family: monospace; font-size: 0.875rem; font-weight: 700; color: var(--indigo); }
.meta-value.gray { color: var(--text-sub); }
/* Card Footer */
.card-footer { background-color: var(--bg-metric); padding: 1.25rem 2.5rem; border-top: 1px solid var(--border-card); display: flex; justify-content: space-between; align-items: center; }
.hb-row { display: flex; align-items: center; gap: 0.75rem; color: var(--text-sub); font-weight: 700; font-size: 0.875rem; }
.hb-value { color: #22d3ee; font-weight: 900; font-size: 1.25rem; letter-spacing: -0.025em; }
.start-time { font-family: monospace; font-size: 0.75rem; color: var(--text-sub); font-weight: 700; opacity: 0.7; }
/* Buttons */
.btn-container { margin-top: 25px; display: flex; flex-direction: column; align-items: center; font-size: 18px; }
.btn-primary {
background-color: #4f46e5; color: white; font-weight: 600; padding: 0.75rem 2rem;
border-radius: 9999px; text-decoration: none; box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem;
}
.btn-primary:hover { background-color: #4338ca; transform: scale(1.05); }
/* Utility */
.blink { animation: blinker 1.5s linear infinite; }
@keyframes blinker { 50% { opacity: 0; } }
.update-flash { animation: flash 1s ease-out; }
@keyframes flash { 0% { background-color: rgba(99, 102, 241, 0.2); } 100% { background-color: transparent; } }
</style>
</head>
<body class="bg-slate-100">
<div class="max-w-full page-enter">
<!-- Header -->
<header>
<div>
<h1><span>핵심</span> 데몬 가동 현황</h1>
<div class="header-desc">
실시간 데이터 수집 프로세스 모니터링 시스템
<span id="update-timer">상태 확인 중...</span>
</div>
</div>
</header>
<!-- Cards Grid -->
<div class="grid">
<?php foreach ($daemons as $daemon): ?>
<?php $target_id = $daemon['d_id']; ?>
<div id="card-<?php echo $target_id; ?>" class="daemon-card">
<div id="kill-flag-<?php echo $target_id; ?>" class="kill-flag-overlay blink" style="display:none;">
종료 프로세스 감지
</div>
<div class="card-body">
<div class="card-head-row">
<div class="card-info-group">
<div id="icon-box-<?php echo $target_id; ?>" class="icon-box">
<i class="fa-solid fa-circle-notch fa-spin"></i>
</div>
<div>
<h3 class="card-title"><?php echo explode('_', $target_id, 3)[2]; ?></h3>
<p class="card-id"><?php echo htmlspecialchars($target_id); ?></p>
</div>
</div>
<span id="status-badge-<?php echo $target_id; ?>" class="status-badge" style="background:#f1f5f9; color:#64748b;">
...
</span>
</div>
<div class="details-container">
<div id="metric-box-<?php echo $target_id; ?>" class="metric-box">
<div class="metric-label-row">
<span class="metric-label">누적 수집 데이터</span>
<span id="mode-badge-<?php echo $target_id; ?>" class="mode-badge">분석 중</span>
</div>
<div>
<span id="total-cnt-<?php echo $target_id; ?>" class="total-cnt">...</span>
<span class="cnt-unit">건</span>
</div>
</div>
<div>
<div class="detail-label-row">
<span class="detail-label">최종 동기화 시점</span>
<span id="diff-<?php echo $target_id; ?>" class="detail-diff">...</span>
</div>
<div id="last-trade-<?php echo $target_id; ?>" class="detail-value-box">...</div>
</div>
<div class="meta-grid">
<div>
<span class="meta-label">프로세스 ID</span>
<span id="pid-<?php echo $target_id; ?>" class="meta-value">...</span>
</div>
<div>
<span class="meta-label">서버 위치</span>
<span class="meta-value gray"><?php echo $daemon['d_ip'] ?? '-'; ?></span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="hb-row">
<i class="fa-regular fa-clock"></i>
<span>생존 신호:</span>
<span id="hb-<?php echo $target_id; ?>" class="hb-value">...</span>
</div>
<div class="start-time">
STARTED: <?php echo substr($daemon['d_start_time'], 5, 11); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="btn-container">
<a href="/home/www/GNU/_PAGE/monitoring/upbit/daemon_market/daemon.php" class="btn-primary">
<i class="fa-solid fa-list-check"></i>
데몬 리스트 전체보기
</a>
</div>
</div>
<script>
// 실시간 데이터를 업데이트하는 함수 (기존 로직 100% 유지)
async function updateData() {
try {
const response = await fetch('?api=1');
const data = await response.json();
data.forEach(d => {
const id = d.d_id;
const cntElem = document.getElementById(`total-cnt-${id}`);
const badge = document.getElementById(`status-badge-${id}`);
const iconBox = document.getElementById(`icon-box-${id}`);
const killOverlay = document.getElementById(`kill-flag-${id}`);
const modeBadge = document.getElementById(`mode-badge-${id}`);
if(cntElem.innerText !== d.total_count) {
cntElem.innerText = d.total_count;
document.getElementById(`metric-box-${id}`).classList.add('update-flash');
setTimeout(() => document.getElementById(`metric-box-${id}`).classList.remove('update-flash'), 1000);
}
if (d.total_count_raw === 1) {
modeBadge.innerText = "데이터 덮어쓰기";
modeBadge.style.background = "rgba(249, 115, 22, 0.1)";
modeBadge.style.color = "#f97316";
modeBadge.style.borderColor = "rgba(249, 115, 22, 0.2)";
} else {
modeBadge.innerText = "데이터 쌓아쓰기";
modeBadge.style.background = "rgba(59, 130, 246, 0.1)";
modeBadge.style.color = "#3b82f6";
modeBadge.style.borderColor = "rgba(59, 130, 246, 0.2)";
}
document.getElementById(`last-trade-${id}`).innerText = d.last_trade;
document.getElementById(`diff-${id}`).innerText = d.trade_diff;
document.getElementById(`pid-${id}`).innerText = d.d_pid;
document.getElementById(`hb-${id}`).innerText = d.heartbeat_diff;
killOverlay.style.display = d.d_kill_flag ? 'block' : 'none';
let bStyle = "", tColor = "", label = "", icon = "";
if (d.d_status === 'RUNNING') {
if (d.is_dead) {
bStyle = "rgba(249, 115, 22, 0.1)"; tColor = "#f97316";
label = "응답 지연"; icon = '<i class="fa-solid fa-triangle-exclamation" style="color:#f97316;"></i>';
} else {
bStyle = "#22c55e"; tColor = "#ffffff";
label = "가동 중"; icon = '<i class="fa-solid fa-bolt" style="color:#10b981;"></i>';
}
} else if (d.d_status === 'STOPPED') {
bStyle = "rgba(239, 68, 68, 0.1)"; tColor = "#ef4444";
label = "중지됨"; icon = '<i class="fa-solid fa-power-off" style="color:#ef4444;"></i>';
} else {
bStyle = "#334155"; tColor = "#94a3b8";
label = "오류"; icon = '<i class="fa-solid fa-bug" style="color:#94a3b8;"></i>';
}
badge.style.background = bStyle;
badge.style.color = tColor;
badge.innerText = label;
iconBox.innerHTML = icon;
});
} catch (e) { console.error("Update fail", e); }
}
let countdown = 5;
setInterval(() => {
countdown--;
if(countdown <= 0) {
updateData();
countdown = 5;
}
document.getElementById('update-timer').innerText = `NEXT SYNC IN ${countdown}s`;
}, 1000);
updateData();
// 테마 감지
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>