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

@@ -394,11 +394,11 @@ function openNewTbmModal() {
}
// 입력자 자동 설정 (readonly)
if (currentUser && currentUser.worker_id) {
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
if (currentUser && currentUser.user_id) {
const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
if (worker) {
document.getElementById('leaderName').textContent = worker.worker_name;
document.getElementById('leaderId').value = worker.worker_id;
document.getElementById('leaderId').value = worker.user_id;
}
} else if (currentUser && currentUser.name) {
document.getElementById('leaderName').textContent = currentUser.name;
@@ -444,7 +444,7 @@ async function renderNewTbmWorkerGrid() {
todayAssignmentsMap = {};
assignments.forEach(a => {
if (a.sessions && a.sessions.length > 0) {
todayAssignmentsMap[a.worker_id] = a;
todayAssignmentsMap[a.user_id] = a;
}
});
} catch(e) {
@@ -454,8 +454,8 @@ async function renderNewTbmWorkerGrid() {
}
grid.innerHTML = allWorkers.map(w => {
const checked = selectedWorkersForNewTbm.has(w.worker_id) ? 'checked' : '';
const assignment = todayAssignmentsMap[w.worker_id];
const checked = selectedWorkersForNewTbm.has(w.user_id) ? 'checked' : '';
const assignment = todayAssignmentsMap[w.user_id];
const fullyAssigned = assignment && assignment.total_hours >= 8;
const partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
@@ -474,9 +474,9 @@ async function renderNewTbmWorkerGrid() {
}
return `
<label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.worker_id}" style="${disabledStyle}">
<input type="checkbox" class="new-tbm-worker-cb" data-worker-id="${w.worker_id}" ${checked} ${disabledAttr}
onchange="toggleNewTbmWorker(${w.worker_id}, this.checked)">
<label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.user_id}" style="${disabledStyle}">
<input type="checkbox" class="new-tbm-worker-cb" data-user-id="${w.user_id}" ${checked} ${disabledAttr}
onchange="toggleNewTbmWorker(${w.user_id}, this.checked)">
<span class="tbm-worker-name">${escapeHtml(w.worker_name)}</span>
<span class="tbm-worker-role">${escapeHtml(w.job_type || '작업자')}</span>
${badgeHtml}
@@ -511,9 +511,9 @@ window.toggleNewTbmWorker = toggleNewTbmWorker;
function selectAllNewTbmWorkers() {
allWorkers.forEach(w => {
const a = todayAssignmentsMap && todayAssignmentsMap[w.worker_id];
const a = todayAssignmentsMap && todayAssignmentsMap[w.user_id];
if (a && a.total_hours >= 8) return; // 종일 배정 제외
selectedWorkersForNewTbm.add(w.worker_id);
selectedWorkersForNewTbm.add(w.user_id);
});
document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => {
if (!cb.disabled) cb.checked = true;
@@ -539,12 +539,12 @@ function populateLeaderSelect() {
if (!leaderSelect) return;
// 로그인한 사용자가 작업자와 연결되어 있는지 확인
if (currentUser && currentUser.worker_id) {
if (currentUser && currentUser.user_id) {
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
if (worker) {
const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : '';
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.worker_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.user_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
leaderSelect.disabled = true;
console.log('✅ 입력자 자동 설정:', worker.worker_name);
} else {
@@ -553,7 +553,7 @@ function populateLeaderSelect() {
leaderSelect.disabled = true;
}
} else {
// 관리자 계정 (worker_id가 없음): 드롭다운으로 선택 가능
// 관리자 계정 (user_id가 없음): 드롭다운으로 선택 가능
const leaders = allWorkers.filter(w =>
w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin'
);
@@ -561,7 +561,7 @@ function populateLeaderSelect() {
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
leaders.map(w => {
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
return `<option value="${escapeHtml(w.worker_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
return `<option value="${escapeHtml(w.user_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
}).join('');
leaderSelect.disabled = false;
console.log('✅ 관리자: 입력자 선택 가능');
@@ -646,8 +646,8 @@ async function saveTbmSession() {
let leaderId = parseInt(document.getElementById('leaderId').value);
if (!leaderId || isNaN(leaderId)) {
if (!currentUser.worker_id) {
console.log('📝 관리자 계정: leader_id를 NULL로 설정');
if (!currentUser.user_id) {
console.log('📝 관리자 계정: leader_user_id를 NULL로 설정');
leaderId = null;
} else {
console.error('❌ 입력자 설정 오류');
@@ -658,7 +658,7 @@ async function saveTbmSession() {
const sessionData = {
session_date: document.getElementById('sessionDate').value,
leader_id: leaderId
leader_user_id: leaderId
};
if (!sessionData.session_date) {
@@ -680,7 +680,7 @@ async function saveTbmSession() {
for (const workerData of workerTaskList) {
for (const taskLine of workerData.tasks) {
members.push({
worker_id: workerData.worker_id,
user_id: workerData.user_id,
project_id: taskLine.project_id || null,
work_type_id: taskLine.work_type_id,
task_id: taskLine.task_id,
@@ -732,7 +732,7 @@ async function saveTbmSession() {
const members = [];
selectedWorkersForNewTbm.forEach(workerId => {
members.push({
worker_id: workerId,
user_id: workerId,
project_id: projectId,
work_type_id: workTypeId,
task_id: null,
@@ -894,11 +894,11 @@ function openWorkerSelectionModal() {
if (!workerCardGrid) return;
// 이미 추가된 작업자 ID 세트
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
workerCardGrid.innerHTML = allWorkers.map(worker => {
const isAdded = addedWorkerIds.has(worker.worker_id);
const safeWorkerId = parseInt(worker.worker_id) || 0;
const isAdded = addedWorkerIds.has(worker.user_id);
const safeWorkerId = parseInt(worker.user_id) || 0;
return `
<div id="worker-card-${safeWorkerId}"
onclick="toggleWorkerSelection(${safeWorkerId})"
@@ -923,7 +923,7 @@ window.openWorkerSelectionModal = openWorkerSelectionModal;
// 작업자 선택 토글
function toggleWorkerSelection(workerId) {
// 이미 추가된 작업자는 선택 불가
const alreadyAdded = workerTaskList.some(w => w.worker_id === workerId);
const alreadyAdded = workerTaskList.some(w => w.user_id === workerId);
if (alreadyAdded) return;
const card = document.getElementById(`worker-card-${workerId}`);
@@ -945,11 +945,11 @@ window.toggleWorkerSelection = toggleWorkerSelection;
// 전체 선택
function selectAllWorkersInModal() {
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
allWorkers.forEach(worker => {
if (!addedWorkerIds.has(worker.worker_id)) {
selectedWorkersInModal.add(worker.worker_id);
const card = document.getElementById(`worker-card-${worker.worker_id}`);
if (!addedWorkerIds.has(worker.user_id)) {
selectedWorkersInModal.add(worker.user_id);
const card = document.getElementById(`worker-card-${worker.user_id}`);
if (card) {
card.style.borderColor = '#3b82f6';
card.style.background = '#eff6ff';
@@ -982,10 +982,10 @@ function confirmWorkerSelection() {
}
selectedWorkersInModal.forEach(workerId => {
const worker = allWorkers.find(w => w.worker_id === workerId);
const worker = allWorkers.find(w => w.user_id === workerId);
if (worker) {
workerTaskList.push({
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
job_type: worker.job_type,
tasks: [
@@ -1970,16 +1970,16 @@ async function openTeamCompositionModal(sessionId) {
// 팀원별로 작업 그룹화
teamMembers.forEach(member => {
if (!workerMap.has(member.worker_id)) {
workerMap.set(member.worker_id, {
worker_id: member.worker_id,
if (!workerMap.has(member.user_id)) {
workerMap.set(member.user_id, {
user_id: member.user_id,
worker_name: member.worker_name,
job_type: member.job_type,
tasks: []
});
}
workerMap.get(member.worker_id).tasks.push({
workerMap.get(member.user_id).tasks.push({
task_line_id: generateUUID(),
project_id: member.project_id,
work_type_id: member.work_type_id,
@@ -2003,7 +2003,7 @@ async function openTeamCompositionModal(sessionId) {
// 입력자 표시
if (session.leader_name) {
document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`;
document.getElementById('leaderId').value = session.leader_id;
document.getElementById('leaderId').value = session.leader_user_id;
} else if (session.created_by_name) {
document.getElementById('leaderName').value = `${session.created_by_name} (관리자)`;
document.getElementById('leaderId').value = '';
@@ -2026,7 +2026,7 @@ function updateSelectedWorkers() {
selectedWorkers.clear();
document.querySelectorAll('.worker-checkbox:checked').forEach(cb => {
selectedWorkers.add(parseInt(cb.dataset.workerId));
selectedWorkers.add(parseInt(cb.dataset.userId));
});
const selectedCount = document.getElementById('selectedCount');
@@ -2038,7 +2038,7 @@ function updateSelectedWorkers() {
selectedList.innerHTML = '<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>';
} else {
const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
const worker = allWorkers.find(w => w.worker_id === id);
const worker = allWorkers.find(w => w.user_id === id);
return worker ? `
<span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; background: #3b82f6; color: white; border-radius: 9999px; font-size: 0.875rem;">
${worker.worker_name}
@@ -2053,7 +2053,7 @@ window.updateSelectedWorkers = updateSelectedWorkers;
// 작업자 제거
function removeWorker(workerId) {
const checkbox = document.querySelector(`.worker-checkbox[data-worker-id="${workerId}"]`);
const checkbox = document.querySelector(`.worker-checkbox[data-user-id="${workerId}"]`);
if (checkbox) {
checkbox.checked = false;
updateSelectedWorkers();
@@ -2094,7 +2094,7 @@ async function saveTeamComposition() {
}
const members = Array.from(selectedWorkers).map(workerId => ({
worker_id: workerId
user_id: workerId
}));
try {
@@ -2427,7 +2427,7 @@ async function completeTbmSession() {
}
attendanceData.push({
worker_id: completeModalTeam[i].worker_id,
user_id: completeModalTeam[i].user_id,
attendance_type: type,
attendance_hours: hours
});
@@ -2527,15 +2527,15 @@ async function viewTbmSession(sessionId) {
// 작업자별로 그룹화
const workerMap = new Map();
team.forEach(member => {
if (!workerMap.has(member.worker_id)) {
workerMap.set(member.worker_id, {
if (!workerMap.has(member.user_id)) {
workerMap.set(member.user_id, {
worker_name: member.worker_name,
job_type: member.job_type,
is_present: member.is_present,
tasks: []
});
}
workerMap.get(member.worker_id).tasks.push(member);
workerMap.get(member.user_id).tasks.push(member);
});
teamContainer.style.display = 'flex';
@@ -2692,12 +2692,12 @@ async function openHandoverModal(sessionId) {
const toLeaderSelect = document.getElementById('toLeaderId');
const otherLeaders = allWorkers.filter(w =>
(w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.worker_id !== session.leader_id
w.user_id !== session.leader_user_id
);
toLeaderSelect.innerHTML = '<option value="">인수자 선택...</option>' +
otherLeaders.map(w => `
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>
<option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>
`).join('');
// 인계할 팀원 목록
@@ -2710,7 +2710,7 @@ async function openHandoverModal(sessionId) {
onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'">
<input type="checkbox"
class="handover-worker-checkbox"
value="${member.worker_id}"
value="${member.user_id}"
checked
style="width: 16px; height: 16px; cursor: pointer;">
<span style="font-weight: 500; font-size: 0.875rem;">${member.worker_name}</span>
@@ -2771,9 +2771,9 @@ async function saveHandover() {
}
try {
// 세션 정보 조회 (from_leader_id 가져오기)
// 세션 정보 조회 (from_leader_user_id 가져오기)
const sessionData = await window.TbmAPI.getSession(sessionId);
const fromLeaderId = sessionData?.leader_id;
const fromLeaderId = sessionData?.leader_user_id;
if (!fromLeaderId) {
showToast('세션 정보를 찾을 수 없습니다.', 'error');
@@ -2782,13 +2782,13 @@ async function saveHandover() {
const handoverData = {
session_id: sessionId,
from_leader_id: fromLeaderId,
to_leader_id: toLeaderId,
from_leader_user_id: fromLeaderId,
to_leader_user_id: toLeaderId,
handover_date: handoverDate,
handover_time: handoverTime,
reason: reason,
handover_notes: handoverNotes,
worker_ids: workerIds
user_ids: workerIds
};
const response = await window.TbmAPI.saveHandover(handoverData);
@@ -2855,12 +2855,12 @@ async function executeSplit(memberIdx) {
}
try {
await window.TbmAPI.updateTeamMember(splitModalSessionId, {
worker_id: m.worker_id, project_id: m.project_id, work_type_id: m.work_type_id,
user_id: m.user_id, project_id: m.project_id, work_type_id: m.work_type_id,
task_id: m.task_id, workplace_category_id: m.workplace_category_id, workplace_id: m.workplace_id,
work_detail: m.work_detail, is_present: true, work_hours: splitHours
});
await window.TbmAPI.splitAssignment(splitModalSessionId, {
worker_id: m.worker_id, work_hours: currentHours - splitHours,
user_id: m.user_id, work_hours: currentHours - splitHours,
project_id: m.project_id, work_type_id: m.work_type_id
});
showToast(`${escapeHtml(m.worker_name)} 분할 완료: ${splitHours}h + ${currentHours - splitHours}h`, 'success');
@@ -2934,8 +2934,8 @@ async function togglePullSessionMembers(sessionId, el) {
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.375rem 0; border-bottom:1px solid #f9fafb;">
<span>${escapeHtml(m.worker_name)} <span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span></span>
<div style="display:flex; gap:0.25rem; align-items:center;">
<input type="number" id="pull_h_${sessionId}_${m.worker_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.worker_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
<input type="number" id="pull_h_${sessionId}_${m.user_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.user_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
</div>
</div>`;
}).join('') || '<div style="color:#9ca3af; padding:0.25rem;">팀원 없음</div>';
@@ -2954,7 +2954,7 @@ async function executePull(sourceSessionId, workerId, workerName) {
try {
const res = await window.TbmAPI.transfer({
transfer_type: 'pull',
worker_id: workerId,
user_id: workerId,
source_session_id: sourceSessionId,
dest_session_id: pullModalSessionId,
hours: hours