fix(monthly-comparison): 0일 0h 수정 + 관리자 검토 태깅
- getAllStatus: daily_attendance_records JOIN으로 실제 근무일/시간 집계 - vacation_days: vacation_types.deduct_days SUM (반차 0.5 정확 반영) - admin_checked 컬럼 + POST /admin-check API (upsert 패턴) - 상태 뱃지 라벨: 미검토/확인요청/수정요청/반려/확인완료 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -262,6 +262,20 @@ const MonthlyComparisonController = {
|
||||
}
|
||||
},
|
||||
|
||||
// POST /admin-check (관리자 개별 검토 태깅)
|
||||
adminCheck: async (req, res) => {
|
||||
try {
|
||||
const { user_id, year, month, checked } = req.body;
|
||||
if (!user_id || !year || !month) return res.status(400).json({ success: false, message: 'user_id, year, month 필수' });
|
||||
const checkedBy = req.user.user_id || req.user.id;
|
||||
const result = await Model.adminCheck(parseInt(user_id), parseInt(year), parseInt(month), !!checked, checkedBy);
|
||||
res.json({ success: true, data: result, message: checked ? '검토완료 표시' : '검토 해제' });
|
||||
} catch (err) {
|
||||
logger.error('monthlyComparison adminCheck error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// GET /all-status (support_team+)
|
||||
getAllStatus: async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -4,3 +4,4 @@ ALTER TABLE monthly_work_confirmations
|
||||
ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS reviewed_by INT NULL;
|
||||
ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMP NULL;
|
||||
ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS change_details TEXT NULL;
|
||||
ALTER TABLE monthly_work_confirmations ADD COLUMN IF NOT EXISTS admin_checked TINYINT(1) DEFAULT 0;
|
||||
|
||||
@@ -256,7 +256,7 @@ const MonthlyComparisonModel = {
|
||||
}
|
||||
},
|
||||
|
||||
// 5. 전체 작업자 확인 현황
|
||||
// 5. 전체 작업자 확인 현황 (실제 근태 데이터 집계 포함)
|
||||
async getAllStatus(year, month, departmentId) {
|
||||
const db = await getDb();
|
||||
let sql = `
|
||||
@@ -265,15 +265,29 @@ const MonthlyComparisonModel = {
|
||||
COALESCE(d.department_name, '미배정') AS department_name,
|
||||
COALESCE(mwc.status, 'pending') AS status,
|
||||
mwc.confirmed_at, mwc.rejected_at, mwc.reject_reason,
|
||||
mwc.total_work_days, mwc.total_work_hours,
|
||||
mwc.total_overtime_hours, mwc.vacation_days, mwc.mismatch_count
|
||||
mwc.change_details, COALESCE(mwc.admin_checked, 0) AS admin_checked,
|
||||
COALESCE(att.work_days, 0) AS total_work_days,
|
||||
COALESCE(att.work_hours, 0) AS total_work_hours,
|
||||
COALESCE(att.overtime_hours, 0) AS total_overtime_hours,
|
||||
COALESCE(att.vac_days, 0) AS vacation_days
|
||||
FROM workers w
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
LEFT JOIN monthly_work_confirmations mwc
|
||||
ON w.user_id = mwc.user_id AND mwc.year = ? AND mwc.month = ?
|
||||
WHERE w.status = 'active'
|
||||
LEFT JOIN (
|
||||
SELECT dar.user_id,
|
||||
COUNT(CASE WHEN dar.total_work_hours > 0 THEN 1 END) AS work_days,
|
||||
COALESCE(SUM(dar.total_work_hours), 0) AS work_hours,
|
||||
COALESCE(SUM(CASE WHEN dar.total_work_hours > 8 THEN dar.total_work_hours - 8 ELSE 0 END), 0) AS overtime_hours,
|
||||
COALESCE(SUM(CASE WHEN vt.deduct_days IS NOT NULL THEN vt.deduct_days ELSE 0 END), 0) AS vac_days
|
||||
FROM daily_attendance_records dar
|
||||
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
||||
WHERE YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ?
|
||||
GROUP BY dar.user_id
|
||||
) att ON w.user_id = att.user_id
|
||||
WHERE w.status = 'active' AND w.user_id IS NOT NULL
|
||||
`;
|
||||
const params = [year, month];
|
||||
const params = [year, month, year, month];
|
||||
if (departmentId) {
|
||||
sql += ' AND w.department_id = ?';
|
||||
params.push(departmentId);
|
||||
@@ -284,6 +298,17 @@ const MonthlyComparisonModel = {
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 5b. 관리자 개별 검토 태깅
|
||||
async adminCheck(userId, year, month, checked, checkedBy) {
|
||||
const db = await getDb();
|
||||
await db.query(`
|
||||
INSERT INTO monthly_work_confirmations (user_id, year, month, status, admin_checked)
|
||||
VALUES (?, ?, ?, 'pending', ?)
|
||||
ON DUPLICATE KEY UPDATE admin_checked = ?
|
||||
`, [userId, year, month, checked ? 1 : 0, checked ? 1 : 0]);
|
||||
return { admin_checked: checked };
|
||||
},
|
||||
|
||||
// 6. 지원팀 사용자 목록 (알림 수신자)
|
||||
async getSupportTeamUsers() {
|
||||
const db = await getDb();
|
||||
|
||||
@@ -29,6 +29,9 @@ router.post('/review-send', requireSupportTeam, ctrl.reviewSend);
|
||||
// 관리자: 수정요청 응답 (change_request → review_sent 또는 rejected)
|
||||
router.post('/review-respond', requireSupportTeam, ctrl.reviewRespond);
|
||||
|
||||
// 관리자: 개별 검토 태깅
|
||||
router.post('/admin-check', requireSupportTeam, ctrl.adminCheck);
|
||||
|
||||
// 전체 현황 (support_team+)
|
||||
router.get('/all-status', requireSupportTeam, ctrl.getAllStatus);
|
||||
|
||||
|
||||
@@ -361,9 +361,9 @@ function renderWorkerList(workers) {
|
||||
}
|
||||
|
||||
el.innerHTML = filtered.map(w => {
|
||||
const statusBadge = `<span class="mc-worker-status-badge ${w.status}">${
|
||||
{ confirmed: '확인완료', pending: '미확인', rejected: '반려' }[w.status] || ''
|
||||
}</span>`;
|
||||
const statusLabels = { confirmed: '확인완료', pending: '미검토', review_sent: '확인요청', change_request: '수정요청', rejected: '반려' };
|
||||
const statusBadge = `<span class="mc-worker-status-badge ${w.status}">${statusLabels[w.status] || ''}</span>`;
|
||||
const checkedBadge = w.admin_checked ? ' <span style="color:#10b981;font-size:0.7rem;">✓검토</span>' : '';
|
||||
const mismatchBadge = w.mismatch_count > 0
|
||||
? `<span class="mc-worker-mismatch">⚠️ 불일치${w.mismatch_count}</span>` : '';
|
||||
const rejectReason = w.status === 'rejected' && w.reject_reason
|
||||
@@ -374,7 +374,7 @@ function renderWorkerList(workers) {
|
||||
<div class="mc-worker-card" onclick="viewWorkerDetail(${w.user_id})">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<div class="mc-worker-name">${escHtml(w.worker_name)} ${mismatchBadge}</div>
|
||||
<div class="mc-worker-name">${escHtml(w.worker_name)}${checkedBadge} ${mismatchBadge}</div>
|
||||
<div class="mc-worker-dept">${escHtml(w.department_name)} · ${escHtml(w.job_type)}</div>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right text-gray-300"></i>
|
||||
|
||||
@@ -162,7 +162,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=2026040101"></script>
|
||||
<script src="/js/monthly-comparison.js?v=2026040102"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user