trafficmonitor init...1

This commit is contained in:
choi.jh 2025-11-13 13:55:17 +09:00
parent 5c5c66227f
commit 63d159c1be
4 changed files with 380 additions and 1 deletions

View File

@ -52,6 +52,8 @@ $routes->group('admin', ['namespace' => 'App\Controllers\Admin', 'filter' => 'au
$routes->post('batchjob', 'TrafficController::batchjob');
$routes->post('batchjob_delete', 'TrafficController::batchjob_delete');
$routes->get('download/(:alpha)', 'TrafficController::download/$1');
$routes->get('dashboard', 'TrafficController::dashboard');
$routes->post('data', 'TrafficController::getAggregatedData');
});
$routes->group('collector', ['namespace' => 'App\Controllers\Admin'], function ($routes) {
$routes->get('/', 'CollectorController::index');

View File

@ -25,7 +25,7 @@ class TrafficController extends AdminController
}
protected function action_init_process(string $action): void
{
$fields = ['client', 'server', 'switch', 'server_ip', 'interface', 'ip', 'status'];
$fields = ['client', 'server', 'switch', 'server_ip', 'interface', 'ip', 'status'];
$filters = ['status'];
switch ($action) {
case 'create':
@ -71,4 +71,46 @@ class TrafficController extends AdminController
{
return parent::view_process($uid);
}
public function dashboard(): string
{
$this->action_init_process(__FUNCTION__);
return $this->action_render_process($this->getActionPaths(), __FUNCTION__, $this->getViewDatas());
}
/**
* AJAX 요청을 처리하고, 모델에서 집계된 데이터를 JSON 형식으로 반환합니다.
*/
public function getAggregatedData(): ResponseInterface
{
$uid = $this->request->getPost('uid');
$entity = $this->service->getEntity($uid);
if (!$entity instanceof TrafficEntity) {
throw new \Exception(__METHOD__ . "에서 오류발생:{$uid}에 해당하는 트래픽정보를 찾을수없습니다.");
}
$startDate = $this->request->getPost('startDate');
$endDate = $this->request->getPost('endDate');
// 입력값 검증 (실제 프로덕션에서는 반드시 수행해야 함)
if (empty($startDate) || empty($endDate)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error',
'message' => '필수 파라미터(날짜 또는 집계 기준)가 누락되었습니다.',
]);
}
try {
// 모델 로드 및 데이터 조회
$collectorService = service('collectorservice');
$data = $collectorService->getAggregated($entity, $startDate, $endDate);
// 데이터를 JSON 형식으로 반환
return $this->response->setJSON([
'status' => 'success',
'data' => $data // 이 데이터는 이제 사용자가 제공한 필드 구조를 가집니다.
]);
} catch (\Exception $e) {
// 예외 처리
log_message('error', 'Traffic data aggregation error: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'status' => 'error',
'message' => '데이터 처리 중 서버 오류가 발생했습니다.',
]);
}
}
}

View File

