로그 리스트 항목 생성

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

View 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>&times;</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() ?>