로그 리스트 항목 생성

This commit is contained in:
2026-03-26 11:36:21 +09:00
parent 0b6ed3df73
commit c22b023310
6 changed files with 1032 additions and 25 deletions

View File

@@ -0,0 +1,366 @@
<?php
namespace App\Controllers\Manage;
use App\Controllers\BaseController;
use App\Models\Entities\NaverWorkerLogModel;
class WorkerLogController extends BaseController
{
protected $logModel;
public function __construct()
{
$this->logModel = new NaverWorkerLogModel();
}
/**
* 실패 로그 목록 (오류 유형별 분석)
*/
public function failedList()
{
$db = \Config\Database::connect();
// 1. 전체 통계
$stats = [
'total_fail' => $this->logModel->where('status', 'FAIL')->countAllResults(false),
'retry_available' => $this->logModel
->where('status', 'FAIL')
->groupStart()
->where('retry_cnt IS NULL')
->orWhere('retry_cnt <', 3)
->groupEnd()
->countAllResults(false),
'retry_exhausted' => $this->logModel
->where('status', 'FAIL')
->where('retry_cnt >=', 3)
->countAllResults(false),
];
// 2. 오류 유형별 분류
$errorTypes = $this->classifyErrors();
// 3. 최근 실패 목록
$recentFails = $this->logModel
->select('seq, atcl_no, status, error_msg, retry_cnt, created_at')
->where('status', 'FAIL')
->orderBy('created_at', 'DESC')
->findAll(50);
// 각 오류에 대한 해결 가이드 추가
foreach ($recentFails as &$log) {
$log['error_type'] = $this->detectErrorType($log['error_msg']);
$log['solution'] = $this->getSolution($log['error_type']);
$log['can_retry'] = $this->canRetry($log);
}
$data = [
'stats' => $stats,
'errorTypes' => $errorTypes,
'logs' => $recentFails
];
return view('manage/worker_log/failed_list', array_merge($this->data, $data));
}
/**
* 오류 유형 분류
*/
protected function classifyErrors()
{
$db = \Config\Database::connect();
$errorPatterns = [
'foreign_key' => [
'pattern' => 'foreign key constraint',
'label' => 'FK 제약조건 위반',
'severity' => 'high',
'count' => 0
],
'duplicate' => [
'pattern' => 'Duplicate entry',
'label' => '중복 데이터',
'severity' => 'medium',
'count' => 0
],
'missing_user' => [
'pattern' => 'usr_sq.*users 테이블에 없음',
'label' => '담당자 정보 없음',
'severity' => 'medium',
'count' => 0
],
'empty_payload' => [
'pattern' => '빈 페이로드',
'label' => '빈 데이터',
'severity' => 'low',
'count' => 0
],
'api_error' => [
'pattern' => 'API.*FAIL|HTTP.*40[0-9]|HTTP.*50[0-9]',
'label' => 'API 통신 오류',
'severity' => 'medium',
'count' => 0
],
'unknown' => [
'pattern' => '',
'label' => '기타 오류',
'severity' => 'low',
'count' => 0
]
];
$failedLogs = $this->logModel
->select('error_msg')
->where('status', 'FAIL')
->findAll();
foreach ($failedLogs as $log) {
$classified = false;
foreach ($errorPatterns as $key => &$pattern) {
if ($key === 'unknown') continue;
if ($pattern['pattern'] && preg_match('/' . $pattern['pattern'] . '/i', $log['error_msg'])) {
$pattern['count']++;
$classified = true;
break;
}
}
if (!$classified) {
$errorPatterns['unknown']['count']++;
}
}
return $errorPatterns;
}
/**
* 오류 유형 감지
*/
protected function detectErrorType($errorMsg)
{
if (!$errorMsg) return 'unknown';
if (stripos($errorMsg, 'foreign key constraint') !== false) {
return 'foreign_key';
}
if (stripos($errorMsg, 'Duplicate entry') !== false) {
return 'duplicate';
}
if (stripos($errorMsg, 'usr_sq') !== false && stripos($errorMsg, 'users 테이블') !== false) {
return 'missing_user';
}
if (stripos($errorMsg, '빈 페이로드') !== false) {
return 'empty_payload';
}
if (preg_match('/API.*FAIL|HTTP.*40[0-9]|HTTP.*50[0-9]/i', $errorMsg)) {
return 'api_error';
}
return 'unknown';
}
/**
* 오류 유형별 해결 방법
*/
protected function getSolution($errorType)
{
$solutions = [
'foreign_key' => [
'title' => 'DB 데이터 수정 필요',
'steps' => [
'1. 참조하는 테이블의 데이터가 존재하는지 확인',
'2. region 테이블의 orphaned usr_sq 값 수정',
'3. TypeSParameterMapper의 폴백 로직 확인'
],
'auto_retry' => false
],
'duplicate' => [
'title' => '중복 데이터 처리',
'steps' => [
'1. 기존 데이터 확인 (atcl_no로 검색)',
'2. 중복 원인 파악 (동일 요청 2회 수신?)',
'3. 필요시 기존 데이터 삭제 후 재처리'
],
'auto_retry' => false
],
'missing_user' => [
'title' => '담당자 정보 수정됨 - 재처리 가능',
'steps' => [
'✅ TypeSParameterMapper가 자동 폴백 적용됨',
'✅ 바로 재처리 가능 (usr_sq=1로 처리됨)'
],
'auto_retry' => true
],
'empty_payload' => [
'title' => '잘못된 요청 데이터',
'steps' => [
'1. raw_payload 확인',
'2. 네이버 API 응답 확인',
'3. 재처리 불가 - 데이터 삭제 권장'
],
'auto_retry' => false
],
'api_error' => [
'title' => 'API 통신 오류',
'steps' => [
'1. 일시적 오류인지 확인 (네트워크, 타임아웃)',
'2. 네이버 API 서버 상태 확인',
'3. 일시적 오류면 재처리 가능'
],
'auto_retry' => true
],
'unknown' => [
'title' => '원인 미상',
'steps' => [
'1. error_msg 상세 확인',
'2. 로그 파일 확인',
'3. 개발팀 검토 필요'
],
'auto_retry' => false
]
];
return $solutions[$errorType] ?? $solutions['unknown'];
}
/**
* 재시도 가능 여부
*/
protected function canRetry($log)
{
// 재시도 횟수 체크
if (($log['retry_cnt'] ?? 0) >= 3) {
return false;
}
// 오류 유형별 재시도 가능 여부
$errorType = $this->detectErrorType($log['error_msg']);
$solution = $this->getSolution($errorType);
return $solution['auto_retry'] ?? false;
}
/**
* 선택한 로그 재처리 (AJAX)
*/
public function retrySelected()
{
$logIds = $this->request->getPost('log_ids');
if (empty($logIds) || !is_array($logIds)) {
return $this->response->setJSON([
'success' => false,
'message' => '재처리할 로그를 선택해주세요.'
]);
}
$naverService = new \App\Services\NaverService();
$results = [
'success' => 0,
'fail' => 0,
'details' => []
];
foreach ($logIds as $logId) {
$log = $this->logModel->find($logId);
if (!$log) {
$results['details'][] = [
'id' => $logId,
'status' => 'error',
'message' => '로그를 찾을 수 없습니다.'
];
continue;
}
// 재시도 가능 여부 체크
if (!$this->canRetry($log)) {
$results['details'][] = [
'id' => $logId,
'atcl_no' => $log['atcl_no'],
'status' => 'skip',
'message' => '재시도 불가능 (횟수 초과 또는 수정 필요)'
];
continue;
}
try {
// 재처리
$this->logModel->update($logId, ['status' => 'RETRY']);
$responseJson = json_decode($log['raw_payload'], true);
$payload = $responseJson['request_data'] ?? [];
if (empty($payload)) {
throw new \Exception("빈 페이로드 데이터");
}
$insertId = $naverService->processArticle($payload);
$this->logModel->update($logId, [
'atcl_no' => $payload['articleNumber'] ?? null,
'status' => 'SUCCESS',
'target_db_id' => $insertId,
'error_msg' => null,
'retry_cnt' => ($log['retry_cnt'] ?? 0) + 1
]);
$results['success']++;
$results['details'][] = [
'id' => $logId,
'atcl_no' => $log['atcl_no'],
'status' => 'success',
'message' => "성공 (DB ID: {$insertId})"
];
} catch (\Exception $e) {
$this->logModel->update($logId, [
'status' => 'FAIL',
'error_msg' => $e->getMessage(),
'retry_cnt' => ($log['retry_cnt'] ?? 0) + 1
]);
$results['fail']++;
$results['details'][] = [
'id' => $logId,
'atcl_no' => $log['atcl_no'],
'status' => 'fail',
'message' => $e->getMessage()
];
}
}
return $this->response->setJSON([
'success' => true,
'results' => $results
]);
}
/**
* 특정 로그 상세 (AJAX)
*/
public function detail($id)
{
$log = $this->logModel->find($id);
if (!$log) {
return $this->response->setJSON([
'success' => false,
'message' => '로그를 찾을 수 없습니다.'
]);
}
// raw_payload JSON 파싱
$log['parsed_payload'] = json_decode($log['raw_payload'], true);
$log['error_type'] = $this->detectErrorType($log['error_msg']);
$log['solution'] = $this->getSolution($log['error_type']);
$log['can_retry'] = $this->canRetry($log);
return $this->response->setJSON([
'success' => true,
'log' => $log
]);
}
}