feat(sprint005): 월간 확인 워크플로우 — 관리자 확인요청 + 수정요청

- DB: status ENUM 확장 (review_sent, change_request) + reviewed_by/at, change_details
- API: POST /review-send (일괄 확인요청), POST /review-respond (수정 승인/거부)
- 작업자: pending=검토대기, review_sent=확인/수정요청, rejected=동의(재확인)
- 관리자: 필터 탭 확장 + 확인요청 일괄 발송 버튼
- confirm 상태 전환 검증: pending→confirmed 차단

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-01 08:09:36 +09:00
parent 1340918f8e
commit 65e5530a6a
10 changed files with 273 additions and 39 deletions

View File

@@ -330,8 +330,22 @@ function renderAdminSummary(s) {
document.getElementById('progressText').textContent = `확인 현황: ${s.confirmed || 0}/${total}명 완료`;
document.getElementById('statusCounts').innerHTML =
`<span>✅ ${s.confirmed || 0} 확인</span>` +
`<span> ${s.pending || 0} 대기</span>` +
`<span>📩 ${s.review_sent || 0} 확인요청</span>` +
`<span>⏳ ${s.pending || 0} 미검토</span>` +
`<span>📝 ${s.change_request || 0} 수정요청</span>` +
`<span>❌ ${s.rejected || 0} 반려</span>`;
// 확인요청 일괄 발송 버튼
var reviewBtn = document.getElementById('reviewSendBtn');
if (reviewBtn) {
var pendingCount = (s.pending || 0);
if (pendingCount > 0) {
reviewBtn.classList.remove('hidden');
reviewBtn.textContent = `미검토 ${pendingCount}명 확인요청 발송`;
} else {
reviewBtn.classList.add('hidden');
}
}
}
function renderWorkerList(workers) {
@@ -497,6 +511,28 @@ async function downloadExcel() {
}
}
// ===== Review Send (확인요청 일괄 발송) =====
async function sendReviewAll() {
if (isProcessing) return;
if (!confirm(currentYear + '년 ' + currentMonth + '월 미검토 작업자 전체에게 확인요청을 발송하시겠습니까?')) return;
isProcessing = true;
try {
var res = await window.apiCall('/monthly-comparison/review-send', 'POST', {
year: currentYear, month: currentMonth
});
if (res && res.success) {
showToast(res.message || '확인요청 발송 완료', 'success');
loadAdminStatus();
} else {
showToast(res && res.message || '발송 실패', 'error');
}
} catch (e) {
showToast('네트워크 오류', 'error');
} finally {
isProcessing = false;
}
}
// ===== View Toggle =====
function toggleViewMode() {
if (currentMode === 'admin') {