@ -76,6 +76,10 @@ class CollectorService extends CommonService
$this->model->orLike($this->model->getTable() . "." . $this->model->getTitleField(), $word, 'both');
parent::setSearchWord($word);
}
//Chart용
public function getAggregated(TrafficEntity $trafficEntity, string $startDate, string $endDate) {
d
}
//SNMP연결
private function getSNMPOctets(TrafficEntity $trafficEntity, string $oid): ?int
{

View File

@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?? '대시보드' ?></title>
<!-- Bootstrap 5 CSS CDN 로드 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Chart.js CDN 로드 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
/* 커스텀 스타일 및 색상 변수 설정 */
:root {
--bs-primary: #0d6efd;
/* 기본 Bootstrap Primary */
--bs-success: #198754;
/* IN 트래픽 색상 (Chart.js) */
--bs-danger: #dc3545;
/* OUT 트래픽 색상 (Chart.js) */
}
body {
background-color: #f8f9fa;
font-family: 'Inter', sans-serif;
}
.dashboard-card {
transition: box-shadow 0.3s ease;
}
.dashboard-card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.aggregation-button.active {
background-color: var(--bs-primary);
color: white;
}
.aggregation-button {
transition: background-color 0.2s, box-shadow 0.2s;
}
.chart-container {
position: relative;
height: 400px;
/* 차트 높이 고정 */
}
</style>
</head>
<body class="p-4 p-md-5">
<div id="app" class="container-fluid" style="max-width: 1200px;">
<header class="text-center mb-5">
<h1 class="display-5 fw-bold text-dark mb-2"><?= $title ?? '트래픽 데이터 시각화 대시보드' ?></h1>
<p class="text-secondary">기간 집계 기준을 선택하여 트래픽 추이를 확인하세요.</p>
</header>
<!-- Configuration Card -->
<div class="card shadow-lg mb-5 dashboard-card">
<div class="card-body p-4 p-md-5">
<h2 class="h5 card-title text-dark mb-4">데이터 조회 설정</h2>
<div class="row g-3 align-items-end">
<!-- 1. Start Date Picker -->
<div class="col-12 col-md-3">
<label for="startDate" class="form-label text-muted">시작 날짜</label>
<input type="date" id="startDate" class="form-control" value="<?= date('Y-m-d', strtotime('-7 days')) ?>">
</div>
<!-- 2. End Date Picker -->
<div class="col-12 col-md-3">
<label for="endDate" class="form-label text-muted">종료 날짜</label>
<input type="date" id="endDate" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<!-- 3. Aggregation Selector -->
<div class="col-12 col-md-6">
<label class="form-label text-muted">집계 기준</label>
<div id="aggregationSelector" class="d-flex flex-wrap gap-2">
<button type="button" data-period="hourly" class="aggregation-button btn btn-outline-secondary btn-sm">시간당</button>
<button type="button" data-period="daily" class="aggregation-button btn btn-outline-secondary btn-sm active">일일</button>
<button type="button" data-period="weekly" class="aggregation-button btn btn-outline-secondary btn-sm">주간</button>
<button type="button" data-period="monthly" class="aggregation-button btn btn-outline-secondary btn-sm">월간</button>
</div>
</div>
</div>
<button onclick="fetchDataAndDrawChart()" id="viewChartBtn" class="mt-4 btn btn-primary btn-lg w-100 w-md-auto shadow-sm">
차트 보기
</button>
</div>
</div>
<!-- Chart Card -->
<div class="card shadow-lg dashboard-card">
<div class="card-body p-4 p-md-5">
<!-- 동적으로 업데이트될 메인 제목 영역 -->
<h2 id="chartTitle" class="h5 card-title text-dark mb-1">트래픽 속도 추이 (Kbit/s)</h2>
<!-- 동적으로 업데이트될 날짜 정보 영역 -->
<p id="dateRangeInfo" class="text-secondary mb-4"></p>
<div id="loadingIndicator" class="text-center p-5 hidden" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-primary fw-bold">데이터를 집계 중입니다... 잠시만 기다려 주세요.</p>
</div>
<div id="errorMessage" class="alert alert-danger text-center hidden" role="alert" style="display: none;">
데이터를 불러오는데 실패했습니다. (서버 오류)
</div>
<div class="chart-container">
<canvas id="trafficChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS Bundle 로드 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 전역 변수 설정
let trafficChart = null;
document.addEventListener('DOMContentLoaded', () => {
// 초기 집계 기준 설정 이벤트 리스너
document.querySelectorAll('.aggregation-button').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.aggregation-button').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
// 초기 로드 시 차트 그리기
fetchDataAndDrawChart();
});
// -----------------------------------------------------------
// 1. CI4 백엔드 엔드포인트를 호출하여 데이터 가져오기 (AJAX)
// -----------------------------------------------------------
async function fetchAggregatedData(startDate, endDate, period) {
// PHP 코드가 JavaScript 문자열 값으로 올바르게 평가되도록 수정
const endpoint = '<?= base_url("traffic/data") ?>'; // CI4 라우트로 지정된 엔드포인트
const formData = new FormData();
formData.append('startDate', startDate);
formData.append('endDate', endDate);
formData.append('period', period);
const maxRetries = 3;
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(endpoint, {
method: 'POST',
body: formData,
});
const result = await response.json();
if (!response.ok || result.status === 'error') {
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
}
return result.data; // 모델에서 반환된 데이터 배열
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i === maxRetries - 1) {
throw new Error("서버에서 데이터를 가져오는데 실패했습니다. 콘솔을 확인하세요.");
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // 지수 백오프
}
}
}
// -----------------------------------------------------------
// 2. 차트 그리기 및 업데이트 로직
// -----------------------------------------------------------
async function fetchDataAndDrawChart() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const period = document.querySelector('.aggregation-button.active').dataset.period;
const chartTitleElement = document.getElementById('chartTitle');
const dateRangeInfoElement = document.getElementById('dateRangeInfo'); // 날짜 정보 표시 영역
const loadingIndicator = document.getElementById('loadingIndicator');
const errorMessage = document.getElementById('errorMessage');
const canvas = document.getElementById('trafficChart');
const viewChartBtn = document.getElementById('viewChartBtn');
// UI 초기화 및 로딩 표시
loadingIndicator.style.display = 'block';
errorMessage.style.display = 'none';
canvas.style.opacity = '0';
viewChartBtn.disabled = true;
// 차트 제목 및 날짜 정보 업데이트
const periodMapKo = {
hourly: '시간당',
daily: '일일',
weekly: '주간',
monthly: '월간'
};
// 메인 제목: Kbit/s 정보만
chartTitleElement.textContent = `트래픽 속도 추이 (IN/OUT Kbit/s)`;
// 시작일, 마지막일 정보 표시
dateRangeInfoElement.textContent = `기간: ${startDate} 부터 ${endDate} 까지 (${periodMapKo[period]} 집계)`;
try {
// CI4 백엔드(Controller -> Model)에서 데이터 가져오기
const aggregatedData = await fetchAggregatedData(startDate, endDate, period);
// --- IN, OUT Kbit/s 데이터만 추출 ---
const labels = aggregatedData.map(d => d.created_at);
const inData = aggregatedData.map(d => d.in_kbits);
const outData = aggregatedData.map(d => d.out_kbits);
// ----------------------------------
drawChart(labels, inData, outData);
} catch (error) {
console.error("Chart data processing error:", error);
errorMessage.textContent = `데이터 로딩 실패: ${error.message}`;
errorMessage.style.display = 'block';
} finally {
loadingIndicator.style.display = 'none';
canvas.style.opacity = '1';
viewChartBtn.disabled = false;
}
}
function drawChart(labels, inData, outData) {
const ctx = document.getElementById('trafficChart').getContext('2d');
// 기존 차트가 있으면 파괴
if (trafficChart) {
trafficChart.destroy();
}
trafficChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'IN Kbit/s',
data: inData,
borderColor: '#198754', // Bootstrap Success Color (Green)
backgroundColor: 'rgba(25, 135, 84, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 6,
},
{
label: 'OUT Kbit/s',
data: outData,
borderColor: '#dc3545', // Bootstrap Danger Color (Red)
backgroundColor: 'rgba(220, 53, 69, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 6,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Kbit/s', // Y축 레이블에 Kbit/s 명시
font: {
weight: 'bold'
}
}
},
x: {
title: {
display: true,
text: '기간',
font: {
weight: 'bold'
}
},
ticks: {
autoSkip: true,
maxTicksLimit: 20
}
}
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 20
}
},
tooltip: {
mode: 'index',
intersect: false,
}
},
hover: {
mode: 'nearest',
intersect: true
}
}
});
}
</script>
</body>
</html>