diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 903ee9f..e46662a 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -661,6 +661,12 @@ $routes->group('manage', ['namespace' => 'App\Controllers\Manage'], function ($r
/** API - 로그인로그관리 */
$routes->get('loginlog/getLogList', 'LoginLog::getLogList');
$routes->get('loginlog/excel', 'LoginLog::excel');
+
+ /** Worker 로그 관리 */
+ $routes->get('workerlog', 'WorkerLog::index');
+ $routes->get('workerlog/stream', 'WorkerLog::stream');
+ $routes->get('workerlog/download', 'WorkerLog::download');
+ $routes->post('workerlog/delete', 'WorkerLog::delete');
});
/**
diff --git a/app/Controllers/Manage/WorkerLog.php b/app/Controllers/Manage/WorkerLog.php
new file mode 100644
index 0000000..31b504c
--- /dev/null
+++ b/app/Controllers/Manage/WorkerLog.php
@@ -0,0 +1,221 @@
+ ROOTPATH . 'worker/logs',
+ 'naver_worker' => WRITEPATH . 'logs/worker'
+ ];
+
+ // 날짜 필터 (기본값: 오늘)
+ $date = $this->request->getGet('date') ?? date('Y-m-d');
+ $logType = $this->request->getGet('type') ?? 'all';
+
+ $logs = [];
+
+ // API Receiver 로그 읽기
+ if ($logType === 'all' || $logType === 'api_receiver') {
+ $apiLogFile = $logDirs['api_receiver'] . '/' . $date . '.log';
+ if (file_exists($apiLogFile)) {
+ $logs['api_receiver'] = $this->parseLogFile($apiLogFile);
+ }
+ }
+
+ // Naver Worker 로그 읽기
+ if ($logType === 'all' || $logType === 'naver_worker') {
+ $workerLogFile = $logDirs['naver_worker'] . '/' . $date . '.log';
+ if (file_exists($workerLogFile)) {
+ $logs['naver_worker'] = $this->parseLogFile($workerLogFile);
+ }
+
+ // Failed 로그도 읽기
+ $failedLogFile = $logDirs['naver_worker'] . '/' . $date . '_failed.log';
+ if (file_exists($failedLogFile)) {
+ $logs['naver_worker_failed'] = $this->parseLogFile($failedLogFile);
+ }
+ }
+
+ // 모든 로그를 시간순으로 통합 정렬
+ $allLogs = [];
+ foreach ($logs as $type => $entries) {
+ foreach ($entries as $entry) {
+ $entry['source'] = $type;
+ $allLogs[] = $entry;
+ }
+ }
+
+ // 시간 역순 정렬 (최신순)
+ usort($allLogs, function($a, $b) {
+ return strtotime($b['timestamp']) - strtotime($a['timestamp']);
+ });
+
+ $data['logs'] = $allLogs;
+ $data['date'] = $date;
+ $data['logType'] = $logType;
+ $data['logDirs'] = $logDirs;
+
+ // 사용 가능한 날짜 목록 (최근 30일)
+ $data['availableDates'] = $this->getAvailableLogDates($logDirs);
+
+ return view('pages/manage/worker_log', $data);
+ }
+
+ /**
+ * 로그 파일 파싱
+ */
+ private function parseLogFile($filePath)
+ {
+ $logs = [];
+ $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+
+ foreach ($lines as $line) {
+ // 로그 포맷: [2025-12-22 10:30:45] [INFO] [NaverWorker::run:95] 메시지
+ if (preg_match('/\[(.+?)\]\s*\[(.+?)\]\s*(?:\[(.+?)\]\s*)?(.+)/', $line, $matches)) {
+ $logs[] = [
+ 'timestamp' => $matches[1],
+ 'level' => $matches[2],
+ 'location' => $matches[3] ?? '',
+ 'message' => $matches[4]
+ ];
+ } else {
+ // 파싱 실패한 경우 원본 그대로
+ $logs[] = [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'level' => 'UNKNOWN',
+ 'location' => '',
+ 'message' => $line
+ ];
+ }
+ }
+
+ return $logs;
+ }
+
+ /**
+ * 로그 파일이 존재하는 날짜 목록 가져오기
+ */
+ private function getAvailableLogDates($logDirs)
+ {
+ $dates = [];
+
+ foreach ($logDirs as $dir) {
+ if (!is_dir($dir)) continue;
+
+ $files = scandir($dir);
+ foreach ($files as $file) {
+ if (preg_match('/(\d{4}-\d{2}-\d{2})(?:_failed)?\.log$/', $file, $matches)) {
+ $dates[$matches[1]] = true;
+ }
+ }
+ }
+
+ $dates = array_keys($dates);
+ rsort($dates); // 최신순 정렬
+
+ return array_slice($dates, 0, 30); // 최근 30일만
+ }
+
+ /**
+ * 실시간 로그 스트리밍 (Ajax)
+ */
+ public function stream()
+ {
+ $type = $this->request->getGet('type') ?? 'naver_worker';
+ $lastId = (int) ($this->request->getGet('lastId') ?? 0);
+
+ $logDirs = [
+ 'api_receiver' => ROOTPATH . 'worker/logs',
+ 'naver_worker' => WRITEPATH . 'logs/worker'
+ ];
+
+ $logFile = $logDirs[$type] . '/' . date('Y-m-d') . '.log';
+
+ $newLogs = [];
+ if (file_exists($logFile)) {
+ $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+
+ // lastId 이후의 로그만 반환
+ if ($lastId < count($lines)) {
+ $newLines = array_slice($lines, $lastId);
+
+ foreach ($newLines as $line) {
+ if (preg_match('/\[(.+?)\]\s*\[(.+?)\]\s*(?:\[(.+?)\]\s*)?(.+)/', $line, $matches)) {
+ $newLogs[] = [
+ 'timestamp' => $matches[1],
+ 'level' => $matches[2],
+ 'location' => $matches[3] ?? '',
+ 'message' => $matches[4]
+ ];
+ }
+ }
+ }
+ }
+
+ return $this->response->setJSON([
+ 'success' => true,
+ 'logs' => $newLogs,
+ 'lastId' => $lastId + count($newLogs)
+ ]);
+ }
+
+ /**
+ * 로그 파일 다운로드
+ */
+ public function download()
+ {
+ $date = $this->request->getGet('date') ?? date('Y-m-d');
+ $type = $this->request->getGet('type') ?? 'naver_worker';
+
+ $logDirs = [
+ 'api_receiver' => ROOTPATH . 'worker/logs',
+ 'naver_worker' => WRITEPATH . 'logs/worker'
+ ];
+
+ $logFile = $logDirs[$type] . '/' . $date . '.log';
+
+ if (!file_exists($logFile)) {
+ return $this->response->setStatusCode(404)->setBody('로그 파일을 찾을 수 없습니다.');
+ }
+
+ return $this->response->download($logFile, null);
+ }
+
+ /**
+ * 로그 파일 삭제
+ */
+ public function delete()
+ {
+ $date = $this->request->getPost('date');
+ $type = $this->request->getPost('type');
+
+ if (!$date || !$type) {
+ return $this->response->setJSON(['success' => false, 'message' => '필수 파라미터 누락']);
+ }
+
+ $logDirs = [
+ 'api_receiver' => ROOTPATH . 'worker/logs',
+ 'naver_worker' => WRITEPATH . 'logs/worker'
+ ];
+
+ $logFile = $logDirs[$type] . '/' . $date . '.log';
+
+ if (file_exists($logFile)) {
+ unlink($logFile);
+ return $this->response->setJSON(['success' => true, 'message' => '로그 파일이 삭제되었습니다.']);
+ }
+
+ return $this->response->setJSON(['success' => false, 'message' => '로그 파일을 찾을 수 없습니다.']);
+ }
+}
diff --git a/app/Views/pages/manage/worker_log.php b/app/Views/pages/manage/worker_log.php
new file mode 100644
index 0000000..a3aa2ca
--- /dev/null
+++ b/app/Views/pages/manage/worker_log.php
@@ -0,0 +1,284 @@
+= $this->extend('layouts/base') ?>
+
+= $this->section('content') ?>
+
+
+
+
+
+
+
+ Worker 로그 통합 관리
+
+ API Receiver 및 Worker 서비스의 로그를 한 곳에서 확인할 수 있습니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = count($logs) ?>
+
+
+
+
+
+
+
+
+
+
+
+ = count(array_filter($logs, fn($l) => $l['level'] === 'INFO')) ?>
+
+
+
+
+
+
+
+
+
+
+
+ = count(array_filter($logs, fn($l) => $l['level'] === 'ERROR')) ?>
+
+
+
+
+
+
+
+
+
+
+
+ = count(array_filter($logs, fn($l) => $l['level'] === 'DEBUG')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ | 시간 |
+ 레벨 |
+ 소스 |
+ 위치 |
+ 메시지 |
+
+
+
+
+
+ | 로그가 없습니다. |
+
+
+
+
+ | = esc($log['timestamp']) ?> |
+
+
+ = esc($log['level']) ?>
+
+ |
+
+
+ 'API Receiver',
+ 'naver_worker' => 'Worker',
+ 'naver_worker_failed' => 'Worker (Failed)'
+ ];
+ echo $sourceLabel[$log['source']] ?? $log['source'];
+ ?>
+
+ |
+ = esc($log['location']) ?> |
+
+
+ = esc($log['message']) ?>
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+= $this->endSection() ?>