로그 리스트 항목 생성
This commit is contained in:
209
app/Commands/NaverRetry.php
Normal file
209
app/Commands/NaverRetry.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,9 +147,23 @@ class NaverWorker extends BaseCommand
|
|||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
CLI::error("❌ Task Failed: " . $e->getMessage());
|
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로 업데이트 (필수) (재연결 처리 포함)
|
// 1. DB 상태를 FAIL로 업데이트 (필수) (재연결 처리 포함)
|
||||||
$this->safeUpdateLog($logModel, $logId, [
|
$this->safeUpdateLog($logModel, $logId, [
|
||||||
|
'atcl_no' => $atclNo,
|
||||||
'status' => 'FAIL',
|
'status' => 'FAIL',
|
||||||
'error_msg' => $e->getMessage()
|
'error_msg' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -697,3 +697,12 @@ $routes->post('/login/chkLogin', 'Login::chkLogin');
|
|||||||
if (is_file($filepath = APPPATH . 'Config/Routes/Api.php')) {
|
if (is_file($filepath = APPPATH . 'Config/Routes/Api.php')) {
|
||||||
require $filepath;
|
require $filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker 관리
|
||||||
|
*/
|
||||||
|
$routes->group('manage/worker', ['namespace' => 'App\Controllers\Manage'], function ($routes) {
|
||||||
|
$routes->get('failed', 'WorkerLogController::failedList');
|
||||||
|
$routes->post('retry', 'WorkerLogController::retrySelected');
|
||||||
|
$routes->get('detail/(:num)', 'WorkerLogController::detail/$1');
|
||||||
|
});
|
||||||
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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,6 +134,30 @@ class NaverApiClient
|
|||||||
return $this->request('POST', $url, $priceData);
|
return $this->request('POST', $url, $priceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현장확인 슬롯 동기화
|
||||||
|
* API (가) - /kiso/center/site-slot
|
||||||
|
* 신규/변경 일자별 데이터 전송
|
||||||
|
* @syncData array
|
||||||
|
* @param string $baseDate 기준일자(ISO 8601 형식: YYYY-MM-DD 예: 2024-01-31)
|
||||||
|
* @param string $legalDivisionNumber 구역번호 "1111000000"
|
||||||
|
* @param object slots 오전/오후/ 슬롯 정보
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"baseDate": "2025-04-30",
|
||||||
|
"legalDivisionNumber": "1111000000",
|
||||||
|
"slots": {
|
||||||
|
"am": { "max": 10, "reserved": 0 },
|
||||||
|
"pm": { "max": 100, "reserved": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
public function syncSiteSlot($syncData){
|
||||||
|
$url = $this->baseUrl . "/kiso/center/verification-site/slots";
|
||||||
|
return $this->request('POST', $url, $syncData);
|
||||||
|
}
|
||||||
|
|
||||||
public function submitSyncResult(string $reserveNoList): ?array
|
public function submitSyncResult(string $reserveNoList): ?array
|
||||||
{
|
{
|
||||||
$url = "{$this->baseUrl}/site/submitSyncResult.nhn";
|
$url = "{$this->baseUrl}/site/submitSyncResult.nhn";
|
||||||
@@ -457,31 +481,6 @@ class NaverApiClient
|
|||||||
return $this->request('POST', $url, $postData);
|
return $this->request('POST', $url, $postData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 현장확인 슬롯 동기화
|
|
||||||
* API (가) - /kiso/center/site-slot
|
|
||||||
* 신규/변경 일자별 데이터 전송
|
|
||||||
* @syncData array
|
|
||||||
* @param string $baseDate 기준일자(ISO 8601 형식: YYYY-MM-DD 예: 2024-01-31)
|
|
||||||
* @param string $legalDivisionNumber 구역번호 "1111000000"
|
|
||||||
* @param object slots 오전/오후/ 슬롯 정보
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"baseDate": "2025-04-30",
|
|
||||||
"legalDivisionNumber": "1111000000",
|
|
||||||
"slots": {
|
|
||||||
"am": { "max": 10, "reserved": 0 },
|
|
||||||
"pm": { "max": 100, "reserved": 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
public function syncSiteSlot($syncData){
|
|
||||||
$url = $this->baseUrl . "/kiso/center/verification-site/slots";
|
|
||||||
return $this->request('POST', $url, $syncData);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CURL 공통 실행 함수
|
* CURL 공통 실행 함수
|
||||||
*/
|
*/
|
||||||
|
|||||||
410
app/Views/manage/worker_log/failed_list.php
Normal file
410
app/Views/manage/worker_log/failed_list.php
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
<?= $this->extend('layouts/main') ?>
|
||||||
|
|
||||||
|
<?= $this->section('page_styles') ?>
|
||||||
|
<style>
|
||||||
|
.stats-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.stats-card h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.stats-card .number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.stats-card.total { background: #f8f9fa; border-left: 4px solid #6c757d; }
|
||||||
|
.stats-card.retryable { background: #d1ecf1; border-left: 4px solid #17a2b8; }
|
||||||
|
.stats-card.exhausted { background: #f8d7da; border-left: 4px solid #dc3545; }
|
||||||
|
|
||||||
|
.error-type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.severity-high { background: #dc3545; color: white; }
|
||||||
|
.severity-medium { background: #ffc107; color: #000; }
|
||||||
|
.severity-low { background: #28a745; color: white; }
|
||||||
|
|
||||||
|
.solution-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.solution-box h5 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
.solution-box ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.log-table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.log-table .error-msg {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.log-table .error-msg:hover {
|
||||||
|
overflow: visible;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-retry-count {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-retry-selected {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
<div class="app-page-title">
|
||||||
|
<div class="page-title-wrapper">
|
||||||
|
<div class="page-title-heading">
|
||||||
|
<div class="page-title-icon">
|
||||||
|
<i class="pe-7s-attention icon-gradient bg-mean-fruit"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Worker 실패 로그 관리
|
||||||
|
<div class="page-title-subheading">
|
||||||
|
처리 실패한 작업을 분석하고 재처리합니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 카드 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stats-card total">
|
||||||
|
<h3>전체 실패 건수</h3>
|
||||||
|
<p class="number"><?= number_format($stats['total_fail']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stats-card retryable">
|
||||||
|
<h3>재시도 가능</h3>
|
||||||
|
<p class="number"><?= number_format($stats['retry_available']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stats-card exhausted">
|
||||||
|
<h3>재시도 횟수 초과</h3>
|
||||||
|
<p class="number"><?= number_format($stats['retry_exhausted']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 오류 유형별 분석 -->
|
||||||
|
<div class="main-card mb-3 card">
|
||||||
|
<div class="card-header">오류 유형별 분석</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<?php foreach ($errorTypes as $key => $type): ?>
|
||||||
|
<?php if ($type['count'] > 0): ?>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<span class="error-type-badge severity-<?= $type['severity'] ?>">
|
||||||
|
<?= $type['label'] ?>
|
||||||
|
</span>
|
||||||
|
<strong><?= number_format($type['count']) ?>건</strong>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 실패 로그 목록 -->
|
||||||
|
<div class="main-card mb-3 card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>최근 실패 로그 (50건)</span>
|
||||||
|
<button class="btn btn-primary btn-sm" id="retrySelected" disabled>
|
||||||
|
<i class="fa fa-refresh"></i> 선택 항목 재처리
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover log-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="40">
|
||||||
|
<input type="checkbox" id="selectAll">
|
||||||
|
</th>
|
||||||
|
<th width="80">ID</th>
|
||||||
|
<th width="120">매물번호</th>
|
||||||
|
<th width="100">오류 유형</th>
|
||||||
|
<th>오류 메시지</th>
|
||||||
|
<th width="80">재시도</th>
|
||||||
|
<th width="150">발생시각</th>
|
||||||
|
<th width="100">액션</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?php if ($log['can_retry']): ?>
|
||||||
|
<input type="checkbox" class="log-checkbox" value="<?= $log['seq'] ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<i class="fa fa-ban text-muted" title="재시도 불가"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= $log['seq'] ?></td>
|
||||||
|
<td><?= $log['atcl_no'] ?? '-' ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$typeInfo = $errorTypes[$log['error_type']] ?? ['label' => '기타', 'severity' => 'low'];
|
||||||
|
?>
|
||||||
|
<span class="error-type-badge severity-<?= $typeInfo['severity'] ?>">
|
||||||
|
<?= $typeInfo['label'] ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="error-msg" title="<?= esc($log['error_msg']) ?>">
|
||||||
|
<?= esc($log['error_msg']) ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?php if ($log['retry_cnt'] > 0): ?>
|
||||||
|
<span class="badge-retry-count"><?= $log['retry_cnt'] ?>회</span>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= date('m-d H:i', strtotime($log['created_at'])) ?></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-info view-detail" data-id="<?= $log['seq'] ?>">
|
||||||
|
상세
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if (empty($logs)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-muted py-4">
|
||||||
|
실패한 로그가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('modals') ?>
|
||||||
|
<!-- 상세 모달 -->
|
||||||
|
<div class="modal fade" id="detailModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">로그 상세 정보</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="detailContent">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border" role="status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="retryOne" style="display:none;">
|
||||||
|
<i class="fa fa-refresh"></i> 재처리
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('page_scripts') ?>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 전체 선택
|
||||||
|
$('#selectAll').on('change', function() {
|
||||||
|
$('.log-checkbox').prop('checked', this.checked);
|
||||||
|
updateRetryButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 개별 선택
|
||||||
|
$('.log-checkbox').on('change', function() {
|
||||||
|
updateRetryButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 재처리 버튼 활성화/비활성화
|
||||||
|
function updateRetryButton() {
|
||||||
|
const checked = $('.log-checkbox:checked').length;
|
||||||
|
$('#retrySelected').prop('disabled', checked === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 항목 재처리
|
||||||
|
$('#retrySelected').on('click', function() {
|
||||||
|
const logIds = $('.log-checkbox:checked').map(function() {
|
||||||
|
return $(this).val();
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
if (logIds.length === 0) {
|
||||||
|
Swal.fire('알림', '재처리할 항목을 선택해주세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: '재처리 확인',
|
||||||
|
text: `선택한 ${logIds.length}건을 재처리하시겠습니까?`,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '재처리',
|
||||||
|
cancelButtonText: '취소'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
retryLogs(logIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 재처리 실행
|
||||||
|
function retryLogs(logIds) {
|
||||||
|
const btn = $('#retrySelected');
|
||||||
|
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 처리 중...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '<?= base_url('manage/worker/retry') ?>',
|
||||||
|
method: 'POST',
|
||||||
|
data: { log_ids: logIds },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
const results = response.results;
|
||||||
|
let message = `성공: ${results.success}건, 실패: ${results.fail}건\n\n`;
|
||||||
|
|
||||||
|
results.details.forEach(detail => {
|
||||||
|
const icon = detail.status === 'success' ? '✅' :
|
||||||
|
detail.status === 'skip' ? '⏭' : '❌';
|
||||||
|
message += `${icon} [${detail.atcl_no}] ${detail.message}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: '재처리 완료',
|
||||||
|
text: message,
|
||||||
|
icon: results.success > 0 ? 'success' : 'warning',
|
||||||
|
preConfirm: () => location.reload()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire('오류', response.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire('오류', '재처리 중 오류가 발생했습니다.', 'error');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> 선택 항목 재처리');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상세 보기
|
||||||
|
$('.view-detail').on('click', function() {
|
||||||
|
const logId = $(this).data('id');
|
||||||
|
$('#detailContent').html('<div class="text-center py-4"><div class="spinner-border"></div></div>');
|
||||||
|
$('#detailModal').modal('show');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `<?= base_url('manage/worker/detail') ?>/${logId}`,
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
displayDetail(response.log);
|
||||||
|
} else {
|
||||||
|
$('#detailContent').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#detailContent').html('<div class="alert alert-danger">로그를 불러오는 중 오류가 발생했습니다.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 상세 정보 표시
|
||||||
|
function displayDetail(log) {
|
||||||
|
const solution = log.solution;
|
||||||
|
const errorType = log.error_type;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>로그 ID:</strong> ${log.seq}<br>
|
||||||
|
<strong>매물번호:</strong> ${log.atcl_no || '-'}<br>
|
||||||
|
<strong>상태:</strong> <span class="badge badge-danger">${log.status}</span><br>
|
||||||
|
<strong>재시도 횟수:</strong> ${log.retry_cnt || 0}회<br>
|
||||||
|
<strong>발생시각:</strong> ${log.created_at}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>오류 메시지:</strong>
|
||||||
|
<div class="alert alert-danger">${log.error_msg}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solution-box mb-3">
|
||||||
|
<h5>${solution.title}</h5>
|
||||||
|
<ul>
|
||||||
|
${solution.steps.map(step => `<li>${step}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>원본 Payload:</strong>
|
||||||
|
<pre class="bg-light p-3" style="max-height: 300px; overflow-y: auto;">${JSON.stringify(log.parsed_payload, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('#detailContent').html(html);
|
||||||
|
|
||||||
|
// 재처리 버튼 표시
|
||||||
|
if (log.can_retry) {
|
||||||
|
$('#retryOne').show().off('click').on('click', function() {
|
||||||
|
$('#detailModal').modal('hide');
|
||||||
|
retryLogs([log.seq]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$('#retryOne').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
Reference in New Issue
Block a user