fix(sprint004): 코드 리뷰 반영 — vacation_days 소수 + 이중제출 방지 + deprecated 테이블 전환

- monthlyComparisonModel: vacation_types.deduct_days AS vacation_days 추가
- monthlyComparisonController: vacationDays++ → parseFloat(attend.vacation_days) 소수 지원
- monthly-comparison.js: confirmMonth/submitReject 이중 제출 방지 (isProcessing 플래그)
- vacationBalanceModel: create/update/delete/bulkCreate → sp_vacation_balances + balance_type 매핑

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-31 10:42:12 +09:00
parent 1980c83377
commit f3b7f1a34f
4 changed files with 368 additions and 108 deletions

View File

@@ -282,12 +282,33 @@ function renderConfirmationStatus(conf) {
const statusEl = document.getElementById('confirmedStatus');
const badge = document.getElementById('statusBadge');
if (!conf) { actions.classList.remove('hidden'); statusEl.classList.add('hidden'); return; }
if (!conf) {
// detail 모드(관리자가 타인의 기록 조회)에서는 버튼 숨김
if (currentMode === 'detail') {
actions.classList.add('hidden');
} else {
actions.classList.remove('hidden');
}
statusEl.classList.add('hidden');
return;
}
badge.textContent = { pending: '미확인', confirmed: '확인완료', rejected: '반려' }[conf.status] || '';
badge.className = `mc-status-badge ${conf.status}`;
if (conf.status === 'confirmed') {
if (currentMode === 'detail') {
// 관리자 상세 뷰: 확인/반려 버튼 숨기고 상태만 표시
actions.classList.add('hidden');
if (conf.status !== 'pending') {
statusEl.classList.remove('hidden');
const statusLabel = { confirmed: '확인 완료', rejected: '반려' }[conf.status] || '';
const dt = conf.confirmed_at ? new Date(conf.confirmed_at).toLocaleString('ko') : '';
const reason = conf.reject_reason ? ` (사유: ${conf.reject_reason})` : '';
document.getElementById('confirmedText').textContent = `${dt} ${statusLabel}${reason}`;
} else {
statusEl.classList.add('hidden');
}
} else if (conf.status === 'confirmed') {
actions.classList.add('hidden');
statusEl.classList.remove('hidden');
const dt = conf.confirmed_at ? new Date(conf.confirmed_at).toLocaleString('ko') : '';
@@ -365,22 +386,30 @@ function filterWorkers(status) {
function updateExportButton(summary, workers) {
const btn = document.getElementById('exportBtn');
const note = document.getElementById('exportNote');
const pendingCount = (workers || []).filter(w => w.status === 'pending').length;
const pendingCount = (workers || []).filter(w => !w.status || w.status === 'pending').length;
const rejectedCount = (workers || []).filter(w => w.status === 'rejected').length;
const allConfirmed = pendingCount === 0 && rejectedCount === 0;
if (pendingCount === 0) {
if (allConfirmed) {
btn.disabled = false;
note.textContent = '모든 작업자가 확인을 완료했습니다';
} else {
btn.disabled = true;
const rejectedCount = (workers || []).filter(w => w.status === 'rejected').length;
note.textContent = `${pendingCount}명 미확인${rejectedCount > 0 ? `, ${rejectedCount}명 반려` : ''} — 전원 확인 후 다운로드 가능합니다`;
const parts = [];
if (pendingCount > 0) parts.push(`${pendingCount}명 미확인`);
if (rejectedCount > 0) parts.push(`${rejectedCount}명 반려`);
note.textContent = `${parts.join(', ')} — 전원 확인 후 다운로드 가능합니다`;
}
}
// ===== Actions =====
let isProcessing = false;
async function confirmMonth() {
if (isProcessing) return;
if (!confirm(`${currentYear}${currentMonth}월 근무 내역을 확인하시겠습니까?`)) return;
isProcessing = true;
try {
let res;
if (MOCK_ENABLED) {
@@ -399,6 +428,8 @@ async function confirmMonth() {
}
} catch (e) {
showToast('네트워크 오류', 'error');
} finally {
isProcessing = false;
}
}
@@ -412,12 +443,14 @@ function closeRejectModal() {
}
async function submitReject() {
if (isProcessing) return;
const reason = document.getElementById('rejectReason').value.trim();
if (!reason) {
showToast('반려 사유를 입력해주세요', 'error');
return;
}
isProcessing = true;
try {
let res;
if (MOCK_ENABLED) {
@@ -437,6 +470,8 @@ async function submitReject() {
}
} catch (e) {
showToast('네트워크 오류', 'error');
} finally {
isProcessing = false;
}
}