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 ]); } }