feat(monthly-comparison): 검토완료 상단 토글 + 월 유지 + 확인요청 조건

- 검토완료 버튼: 하단 제거 → 헤더 토글 ("검토하기" ↔ "✓ 검토완료")
- 상세→목록 복귀: year/month URL 유지 (4월로 리셋 방지)
- 확인요청: 전원 admin_checked 시만 활성화 (정책 변경)
- Sprint 004 PLAN.md: 정책 변경 이력 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-01 08:37:19 +09:00
parent 4bb4fbd225
commit 4309d308bc
3 changed files with 198 additions and 26 deletions

View File

@@ -165,22 +165,21 @@ async function loadMyRecords() {
comparisonData = res.data;
// detail 모드: 작업자 이름 표시
// detail 모드: 작업자 이름 + 검토완료 버튼 (상단 헤더)
if (currentMode === 'detail' && comparisonData.user) {
document.getElementById('pageTitle').textContent =
(comparisonData.user.worker_name || '') + ' 근무 비교';
var isChecked = comparisonData.confirmation && comparisonData.confirmation.admin_checked;
var checkBtnHtml = '<button type="button" id="headerCheckBtn" onclick="toggleAdminCheck()" style="' +
'padding:6px 12px;border-radius:8px;font-size:0.75rem;font-weight:600;border:none;cursor:pointer;margin-left:auto;' +
(isChecked ? 'background:#dcfce7;color:#166534;' : 'background:#f3f4f6;color:#6b7280;') +
'">' + (isChecked ? '✓ 검토완료' : '검토하기') + '</button>';
document.getElementById('pageTitle').innerHTML =
(comparisonData.user.worker_name || '') + ' 근무 비교' + checkBtnHtml;
}
renderSummaryCards(comparisonData.summary);
renderMismatchAlert(comparisonData.summary);
renderDailyList(comparisonData.daily_records || []);
renderConfirmationStatus(comparisonData.confirmation);
// detail 모드: 검토완료 버튼 표시
var adminCheckBtn = document.getElementById('adminCheckBtn');
if (adminCheckBtn && currentMode === 'detail') {
adminCheckBtn.classList.remove('hidden');
}
} catch (e) {
listEl.innerHTML = '<div class="mc-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>네트워크 오류</p></div>';
}
@@ -348,13 +347,21 @@ function renderAdminSummary(s) {
`<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) {
var uncheckedCount = (adminData?.workers || []).filter(function(w) { return !w.admin_checked && w.status === 'pending'; }).length;
if (pendingCount > 0 && uncheckedCount === 0) {
reviewBtn.classList.remove('hidden');
reviewBtn.textContent = `미검토 ${pendingCount}명 확인요청 발송`;
reviewBtn.disabled = false;
reviewBtn.textContent = `${pendingCount}명 확인요청 발송`;
reviewBtn.style.background = '#2563eb';
} else if (pendingCount > 0 && uncheckedCount > 0) {
reviewBtn.classList.remove('hidden');
reviewBtn.disabled = true;
reviewBtn.textContent = `${uncheckedCount}명 미검토 — 전원 검토 후 발송 가능`;
reviewBtn.style.background = '#9ca3af';
} else {
reviewBtn.classList.add('hidden');
}
@@ -524,17 +531,28 @@ async function downloadExcel() {
}
}
// ===== Admin Check (검토완료 태깅) =====
async function markAdminChecked() {
// ===== Admin Check (검토완료 토글) =====
async function toggleAdminCheck() {
if (!currentUserId || isProcessing) return;
var isCurrentlyChecked = comparisonData?.confirmation?.admin_checked;
var newChecked = !isCurrentlyChecked;
isProcessing = true;
try {
var res = await window.apiCall('/monthly-comparison/admin-check', 'POST', {
user_id: currentUserId, year: currentYear, month: currentMonth, checked: true
user_id: currentUserId, year: currentYear, month: currentMonth, checked: newChecked
});
if (res && res.success) {
showToast((comparisonData?.user?.worker_name || '') + ' 검토완료', 'success');
history.back();
// 상태 업데이트
if (comparisonData.confirmation) {
comparisonData.confirmation.admin_checked = newChecked ? 1 : 0;
}
var btn = document.getElementById('headerCheckBtn');
if (btn) {
btn.textContent = newChecked ? '✓ 검토완료' : '검토하기';
btn.style.background = newChecked ? '#dcfce7' : '#f3f4f6';
btn.style.color = newChecked ? '#166534' : '#6b7280';
}
showToast(newChecked ? '검토완료' : '검토 해제', 'success');
} else {
showToast(res?.message || '처리 실패', 'error');
}
@@ -542,6 +560,11 @@ async function markAdminChecked() {
finally { isProcessing = false; }
}
// 목록으로 복귀 (월 유지)
function goBackToList() {
location.href = '/pages/attendance/monthly-comparison.html?mode=admin&year=' + currentYear + '&month=' + currentMonth;
}
// ===== Review Send (확인요청 일괄 발송) =====
async function sendReviewAll() {
if (isProcessing) return;

View File

@@ -37,8 +37,8 @@
<!-- 페이지 헤더 -->
<div class="mc-header">
<div class="mc-header-row">
<button type="button" onclick="history.back()" class="mc-back-btn"><i class="fas fa-arrow-left"></i></button>
<h1 id="pageTitle">월간 근무 비교</h1>
<button type="button" onclick="typeof goBackToList==='function'?goBackToList():history.back()" class="mc-back-btn"><i class="fas fa-arrow-left"></i></button>
<h1 id="pageTitle" style="display:flex;align-items:center;gap:8px;flex:1">월간 근무 비교</h1>
<button id="viewToggleBtn" class="mc-view-toggle hidden" onclick="toggleViewMode()">
<i class="fas fa-users-cog"></i>
</button>
@@ -87,12 +87,7 @@
<span id="confirmedText"></span>
</div>
<!-- 관리자 검토완료 버튼 (detail 모드) -->
<div class="mc-bottom-actions hidden" id="adminCheckBtn">
<button type="button" style="flex:1;padding:14px;background:#10b981;color:white;font-size:0.9rem;font-weight:700;border:none;border-radius:12px;cursor:pointer;" onclick="markAdminChecked()">
<i class="fas fa-check mr-2"></i>검토완료
</button>
</div>
<!-- 하단 검토완료 버튼 제거됨 — 상단 헤더로 이동 -->
</div>
<!-- ═══ 관리자 뷰 ═══ -->
@@ -169,7 +164,7 @@
<script src="/static/js/tkfb-core.js?v=2026033108"></script>
<script src="/js/api-base.js?v=2026031701"></script>
<script src="/js/monthly-comparison.js?v=2026040103"></script>
<script src="/js/monthly-comparison.js?v=2026040104"></script>
<script>initAuth();</script>
</body>
</html>