로그 리스트 항목 생성
This commit is contained in:
366
app/Controllers/Manage/WorkerLogController.php
Normal file
366
app/Controllers/Manage/WorkerLogController.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user