로그 리스트 항목 생성
This commit is contained in:
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