refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)

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>
This commit is contained in:
Hyungi Ahn
2026-03-05 13:13:10 +09:00
parent 2197cdb3d5
commit abd7564e6b
90 changed files with 1790 additions and 925 deletions

View File

@@ -271,7 +271,7 @@
const select = document.getElementById('workerFilter');
workers.forEach(worker => {
const option = document.createElement('option');
option.value = worker.worker_id;
option.value = worker.user_id;
option.textContent = worker.worker_name;
select.appendChild(option);
});
@@ -297,7 +297,7 @@
params: {
start_date: startDate,
end_date: endDate,
worker_id: workerId || undefined
user_id: workerId || undefined
}
});
@@ -306,7 +306,7 @@
params: {
start_date: startDate,
end_date: endDate,
worker_id: workerId || undefined
user_id: workerId || undefined
}
});
@@ -340,10 +340,10 @@
// 출퇴근 기록 맵핑
attendanceRecords.forEach(record => {
const key = `${record.attendance_date}_${record.worker_id}`;
const key = `${record.attendance_date}_${record.user_id}`;
dateWorkerMap.set(key, {
date: record.attendance_date,
worker_id: record.worker_id,
user_id: record.user_id,
worker_name: record.worker_name,
attendance: record,
reports: []
@@ -352,13 +352,13 @@
// 작업 보고서 맵핑
workReports.forEach(report => {
const key = `${report.report_date}_${report.worker_id}`;
const key = `${report.report_date}_${report.user_id}`;
if (dateWorkerMap.has(key)) {
dateWorkerMap.get(key).reports.push(report);
} else {
dateWorkerMap.set(key, {
date: report.report_date,
worker_id: report.worker_id,
user_id: report.user_id,
worker_name: report.worker_name,
attendance: null,
reports: [report]

View File

@@ -499,7 +499,7 @@
function populateAssigneeDropdown() {
const select = document.getElementById('receiveAssignee');
select.innerHTML = '<option value="">담당자 선택</option>' +
workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>`).join('');
workers.map(w => `<option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>`).join('');
}
async function loadRepairRequests() {

View File

@@ -392,7 +392,7 @@
// 데이터 정리
vacationData = {};
workers.forEach(w => {
vacationData[w.worker_id] = {
vacationData[w.user_id] = {
carryover: 0,
annual: 0,
longService: 0,
@@ -403,9 +403,9 @@
// 잔액 데이터 매핑
balances.forEach(b => {
if (!vacationData[b.worker_id]) return;
if (!vacationData[b.user_id]) return;
const code = b.type_code || '';
const data = vacationData[b.worker_id];
const data = vacationData[b.user_id];
if (code === 'CARRYOVER' || b.type_name === '이월') {
data.carryover = b.total_days || 0;
@@ -443,7 +443,7 @@
}
tbody.innerHTML = workers.map((w, idx) => {
const d = vacationData[w.worker_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
const d = vacationData[w.user_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
const carryover = parseFloat(d.carryover) || 0;
const annual = parseFloat(d.annual) || 0;
const longService = parseFloat(d.longService) || 0;
@@ -454,29 +454,29 @@
const remainingClass = remaining > 0 ? 'positive' : remaining < 0 ? 'negative' : 'zero';
return `
<tr data-worker-id="${w.worker_id}">
<tr data-user-id="${w.user_id}">
<td>${idx + 1}</td>
<td class="worker-name">${w.worker_name}</td>
<td>
<input type="number" class="num-input ${carryover < 0 ? 'negative' : ''}"
value="${carryover}" step="0.5"
data-field="carryover"
onchange="updateField(${w.worker_id}, 'carryover', this.value)">
onchange="updateField(${w.user_id}, 'carryover', this.value)">
</td>
<td>
<input type="number" class="num-input"
value="${annual}" step="0.5"
data-field="annual"
onchange="updateField(${w.worker_id}, 'annual', this.value)">
onchange="updateField(${w.user_id}, 'annual', this.value)">
</td>
<td>
<input type="number" class="num-input"
value="${longService}" step="0.5"
data-field="longService"
onchange="updateField(${w.worker_id}, 'longService', this.value)">
onchange="updateField(${w.user_id}, 'longService', this.value)">
</td>
<td>
<button class="special-btn" onclick="openSpecialModal(${w.worker_id}, '${w.worker_name}')">
<button class="special-btn" onclick="openSpecialModal(${w.user_id}, '${w.worker_name}')">
${(d.specials || []).length > 0 ? `${specialTotal}` : '추가'}
${(d.specials || []).length > 0 ? `<span class="special-count">${d.specials.length}</span>` : ''}
</button>
@@ -497,7 +497,7 @@
vacationData[workerId][field] = val;
// 입력 스타일 업데이트
const input = document.querySelector(`tr[data-worker-id="${workerId}"] input[data-field="${field}"]`);
const input = document.querySelector(`tr[data-user-id="${workerId}"] input[data-field="${field}"]`);
if (input) {
input.classList.toggle('negative', val < 0);
}
@@ -508,7 +508,7 @@
}
function updateRowTotals(workerId) {
const row = document.querySelector(`tr[data-worker-id="${workerId}"]`);
const row = document.querySelector(`tr[data-user-id="${workerId}"]`);
if (!row) return;
const d = vacationData[workerId];
@@ -668,12 +668,12 @@
// 데이터 수집
for (const w of workers) {
const d = vacationData[w.worker_id];
const d = vacationData[w.user_id];
if (!d) continue;
if (typeIdMap['CARRYOVER']) {
balancesToSave.push({
worker_id: w.worker_id,
user_id: w.user_id,
vacation_type_id: typeIdMap['CARRYOVER'],
year: currentYear,
total_days: d.carryover
@@ -681,7 +681,7 @@
}
if (typeIdMap['ANNUAL']) {
balancesToSave.push({
worker_id: w.worker_id,
user_id: w.user_id,
vacation_type_id: typeIdMap['ANNUAL'],
year: currentYear,
total_days: d.annual
@@ -689,7 +689,7 @@
}
if (typeIdMap['LONG_SERVICE']) {
balancesToSave.push({
worker_id: w.worker_id,
user_id: w.user_id,
vacation_type_id: typeIdMap['LONG_SERVICE'],
year: currentYear,
total_days: d.longService
@@ -726,7 +726,7 @@
}
if (specialTypeId) {
balancesToSave.push({
worker_id: w.worker_id,
user_id: w.user_id,
vacation_type_id: specialTypeId,
year: currentYear,
total_days: special.days,

View File

@@ -270,18 +270,18 @@
checkinStatus = {};
workers.forEach(w => {
const checkin = checkinList.find(c => c.worker_id === w.worker_id);
const record = records.find(r => r.worker_id === w.worker_id);
const checkin = checkinList.find(c => c.user_id === w.user_id);
const record = records.find(r => r.user_id === w.user_id);
if (checkin?.vacation_status === 'approved' || record?.vacation_type_id) {
checkinStatus[w.worker_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' };
checkinStatus[w.user_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' };
} else if (record && record.is_present === 0) {
checkinStatus[w.worker_id] = { status: 'absent' };
checkinStatus[w.user_id] = { status: 'absent' };
} else if (record && record.is_present === 1) {
checkinStatus[w.worker_id] = { status: 'present' };
checkinStatus[w.user_id] = { status: 'present' };
} else {
// 기록이 없으면 기본 출근
checkinStatus[w.worker_id] = { status: 'present' };
checkinStatus[w.user_id] = { status: 'present' };
}
});
@@ -301,9 +301,9 @@
}
container.innerHTML = workers.map(w => {
const s = checkinStatus[w.worker_id] || { status: 'present' };
const s = checkinStatus[w.user_id] || { status: 'present' };
const label = s.status === 'present' ? '출근' : s.status === 'absent' ? '결근' : (s.vacationType || '연차');
return `<span class="worker-chip ${s.status}" onclick="toggle(${w.worker_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`;
return `<span class="worker-chip ${s.status}" onclick="toggle(${w.user_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`;
}).join('');
updateSummary();
@@ -318,8 +318,8 @@
function setAllPresent() {
workers.forEach(w => {
if (checkinStatus[w.worker_id]?.status !== 'vacation') {
checkinStatus[w.worker_id] = { status: 'present' };
if (checkinStatus[w.user_id]?.status !== 'vacation') {
checkinStatus[w.user_id] = { status: 'present' };
}
});
render();
@@ -327,8 +327,8 @@
function setAllAbsent() {
workers.forEach(w => {
if (checkinStatus[w.worker_id]?.status !== 'vacation') {
checkinStatus[w.worker_id] = { status: 'absent' };
if (checkinStatus[w.user_id]?.status !== 'vacation') {
checkinStatus[w.user_id] = { status: 'absent' };
}
});
render();
@@ -374,10 +374,10 @@
// 연차가 아닌 작업자들만 체크인 데이터로 전송
const checkins = workers
.filter(w => checkinStatus[w.worker_id]?.status !== 'vacation')
.filter(w => checkinStatus[w.user_id]?.status !== 'vacation')
.map(w => ({
worker_id: w.worker_id,
is_present: checkinStatus[w.worker_id]?.status === 'present'
user_id: w.user_id,
is_present: checkinStatus[w.user_id]?.status === 'present'
}));
try {

View File

@@ -177,7 +177,7 @@
// 체크인 목록을 기준으로 출퇴근 기록 생성 (연차 정보 포함)
attendanceRecords = checkinList.map(worker => {
const existingRecord = existingRecords.find(r => r.worker_id === worker.worker_id);
const existingRecord = existingRecords.find(r => r.user_id === worker.user_id);
const isOnVacation = worker.vacation_status === 'approved';
// 기존 기록이 있으면 사용, 없으면 초기화
@@ -185,7 +185,7 @@
return existingRecord;
} else {
return {
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
attendance_date: selectedDate,
total_hours: isOnVacation ? 0 : 8,
@@ -209,7 +209,7 @@
function initializeAttendanceRecords() {
const selectedDate = document.getElementById('selectedDate').value;
attendanceRecords = workers.map(worker => ({
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
attendance_date: selectedDate,
total_hours: 8,
@@ -351,7 +351,7 @@
// 모든 기록을 API 형식에 맞게 변환
const recordsToSave = attendanceRecords.map(record => ({
worker_id: record.worker_id,
user_id: record.user_id,
attendance_date: selectedDate,
total_hours: record.total_hours || 0,
overtime_hours: record.overtime_hours || 0,
@@ -371,7 +371,7 @@
successCount++;
}
} catch (error) {
console.error(`작업자 ${data.worker_id} 저장 오류:`, error);
console.error(`작업자 ${data.user_id} 저장 오류:`, error);
errorCount++;
}
}

View File

@@ -662,7 +662,7 @@
const records = response.data.data;
// 작업자별로 데이터 정리
records.forEach(record => {
const workerId = record.worker_id;
const workerId = record.user_id;
// 날짜 형식 정규화 (다양한 형식 처리)
let dateKey = record.record_date;
if (dateKey) {
@@ -686,7 +686,7 @@
console.error('근태 데이터 조회 오류:', err);
// 작업자별 빈 데이터 초기화
workers.forEach(worker => {
attendanceData[worker.worker_id] = {};
attendanceData[worker.user_id] = {};
});
}
@@ -765,7 +765,7 @@
html += `<tbody>`;
workers.forEach((worker, index) => {
const workerRecords = attendanceData[worker.worker_id] || {};
const workerRecords = attendanceData[worker.user_id] || {};
let totalOvertimeHours = 0;
// 입사일 파싱
@@ -954,7 +954,7 @@
let totalNormalDays = 0;
workers.forEach(worker => {
const records = attendanceData[worker.worker_id] || {};
const records = attendanceData[worker.user_id] || {};
Object.values(records).forEach(record => {
const hours = parseFloat(record.total_work_hours) || 0;
totalWorkHours += hours;

View File

@@ -306,13 +306,13 @@
document.getElementById('adminControls').classList.add('visible');
await loadWorkers();
} else {
// 일반 사용자: 본인 worker_id 사용
if (currentUser?.worker_id) {
currentWorkerId = currentUser.worker_id;
// 일반 사용자: 본인 user_id 사용
if (currentUser?.user_id) {
currentWorkerId = currentUser.user_id;
document.getElementById('infoGrid').style.display = 'grid';
await loadAllData();
} else {
// worker_id가 없는 경우
// user_id가 없는 경우
document.getElementById('noWorkerMessage').style.display = 'block';
}
}
@@ -352,7 +352,7 @@
const select = document.getElementById('workerSelect');
workers.forEach(w => {
const opt = document.createElement('option');
opt.value = w.worker_id;
opt.value = w.user_id;
opt.textContent = w.worker_name;
select.appendChild(opt);
});
@@ -369,7 +369,7 @@
}
currentWorkerId = parseInt(workerId);
const worker = workers.find(w => w.worker_id === currentWorkerId);
const worker = workers.find(w => w.user_id === currentWorkerId);
document.getElementById('workerNameDisplay').textContent =
`${worker?.worker_name || ''}님의 연차 잔여 현황 및 월간 연장근로 시간`;
document.getElementById('infoGrid').style.display = 'grid';
@@ -486,7 +486,7 @@
try {
// 근태 기록에서 연장근로 데이터 조회
const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&worker_id=${currentWorkerId}`);
const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&user_id=${currentWorkerId}`);
const records = res.data.data || [];
// 8시간 초과분 계산

View File

@@ -222,7 +222,7 @@
event.preventDefault();
const data = {
worker_id: parseInt(document.getElementById('inputWorker').value),
user_id: parseInt(document.getElementById('inputWorker').value),
vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
start_date: document.getElementById('inputStartDate').value,
end_date: document.getElementById('inputEndDate').value,

View File

@@ -369,7 +369,7 @@
event.preventDefault();
const data = {
worker_id: parseInt(document.getElementById('inputWorker').value),
user_id: parseInt(document.getElementById('inputWorker').value),
vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
start_date: document.getElementById('inputStartDate').value,
end_date: document.getElementById('inputEndDate').value,

View File

@@ -155,7 +155,7 @@
try {
const currentUser = getCurrentUser();
if (!currentUser || !currentUser.worker_id) {
if (!currentUser || !currentUser.user_id) {
alert('작업자 정보가 없습니다. 관리자에게 문의하세요.');
return;
}
@@ -179,7 +179,7 @@
const currentUser = getCurrentUser();
try {
const response = await axios.get(`/attendance/vacation-balance/${currentUser.worker_id}`);
const response = await axios.get(`/attendance/vacation-balance/${currentUser.user_id}`);
if (response.data.success) {
renderVacationBalance(response.data.data);
}
@@ -222,7 +222,7 @@
const currentUser = getCurrentUser();
const data = {
worker_id: currentUser.worker_id,
user_id: currentUser.user_id,
vacation_type_id: parseInt(document.getElementById('vacationType').value),
start_date: document.getElementById('startDate').value,
end_date: document.getElementById('endDate').value,
@@ -251,7 +251,7 @@
if (response.data.success) {
// 내 신청만 필터링
const myRequests = response.data.data.filter(req =>
req.requested_by === currentUser.user_id || req.worker_id === currentUser.worker_id
req.requested_by === currentUser.user_id || req.user_id === currentUser.user_id
);
renderVacationRequests(myRequests, 'myRequestsList', true, 'delete');
}

View File

@@ -329,7 +329,7 @@
workStatus = {};
workers.forEach(w => {
const record = records.find(r => r.worker_id === w.worker_id);
const record = records.find(r => r.user_id === w.user_id);
// 입사일 이전인지 확인
const joinDate = w.join_date ? w.join_date.split('T')[0] : null;
@@ -337,7 +337,7 @@
if (isBeforeJoin) {
// 입사 전 날짜
workStatus[w.worker_id] = {
workStatus[w.user_id] = {
isPresent: false,
type: 'not_hired',
hours: 0,
@@ -396,7 +396,7 @@
const typeInfo = attendanceTypes.find(t => t.value === type);
workStatus[w.worker_id] = {
workStatus[w.user_id] = {
isPresent: record.is_present === 1 || typeInfo?.isLeave,
type: type,
hours: typeInfo !== undefined ? typeInfo.hours : 8,
@@ -406,7 +406,7 @@
};
} else {
// 출근 체크 기록이 없는 경우 - 결근 상태
workStatus[w.worker_id] = {
workStatus[w.user_id] = {
isPresent: false,
type: 'normal',
hours: 8,
@@ -435,7 +435,7 @@
}
tbody.innerHTML = workers.map((w, idx) => {
const s = workStatus[w.worker_id];
const s = workStatus[w.user_id];
// 미입사 상태 처리
if (s.isNotHired) {
@@ -503,7 +503,7 @@
${statusText}
</td>
<td>
<select class="type-select" onchange="updateType(${w.worker_id}, this.value)">
<select class="type-select" onchange="updateType(${w.user_id}, this.value)">
${attendanceTypes.map(t => `
<option value="${t.value}" ${s.type === t.value ? 'selected' : ''}>${t.label}</option>
`).join('')}
@@ -513,7 +513,7 @@
<td class="hours-cell">
${showOvertimeInput ? `
<input type="number" class="overtime-input" value="${s.overtimeHours}" min="0" max="8" step="0.5"
onchange="updateOvertime(${w.worker_id}, this.value)">
onchange="updateOvertime(${w.user_id}, this.value)">
` : '-'}
</td>
<td class="hours-cell"><strong>${totalHours}h</strong></td>
@@ -551,9 +551,9 @@
function setAllNormal() {
workers.forEach(w => {
workStatus[w.worker_id].type = 'normal';
workStatus[w.worker_id].hours = 8;
workStatus[w.worker_id].overtimeHours = 0;
workStatus[w.user_id].type = 'normal';
workStatus[w.user_id].hours = 8;
workStatus[w.user_id].overtimeHours = 0;
});
render();
updateSummary();
@@ -638,14 +638,14 @@
// 미입사자 제외하고 저장할 데이터 생성
const recordsToSave = workers
.filter(w => !workStatus[w.worker_id]?.isNotHired)
.filter(w => !workStatus[w.user_id]?.isNotHired)
.map(w => {
const s = workStatus[w.worker_id];
const s = workStatus[w.user_id];
const totalHours = s.type === 'overtime' ? s.hours + s.overtimeHours : s.hours;
return {
record_date: date,
worker_id: w.worker_id,
user_id: w.user_id,
attendance_type_id: typeIdMap[s.type] || 1,
vacation_type_id: vacationTypeIdMap[s.type] || null,
total_work_hours: totalHours,
@@ -674,8 +674,8 @@
alert(`${ok}명 저장 완료`);
isAlreadySaved = true;
workers.forEach(w => {
if (workStatus[w.worker_id]) {
workStatus[w.worker_id].isSaved = true;
if (workStatus[w.user_id]) {
workStatus[w.user_id].isSaved = true;
}
});
render();