sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
5.8 KiB
JavaScript
176 lines
5.8 KiB
JavaScript
import { API, getAuthHeaders } from '/js/api-config.js';
|
|
|
|
const yearSel = document.getElementById('year');
|
|
const monthSel = document.getElementById('month');
|
|
const container = document.getElementById('attendanceTableContainer');
|
|
|
|
const holidays = [
|
|
'2025-01-01','2025-01-27','2025-01-28','2025-01-29','2025-01-30','2025-01-31',
|
|
'2025-03-01','2025-03-03','2025-05-01','2025-05-05','2025-05-06',
|
|
'2025-06-03','2025-06-06','2025-08-15','2025-10-03','2025-10-09','2025-12-25'
|
|
];
|
|
|
|
const leaveDefaults = {
|
|
'김두수':16,'임영규':16,'반치원':16,'황인용':16,'표영진':15,
|
|
'김윤섭':16,'이창호':16,'최광욱':16,'박현수':14,'조윤호':0
|
|
};
|
|
|
|
let workers = [];
|
|
|
|
// ✅ 셀렉트 박스 옵션 + 기본 선택 추가
|
|
function fillSelectOptions() {
|
|
const currentY = new Date().getFullYear();
|
|
const currentM = String(new Date().getMonth() + 1).padStart(2, '0');
|
|
|
|
for (let y = currentY; y <= currentY + 5; y++) {
|
|
const selected = y === currentY ? 'selected' : '';
|
|
yearSel.insertAdjacentHTML('beforeend', `<option value="${y}" ${selected}>${y}</option>`);
|
|
}
|
|
|
|
for (let m = 1; m <= 12; m++) {
|
|
const mm = String(m).padStart(2, '0');
|
|
const selected = mm === currentM ? 'selected' : '';
|
|
monthSel.insertAdjacentHTML('beforeend', `<option value="${mm}" ${selected}>${m}월</option>`);
|
|
}
|
|
}
|
|
|
|
// ✅ 작업자 목록 불러오기
|
|
async function fetchWorkers() {
|
|
try {
|
|
const res = await fetch(`${API}/workers`, { headers: getAuthHeaders() });
|
|
const allWorkers = await res.json();
|
|
|
|
// 활성화된 작업자만 필터링
|
|
workers = allWorkers.filter(worker => {
|
|
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
|
});
|
|
|
|
workers.sort((a, b) => a.user_id - b.user_id);
|
|
} catch (err) {
|
|
alert('작업자 불러오기 실패');
|
|
}
|
|
}
|
|
|
|
// ✅ 출근부 불러오기 (해당 연도 전체)
|
|
async function loadAttendance() {
|
|
const year = yearSel.value;
|
|
const month = monthSel.value;
|
|
if (!year || !month) return alert('연도와 월을 선택하세요');
|
|
|
|
const lastDay = new Date(+year, +month, 0).getDate();
|
|
const start = `${year}-01-01`;
|
|
const end = `${year}-12-31`;
|
|
|
|
try {
|
|
const res = await fetch(`${API}/workreports?start=${start}&end=${end}`, {
|
|
headers: getAuthHeaders()
|
|
});
|
|
const data = await res.json();
|
|
renderTable(data, year, month, lastDay);
|
|
} catch (err) {
|
|
alert('출근부 로딩 실패');
|
|
}
|
|
}
|
|
|
|
// ✅ 테이블 렌더링
|
|
function renderTable(data, year, month, lastDay) {
|
|
container.innerHTML = '';
|
|
const weekdays = ['일','월','화','수','목','금','토'];
|
|
const tbl = document.createElement('table');
|
|
|
|
// ⬆️ 헤더 구성
|
|
let thead = `<thead><tr><th rowspan="2">작업자</th>`;
|
|
for (let d = 1; d <= lastDay; d++) thead += `<th>${d}</th>`;
|
|
thead += `<th class="divider" rowspan="2">잔업합계</th><th rowspan="2">사용연차</th><th rowspan="2">잔여연차</th></tr><tr>`;
|
|
for (let d = 1; d <= lastDay; d++) {
|
|
const dow = new Date(+year, +month - 1, d).getDay();
|
|
thead += `<th>${weekdays[dow]}</th>`;
|
|
}
|
|
thead += '</tr></thead>';
|
|
tbl.innerHTML = thead;
|
|
|
|
// ⬇️ 본문
|
|
workers.forEach(w => {
|
|
// ✅ 월간 데이터 (표에 표시용)
|
|
const recsThisMonth = data.filter(r =>
|
|
r.user_id === w.user_id &&
|
|
new Date(r.date).getFullYear() === +year &&
|
|
new Date(r.date).getMonth() + 1 === +month
|
|
);
|
|
|
|
// ✅ 연간 데이터 (연차 계산용)
|
|
const recsThisYear = data.filter(r =>
|
|
r.user_id === w.user_id &&
|
|
new Date(r.date).getFullYear() === +year
|
|
);
|
|
|
|
let otSum = 0;
|
|
let row = `<tr><td>${w.worker_name}</td>`;
|
|
|
|
for (let d = 1; d <= lastDay; d++) {
|
|
const dd = String(d).padStart(2, '0');
|
|
const date = `${year}-${month}-${dd}`;
|
|
|
|
const rec = recsThisMonth.find(r => {
|
|
const rDate = new Date(r.date);
|
|
const yyyy = rDate.getFullYear();
|
|
const mm = String(rDate.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(rDate.getDate()).padStart(2, '0');
|
|
return `${yyyy}-${mm}-${dd}` === date;
|
|
});
|
|
|
|
const dow = new Date(+year, +month - 1, d).getDay();
|
|
const isWe = dow === 0 || dow === 6;
|
|
const isHo = holidays.includes(date);
|
|
|
|
let txt = '', cls = '';
|
|
if (rec) {
|
|
const ot = +rec.overtime_hours || 0;
|
|
if (ot > 0) {
|
|
txt = ot; cls = 'overtime-cell'; otSum += ot;
|
|
} else if (rec.work_details) {
|
|
const d = rec.work_details;
|
|
if (['연차','반차','반반차','조퇴'].includes(d)) {
|
|
txt = d; cls = 'leave';
|
|
} else if (d === '유급') {
|
|
txt = d; cls = 'paid-leave';
|
|
} else if (d === '휴무') {
|
|
txt = d; cls = 'holiday';
|
|
} else {
|
|
txt = d;
|
|
}
|
|
}
|
|
} else {
|
|
txt = (isWe || isHo) ? '휴무' : '';
|
|
cls = (isWe || isHo) ? 'holiday' : 'no-data';
|
|
}
|
|
|
|
row += `<td class="${cls}">${txt}</td>`;
|
|
}
|
|
|
|
const usedTot = recsThisYear
|
|
.filter(r => ['연차','반차','반반차','조퇴'].includes(r.work_details))
|
|
.reduce((s, r) => s + (
|
|
r.work_details === '연차' ? 1 :
|
|
r.work_details === '반차' ? 0.5 :
|
|
r.work_details === '반반차' ? 0.25 : 0.75
|
|
), 0);
|
|
|
|
const remain = (leaveDefaults[w.worker_name] || 0) - usedTot;
|
|
|
|
row += `<td class="divider overtime-sum">${otSum.toFixed(1)}</td>`;
|
|
row += `<td>${usedTot.toFixed(2)}</td><td>${remain.toFixed(2)}</td></tr>`;
|
|
row += `<tr class="separator"><td colspan="${lastDay + 4}"></td></tr>`;
|
|
|
|
tbl.insertAdjacentHTML('beforeend', row);
|
|
});
|
|
|
|
container.appendChild(tbl);
|
|
}
|
|
|
|
// ✅ 초기 로딩
|
|
fillSelectOptions();
|
|
fetchWorkers().then(() => {
|
|
loadAttendance(); // 자동 조회
|
|
});
|
|
document.getElementById('loadAttendance').addEventListener('click', loadAttendance); |