feat(tkpurchase): 업무현황 다건 입력 + 작업자 시간 추적 + 종합 페이지

- DB: 유니크 제약 제거, report_seq 컬럼, work_report_workers 테이블
- API: 트랜잭션 기반 다건 생성/수정, 작업자 CRUD, 요약/엑셀 엔드포인트
- 협력업체 포탈: 다건 보고 UI, 작업자+시간 입력(자동완성), 수정 기능
- 업무현황 페이지: 보고순번/작업자 상세 표시
- 종합 페이지(NEW): 업체별/프로젝트별 취합, 엑셀 추출

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 09:43:33 +09:00
parent 48994cff1f
commit 976e55d672
14 changed files with 881 additions and 93 deletions

View File

@@ -34,14 +34,14 @@ async function loadReports() {
renderReportTable(r.data || [], r.total || 0);
} catch(e) {
console.warn('Report load error:', e);
document.getElementById('reportTableBody').innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">로딩 실패</td></tr>';
document.getElementById('reportTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">로딩 실패</td></tr>';
}
}
function renderReportTable(list, total) {
const tbody = document.getElementById('reportTableBody');
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">업무현황이 없습니다</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-400 py-8">업무현황이 없습니다</td></tr>';
document.getElementById('reportPagination').innerHTML = '';
return;
}
@@ -54,6 +54,7 @@ function renderReportTable(list, total) {
return `<tr class="cursor-pointer hover:bg-gray-50" onclick="viewReportDetail(${r.id})">
<td>${formatDate(r.report_date || r.created_at)}</td>
<td class="text-center hide-mobile">${r.report_seq || 1}</td>
<td class="font-medium">${escapeHtml(r.company_name || '')}</td>
<td class="max-w-xs truncate">${escapeHtml(r.work_content || '')}</td>
<td class="text-center">${r.actual_workers || 0}명</td>
@@ -124,6 +125,24 @@ async function viewReportDetail(id) {
const progressColor = d.progress_rate >= 80 ? 'bg-emerald-500' : d.progress_rate >= 50 ? 'bg-blue-500' : d.progress_rate >= 20 ? 'bg-amber-500' : 'bg-red-500';
// 작업자 목록 테이블
let workersHtml = '';
if (d.workers && d.workers.length > 0) {
const totalHours = d.workers.reduce((sum, w) => sum + Number(w.hours_worked || 0), 0);
workersHtml = `<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">작업자 목록</div>
<div class="bg-gray-50 rounded-lg p-3">
<table class="w-full text-sm">
<thead><tr class="text-xs text-gray-500 border-b"><th class="text-left py-1">작업자</th><th class="text-left py-1">직위</th><th class="text-right py-1">투입시간</th></tr></thead>
<tbody>
${d.workers.map(w => `<tr class="border-b border-gray-100"><td class="py-1">${escapeHtml(w.worker_name)}</td><td class="py-1 text-gray-500">${escapeHtml(w.position || '')}</td><td class="py-1 text-right">${w.hours_worked || 0}h</td></tr>`).join('')}
</tbody>
<tfoot><tr class="font-medium"><td colspan="2" class="py-1">합계 (${d.workers.length}명)</td><td class="py-1 text-right">${totalHours}h</td></tr></tfoot>
</table>
</div>
</div>`;
}
const html = `
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
@@ -131,7 +150,7 @@ async function viewReportDetail(id) {
<div class="text-sm font-medium">${escapeHtml(d.company_name || '')}</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">보고일</div>
<div class="text-xs text-gray-500 mb-1">보고일 (보고 #${d.report_seq || 1})</div>
<div class="text-sm">${formatDateTime(d.report_date || d.created_at)}</div>
</div>
<div>
@@ -147,6 +166,7 @@ async function viewReportDetail(id) {
<span class="text-sm font-medium">${d.progress_rate || 0}%</span>
</div>
</div>
${workersHtml}
<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">작업내용</div>
<div class="text-sm whitespace-pre-wrap bg-gray-50 rounded-lg p-3">${escapeHtml(d.work_content || '-')}</div>