로그 리스트 항목 생성

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

209
app/Commands/NaverRetry.php Normal file
View File

@@ -0,0 +1,209 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use App\Models\Entities\NaverWorkerLogModel;
class NaverRetry extends BaseCommand
{
protected $group = 'Workers';
protected $name = 'naver:retry';
protected $description = '실패한 Naver Worker 작업을 재처리합니다.';
protected $usage = 'naver:retry [log_id] [options]';
protected $arguments = [
'log_id' => '재처리할 특정 로그 ID (선택사항, 없으면 모든 실패 건 재처리)'
];
protected $options = [
'--limit' => '재처리할 최대 개수 (기본: 10)',
'--dry-run' => '실제 실행 없이 목록만 확인',
'--force' => '재시도 횟수 제한 무시 (3회 이상 실패한 건도 재처리)'
];
public function run(array $params)
{
helper(['log']);
$logId = $params[0] ?? null;
$limit = CLI::getOption('limit') ?? 10;
$isDryRun = CLI::getOption('dry-run') !== null;
$isForce = CLI::getOption('force') !== null;
$logModel = model(NaverWorkerLogModel::class);
$naverService = new \App\Services\NaverService();
// 1. 특정 ID 재처리
if ($logId) {
$this->retryOne($logModel, $naverService, $logId, $isDryRun, $isForce);
return;
}
// 2. 실패한 모든 작업 재처리
$this->retryFailed($logModel, $naverService, $limit, $isDryRun, $isForce);
}
/**
* 특정 로그 ID 재처리
*/
protected function retryOne($logModel, $naverService, $logId, $isDryRun, $isForce)
{
$log = $logModel->find($logId);
if (!$log) {
CLI::error("❌ Log ID {$logId} not found");
return;
}
CLI::write(CLI::color("📋 Log ID: {$logId}", 'cyan'));
CLI::write(" Status: {$log['status']}");
CLI::write(" Retry Count: " . ($log['retry_cnt'] ?? 0));
CLI::write(" Error: {$log['error_msg']}");
CLI::write(" Created: {$log['created_at']}");
// 재시도 횟수 체크
if (!$isForce && ($log['retry_cnt'] ?? 0) >= 3) {
CLI::write(CLI::color('⚠️ 이미 3회 이상 재시도했습니다. --force 옵션으로 강제 실행 가능', 'yellow'));
return;
}
// 재시도 불가능한 오류 체크
if (!$isForce && $this->isNonRetryableError($log['error_msg'])) {
CLI::write(CLI::color('⚠️ 재시도 불가능한 오류입니다. 데이터를 먼저 수정해주세요.', 'yellow'));
CLI::write(CLI::color(' (--force 옵션으로 강제 실행 가능)', 'yellow'));
return;
}
if ($isDryRun) {
CLI::write(CLI::color('🔍 DRY RUN - 재처리하지 않음', 'yellow'));
return;
}
$this->processLog($logModel, $naverService, $log);
}
/**
* 재시도 불가능한 오류인지 확인
*/
protected function isNonRetryableError($errorMsg)
{
if (!$errorMsg) return false;
// 데이터 수정이 필요한 오류 패턴
$nonRetryablePatterns = [
'foreign key constraint',
'Duplicate entry',
'빈 페이로드',
'usr_sq.*users 테이블에 없음', // 이미 폴백 적용된 건은 재시도 가능
];
foreach ($nonRetryablePatterns as $pattern) {
if (stripos($errorMsg, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* 실패한 모든 작업 재처리
*/
protected function retryFailed($logModel, $naverService, $limit, $isDryRun, $isForce)
{
$query = $logModel->where('status', 'FAIL');
// force가 아닌 경우 재시도 횟수 3회 미만만 조회
if (!$isForce) {
$query->groupStart()
->where('retry_cnt IS NULL')
->orWhere('retry_cnt <', 3)
->groupEnd();
}
$failedLogs = $query->orderBy('created_at', 'ASC')->findAll($limit);
if (empty($failedLogs)) {
CLI::write(CLI::color('✅ 재처리할 실패 작업이 없습니다.', 'green'));
return;
}
CLI::write(CLI::color("📋 실패한 작업 {count}건 발견", 'cyan')
->replace('{count}', count($failedLogs)));
if ($isDryRun) {
foreach ($failedLogs as $log) {
CLI::write(" - ID: {$log['seq']} | Atcl: {$log['atcl_no']} | Error: {$log['error_msg']}");
}
CLI::write(CLI::color('🔍 DRY RUN - 재처리하지 않음', 'yellow'));
return;
}
// 실제 재처리
$successCount = 0;
$failCount = 0;
foreach ($failedLogs as $log) {
CLI::write(CLI::color("\n🔄 Retrying Log ID: {$log['seq']}", 'yellow'));
$result = $this->processLog($logModel, $naverService, $log);
if ($result) {
$successCount++;
} else {
$failCount++;
}
// 과부하 방지
sleep(1);
}
CLI::write(CLI::color("\n✅ 재처리 완료: 성공 {$successCount}건, 실패 {$failCount}", 'green'));
}
/**
* 로그 데이터 재처리
*/
protected function processLog($logModel, $naverService, $log)
{
try {
// 상태를 RETRY로 변경
$logModel->update($log['seq'], ['status' => 'RETRY']);
// JSON 파싱
$responseJson = json_decode($log['raw_payload'], true);
$payload = $responseJson['request_data'] ?? [];
if (empty($payload)) {
throw new \Exception("빈 페이로드 데이터");
}
CLI::write(" Article: {$payload['articleNumber']}");
// 재처리
$insertId = $naverService->processArticle($payload);
// 성공 시 로그 업데이트
$logModel->update($log['seq'], [
'atcl_no' => $payload['articleNumber'] ?? null,
'status' => 'SUCCESS',
'target_db_id' => $insertId,
'error_msg' => null
]);
CLI::write(CLI::color(" ✅ Success! DB ID: {$insertId}", 'green'));
return true;
} catch (\Exception $e) {
// 실패 시 로그 업데이트
$logModel->update($log['seq'], [
'status' => 'FAIL',
'error_msg' => $e->getMessage()
]);
CLI::error(" ❌ Failed: " . $e->getMessage());
return false;
}
}
}

View File

@@ -147,9 +147,23 @@ class NaverWorker extends BaseCommand
} catch (\Exception $e) {
CLI::error("❌ Task Failed: " . $e->getMessage());
// payload에서 매물번호 추출 시도
$atclNo = null;
try {
if (!empty($rawData)) {
$responseJson = json_decode($rawData, true);
$payload = $responseJson['request_data'] ?? [];
$atclNo = $payload['articleNumber'] ?? null;
}
} catch (\Exception $parseEx) {
// JSON 파싱 실패는 무시
}
// 실패 로그는 여기서 남김
// 1. DB 상태를 FAIL로 업데이트 (필수) (재연결 처리 포함)
$this->safeUpdateLog($logModel, $logId, [
'atcl_no' => $atclNo,
'status' => 'FAIL',
'error_msg' => $e->getMessage()
]);