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

@@ -0,0 +1,318 @@
/**
* Daily Work Report - Module Loader
* 작업보고서 모듈을 초기화하고 연결하는 메인 진입점
*
* 로드 순서:
* 1. state.js - 전역 상태 관리
* 2. utils.js - 유틸리티 함수
* 3. api.js - API 클라이언트
* 4. index.js - 이 파일 (메인 컨트롤러)
*/
class DailyWorkReportController {
constructor() {
this.state = window.DailyWorkReportState;
this.api = window.DailyWorkReportAPI;
this.utils = window.DailyWorkReportUtils;
this.initialized = false;
console.log('[Controller] DailyWorkReportController 생성');
}
/**
* 초기화
*/
async init() {
if (this.initialized) {
console.log('[Controller] 이미 초기화됨');
return;
}
console.log('[Controller] 초기화 시작...');
try {
// 이벤트 리스너 설정
this.setupEventListeners();
// 기본 데이터 로드
await this.api.loadAllData();
// TBM 탭이 기본
await this.switchTab('tbm');
this.initialized = true;
console.log('[Controller] 초기화 완료');
} catch (error) {
console.error('[Controller] 초기화 실패:', error);
window.showMessage?.('초기화 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
/**
* 이벤트 리스너 설정
*/
setupEventListeners() {
// 탭 버튼
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
if (tbmBtn) {
tbmBtn.addEventListener('click', () => this.switchTab('tbm'));
}
if (completedBtn) {
completedBtn.addEventListener('click', () => this.switchTab('completed'));
}
// 완료 보고서 날짜 변경
const completedDateInput = document.getElementById('completedReportDate');
if (completedDateInput) {
completedDateInput.addEventListener('change', () => this.loadCompletedReports());
}
console.log('[Controller] 이벤트 리스너 설정 완료');
}
/**
* 탭 전환
*/
async switchTab(tab) {
this.state.setCurrentTab(tab);
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
const tbmSection = document.getElementById('tbmReportSection');
const completedSection = document.getElementById('completedReportSection');
// 모든 탭 버튼 비활성화
tbmBtn?.classList.remove('active');
completedBtn?.classList.remove('active');
// 모든 섹션 숨기기
if (tbmSection) tbmSection.style.display = 'none';
if (completedSection) completedSection.style.display = 'none';
// 선택된 탭 활성화
if (tab === 'tbm') {
tbmBtn?.classList.add('active');
if (tbmSection) tbmSection.style.display = 'block';
await this.loadTbmData();
} else if (tab === 'completed') {
completedBtn?.classList.add('active');
if (completedSection) completedSection.style.display = 'block';
// 오늘 날짜로 초기화
const dateInput = document.getElementById('completedReportDate');
if (dateInput) {
dateInput.value = this.utils.getKoreaToday();
}
await this.loadCompletedReports();
}
}
/**
* TBM 데이터 로드
*/
async loadTbmData() {
try {
await this.api.loadIncompleteTbms();
await this.api.loadDailyIssuesForTbms();
// 렌더링은 기존 함수 사용 (점진적 마이그레이션)
if (typeof window.renderTbmWorkList === 'function') {
window.renderTbmWorkList();
}
} catch (error) {
console.error('[Controller] TBM 데이터 로드 오류:', error);
window.showMessage?.('TBM 데이터를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* 완료 보고서 로드
*/
async loadCompletedReports() {
try {
const dateInput = document.getElementById('completedReportDate');
const date = dateInput?.value || this.utils.getKoreaToday();
const reports = await this.api.loadCompletedReports(date);
// 렌더링은 기존 함수 사용
if (typeof window.renderCompletedReports === 'function') {
window.renderCompletedReports(reports);
}
} catch (error) {
console.error('[Controller] 완료 보고서 로드 오류:', error);
window.showMessage?.('완료 보고서를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* TBM 작업보고서 제출
*/
async submitTbmWorkReport(index) {
try {
const tbm = this.state.incompleteTbms[index];
if (!tbm) {
throw new Error('TBM 데이터를 찾을 수 없습니다.');
}
// 유효성 검사
const totalHoursInput = document.getElementById(`totalHours_${index}`);
const totalHours = parseFloat(totalHoursInput?.value);
if (!totalHours || totalHours <= 0) {
window.showMessage?.('작업시간을 입력해주세요.', 'warning');
return;
}
// 부적합 시간 계산
const defects = this.state.tempDefects[index] || [];
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
const regularHours = totalHours - errorHours;
if (regularHours < 0) {
window.showMessage?.('부적합 시간이 총 작업시간을 초과할 수 없습니다.', 'warning');
return;
}
// API 데이터 구성
const user = this.state.getCurrentUser();
const reportData = {
tbm_session_id: tbm.session_id,
tbm_assignment_id: tbm.assignment_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.work_type_id,
report_date: this.utils.formatDateForApi(tbm.session_date),
total_hours: totalHours,
regular_hours: regularHours,
error_hours: errorHours,
work_status_id: errorHours > 0 ? 2 : 1,
created_by: user?.user_id || user?.id,
defects: defects.map(d => ({
category_id: d.category_id,
item_id: d.item_id,
issue_report_id: d.issue_report_id,
defect_hours: d.defect_hours,
note: d.note
}))
};
const result = await this.api.submitTbmWorkReport(reportData);
window.showSaveResultModal?.(
'success',
'제출 완료',
`${tbm.worker_name}의 작업보고서가 제출되었습니다.`
);
// 목록 새로고침
await this.loadTbmData();
} catch (error) {
console.error('[Controller] 제출 오류:', error);
window.showSaveResultModal?.(
'error',
'제출 실패',
error.message || '작업보고서 제출 중 오류가 발생했습니다.'
);
}
}
/**
* 세션 일괄 제출
*/
async batchSubmitSession(sessionKey) {
const rows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"][data-type="tbm"]`);
const indices = [];
rows.forEach(row => {
const index = parseInt(row.dataset.index);
const totalHoursInput = document.getElementById(`totalHours_${index}`);
if (totalHoursInput?.value && parseFloat(totalHoursInput.value) > 0) {
indices.push(index);
}
});
if (indices.length === 0) {
window.showMessage?.('제출할 항목이 없습니다. 작업시간을 입력해주세요.', 'warning');
return;
}
const confirmed = confirm(`${indices.length}건의 작업보고서를 일괄 제출하시겠습니까?`);
if (!confirmed) return;
let successCount = 0;
let failCount = 0;
for (const index of indices) {
try {
await this.submitTbmWorkReport(index);
successCount++;
} catch (error) {
failCount++;
console.error(`[Controller] 일괄 제출 오류 (index: ${index}):`, error);
}
}
if (failCount === 0) {
window.showSaveResultModal?.('success', '일괄 제출 완료', `${successCount}건이 성공적으로 제출되었습니다.`);
} else {
window.showSaveResultModal?.('warning', '일괄 제출 부분 완료', `성공: ${successCount}건, 실패: ${failCount}`);
}
}
/**
* 상태 디버그
*/
debug() {
console.log('[Controller] 상태 디버그:');
this.state.debug();
}
}
// 전역 인스턴스 생성
window.DailyWorkReportController = new DailyWorkReportController();
// 하위 호환성: 기존 전역 함수들
window.switchTab = (tab) => window.DailyWorkReportController.switchTab(tab);
window.submitTbmWorkReport = (index) => window.DailyWorkReportController.submitTbmWorkReport(index);
window.batchSubmitTbmSession = (sessionKey) => window.DailyWorkReportController.batchSubmitSession(sessionKey);
// 사용자 정보 함수
window.getUser = () => window.DailyWorkReportState.getUser();
window.getCurrentUser = () => window.DailyWorkReportState.getCurrentUser();
// 날짜 그룹 토글 (UI 함수)
window.toggleDateGroup = function(dateStr) {
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
if (!group) return;
const isExpanded = group.classList.contains('expanded');
const content = group.querySelector('.date-group-content');
const icon = group.querySelector('.date-toggle-icon');
if (isExpanded) {
group.classList.remove('expanded');
group.classList.add('collapsed');
if (content) content.style.display = 'none';
if (icon) icon.textContent = '▶';
} else {
group.classList.remove('collapsed');
group.classList.add('expanded');
if (content) content.style.display = 'block';
if (icon) icon.textContent = '▼';
}
};
// DOMContentLoaded 이벤트에서 초기화
document.addEventListener('DOMContentLoaded', () => {
// 약간의 지연 후 초기화 (다른 스크립트 로드 대기)
setTimeout(() => {
window.DailyWorkReportController.init();
}, 100);
});
console.log('[Module] daily-work-report/index.js 로드 완료');

View File

@@ -0,0 +1,51 @@
// /js/work-report-api.js
import { apiGet, apiPost } from './api-helper.js';
/**
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
* Promise.all을 사용하여 병렬로 API를 호출합니다.
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
*/
export async function getInitialData() {
try {
const [allWorkers, projects, tasks] = await Promise.all([
apiGet('/workers'),
apiGet('/projects'),
apiGet('/tasks')
]);
// 활성화된 작업자만 필터링
const workers = allWorkers.filter(worker => {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
});
// 데이터 형식 검증
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
}
// 작업자 목록은 ID 기준으로 정렬
workers.sort((a, b) => a.user_id - b.user_id);
return { workers, projects, tasks };
} catch (error) {
console.error('초기 데이터 로딩 중 오류 발생:', error);
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
throw error;
}
}
/**
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
* @returns {Promise<object>} - 서버의 응답 결과
*/
export async function createWorkReport(reportData) {
try {
const result = await apiPost('/workreports', reportData);
return result;
} catch (error) {
console.error('작업 보고서 생성 요청 실패:', error);
throw error;
}
}

View File

@@ -0,0 +1,79 @@
// /js/work-report-create.js
import { renderCalendar } from './calendar.js';
import { getInitialData, createWorkReport } from './work-report-api.js';
import { initializeReportTable, getReportData } from './work-report-ui.js';
// 전역 상태 변수
let selectedDate = '';
/**
* 날짜가 선택되었을 때 실행되는 콜백 함수.
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
*/
async function onDateSelect(date) {
selectedDate = date;
const tableBody = document.getElementById('reportBody');
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
try {
const initialData = await getInitialData();
initializeReportTable(initialData);
} catch (error) {
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
}
}
/**
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
* 폼 데이터를 서버에 전송합니다.
*/
async function handleSubmit() {
if (!selectedDate) {
alert('먼저 달력에서 날짜를 선택해주세요.');
return;
}
const reportData = getReportData();
if (!reportData) {
// getReportData 내부에서 이미 alert으로 사용자에게 알림
return;
}
// 각 항목에 선택된 날짜 추가
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '등록 중...';
try {
const result = await createWorkReport(payload);
if (result.success) {
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
} else {
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
}
} catch (error) {
alert('❌ 등록 실패: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '전체 등록';
}
}
/**
* 페이지 초기화 함수
*/
function initializePage() {
renderCalendar('calendar', onDateSelect);
const submitBtn = document.getElementById('submitBtn');
submitBtn.addEventListener('click', handleSubmit);
}
// DOM이 로드되면 페이지 초기화를 시작합니다.
document.addEventListener('DOMContentLoaded', initializePage);

View File

@@ -0,0 +1,141 @@
// /js/work-report-ui.js
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
const DEFAULT_TASK_ID = '15';
/**
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
* @returns {string} - 생성된 HTML 옵션 문자열
*/
function createOptions(items, valueField, textField) {
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
}
/**
* 테이블의 모든 행 번호를 다시 매깁니다.
* @param {HTMLTableSectionElement} tableBody - tbody 요소
*/
function updateRowNumbers(tableBody) {
tableBody.querySelectorAll('tr').forEach((tr, index) => {
tr.cells[0].textContent = index + 1;
});
}
/**
* 하나의 작업 보고서 행(tr)을 생성합니다.
* @param {object} worker - 작업자 정보
* @param {Array} projects - 전체 프로젝트 목록
* @param {Array} tasks - 전체 태스크 목록
* @param {number} index - 행 번호
* @returns {HTMLTableRowElement} - 생성된 tr 요소
*/
function createReportRow(worker, projects, tasks, index) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index + 1}</td>
<td>
<input type="hidden" name="user_id" value="${worker.user_id}">
${worker.worker_name}
</td>
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
<td>
<select name="overtime">
<option value="">없음</option>
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
</select>
</td>
<td>
<select name="work_type">
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
</select>
</td>
<td><input type="text" name="memo" placeholder="메모"></td>
<td><button type="button" class="remove-btn">x</button></td>
`;
// 이벤트 리스너 설정
const workTypeSelect = tr.querySelector('[name="work_type"]');
const projectSelect = tr.querySelector('[name="project_id"]');
const taskSelect = tr.querySelector('[name="task_id"]');
workTypeSelect.addEventListener('change', () => {
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
projectSelect.disabled = isDisabled;
taskSelect.disabled = isDisabled;
if (isDisabled) {
projectSelect.value = DEFAULT_PROJECT_ID;
taskSelect.value = DEFAULT_TASK_ID;
}
});
tr.querySelector('.remove-btn').addEventListener('click', () => {
tr.remove();
updateRowNumbers(tr.parentElement);
});
return tr;
}
/**
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
*/
export function initializeReportTable(initialData) {
const tableBody = document.getElementById('reportBody');
if (!tableBody) return;
tableBody.innerHTML = ''; // 기존 내용 초기화
const { workers, projects, tasks } = initialData;
if (!workers || workers.length === 0) {
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
return;
}
workers.forEach((worker, index) => {
const row = createReportRow(worker, projects, tasks, index);
tableBody.appendChild(row);
});
}
/**
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
*/
export function getReportData() {
const tableBody = document.getElementById('reportBody');
const rows = tableBody.querySelectorAll('tr');
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
alert('등록할 내용이 없습니다.');
return null;
}
const reportData = [];
const workerIds = new Set();
for (const tr of rows) {
const workerId = tr.querySelector('[name="user_id"]').value;
if (workerIds.has(workerId)) {
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
return null;
}
workerIds.add(workerId);
reportData.push({
user_id: workerId,
project_id: tr.querySelector('[name="project_id"]').value,
task_id: tr.querySelector('[name="task_id"]').value,
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
work_details: tr.querySelector('[name="work_type"]').value,
memo: tr.querySelector('[name="memo"]').value
});
}
return reportData;
}

View File

@@ -794,14 +794,14 @@ document.addEventListener('DOMContentLoaded', () => {
// ========== 작업자 연결 기능 ========== //
let departments = [];
let selectedWorkerId = null;
let selectedUserId = null;
// 연결된 작업자 정보 표시 업데이트
function updateLinkedWorkerDisplay(user) {
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
if (!linkedWorkerInfo) return;
if (user.worker_id && user.worker_name) {
if (user.user_id && user.worker_name) {
linkedWorkerInfo.innerHTML = `
<span class="worker-badge">
<span class="worker-name">👤 ${user.worker_name}</span>
@@ -820,7 +820,7 @@ async function openWorkerSelectModal() {
return;
}
selectedWorkerId = currentEditingUser.worker_id || null;
selectedUserId = currentEditingUser.user_id || null;
// 부서 목록 로드
await loadDepartmentsForSelect();
@@ -833,7 +833,7 @@ window.openWorkerSelectModal = openWorkerSelectModal;
// 작업자 선택 모달 닫기
function closeWorkerSelectModal() {
document.getElementById('workerSelectModal').style.display = 'none';
selectedWorkerId = null;
selectedUserId = null;
}
window.closeWorkerSelectModal = closeWorkerSelectModal;
@@ -906,18 +906,18 @@ function renderWorkerListForSelect(workers) {
}
// 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용
const linkedWorkerIds = users
.filter(u => u.worker_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.worker_id);
const linkedUserIds = users
.filter(u => u.user_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.user_id);
container.innerHTML = workers.map(worker => {
const isSelected = selectedWorkerId === worker.worker_id;
const isLinkedToOther = linkedWorkerIds.includes(worker.worker_id);
const linkedUser = isLinkedToOther ? users.find(u => u.worker_id === worker.worker_id) : null;
const isSelected = selectedUserId === worker.user_id;
const isLinkedToOther = linkedUserIds.includes(worker.user_id);
const linkedUser = isLinkedToOther ? users.find(u => u.user_id === worker.user_id) : null;
return `
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.worker_id}, '${worker.worker_name}')`}">
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.user_id}, '${worker.worker_name}')`}">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-info">
<div class="worker-name">${worker.worker_name}</div>
@@ -941,8 +941,8 @@ function getJobTypeName(jobType) {
}
// 작업자 선택
async function selectWorker(workerId, workerName) {
selectedWorkerId = workerId;
async function selectWorker(userId, workerName) {
selectedUserId = userId;
// UI 업데이트
document.querySelectorAll('.worker-select-item').forEach(item => {
@@ -950,7 +950,7 @@ async function selectWorker(workerId, workerName) {
item.querySelector('.select-indicator').textContent = '';
});
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${workerId}"]`);
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${userId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
selectedItem.querySelector('.select-indicator').textContent = '✓';
@@ -959,12 +959,12 @@ async function selectWorker(workerId, workerName) {
// 서버에 저장
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: workerId
user_id: userId
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_id = workerId;
currentEditingUser.user_id = userId;
currentEditingUser.worker_name = workerName;
// 부서 정보도 업데이트
@@ -1003,7 +1003,7 @@ async function unlinkWorker() {
return;
}
if (!currentEditingUser.worker_id) {
if (!currentEditingUser.user_id) {
showToast('연결된 작업자가 없습니다.', 'warning');
closeWorkerSelectModal();
return;
@@ -1015,19 +1015,19 @@ async function unlinkWorker() {
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: null
user_id: null
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_id = null;
currentEditingUser.user_id = null;
currentEditingUser.worker_name = null;
currentEditingUser.department_name = null;
// users 배열 업데이트
const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id);
if (userIndex !== -1) {
users[userIndex] = { ...users[userIndex], worker_id: null, worker_name: null, department_name: null };
users[userIndex] = { ...users[userIndex], user_id: null, worker_name: null, department_name: null };
}
// 표시 업데이트

View File

@@ -322,7 +322,7 @@ async function fetchDailyWorkReports(date) {
async function updateWorkerHours(workerId, date, newHours, reason = '') {
try {
const data = {
worker_id: workerId,
user_id: workerId,
report_date: date,
work_hours: parseFloat(newHours),
modification_reason: reason,
@@ -412,7 +412,7 @@ async function calculateDateStatus(dateStr) {
status = 'missing';
} else {
const hasDiscrepancy = workReports.some(wr => {
const dr = dailyReports.find(d => d.worker_id === wr.worker_id);
const dr = dailyReports.find(d => d.user_id === wr.user_id);
if (!dr) return true;
const expected = calculateExpectedHours(wr.status, wr.overtime_hours);
@@ -607,8 +607,8 @@ async function getWorkersForDate(dateStr) {
// WorkReports 데이터 추가
workReports.forEach(wr => {
workerMap.set(wr.worker_id, {
worker_id: wr.worker_id,
workerMap.set(wr.user_id, {
user_id: wr.user_id,
worker_name: wr.worker_name,
overtime_hours: wr.overtime_hours || 0,
status: wr.status || 'normal',
@@ -621,13 +621,13 @@ async function getWorkersForDate(dateStr) {
// DailyReports 데이터 추가
dailyReports.forEach(dr => {
if (workerMap.has(dr.worker_id)) {
const worker = workerMap.get(dr.worker_id);
if (workerMap.has(dr.user_id)) {
const worker = workerMap.get(dr.user_id);
worker.reported_hours = dr.work_hours;
worker.hasDailyReport = true;
} else {
workerMap.set(dr.worker_id, {
worker_id: dr.worker_id,
workerMap.set(dr.user_id, {
user_id: dr.user_id,
worker_name: dr.worker_name,
overtime_hours: 0,
status: 'normal',
@@ -739,7 +739,7 @@ async function saveEditedWork() {
showMessage('수정 중...', 'loading');
await updateWorkerHours(editingWorker.worker_id, selectedDate, newHours, reason);
await updateWorkerHours(editingWorker.user_id, selectedDate, newHours, reason);
showMessage('✅ 근무시간이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
@@ -767,7 +767,7 @@ async function deleteWorker(worker) {
try {
showMessage('삭제 중...', 'loading');
await deleteWorkerReport(worker.worker_id, selectedDate);
await deleteWorkerReport(worker.user_id, selectedDate);
showMessage('✅ 작업 데이터가 성공적으로 삭제되었습니다!', 'success');
@@ -884,7 +884,7 @@ function renderWorkersList(workers) {
</div>
<div>
<div class="worker-name">${worker.worker_name}</div>
<div class="worker-id">작업자 ID: ${worker.worker_id}</div>
<div class="worker-id">작업자 ID: ${worker.user_id}</div>
</div>
</div>
<div class="status-badge">${getStatusIcon(worker.validationStatus)}</div>

View File

@@ -45,7 +45,7 @@ async function fetchWorkers() {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
});
workers.sort((a, b) => a.worker_id - b.worker_id);
workers.sort((a, b) => a.user_id - b.user_id);
} catch (err) {
alert('작업자 불러오기 실패');
}
@@ -93,14 +93,14 @@ function renderTable(data, year, month, lastDay) {
workers.forEach(w => {
// ✅ 월간 데이터 (표에 표시용)
const recsThisMonth = data.filter(r =>
r.worker_id === w.worker_id &&
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.worker_id === w.worker_id &&
r.user_id === w.user_id &&
new Date(r.date).getFullYear() === +year
);

View File

@@ -34,8 +34,8 @@ export async function getWorkersByDate(date) {
if (reports && reports.length > 0) {
const workerMap = new Map();
reports.forEach(r => {
if (!workerMap.has(r.worker_id)) {
workerMap.set(r.worker_id, { worker_id: r.worker_id, worker_name: r.worker_name });
if (!workerMap.has(r.user_id)) {
workerMap.set(r.user_id, { user_id: r.user_id, worker_name: r.worker_name });
}
});
workers = Array.from(workerMap.values());

View File

@@ -54,7 +54,7 @@ export function renderWorkerList(workers) {
btn.type = 'button';
btn.className = 'btn';
btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id;
btn.dataset.id = worker.user_id;
btn.addEventListener('click', () => btn.classList.toggle('selected'));
DOM.workerList.appendChild(btn);
});
@@ -79,7 +79,7 @@ export function getFormData() {
issue_type_id: DOM.issueTypeSelect.value,
start_time: DOM.timeStart.value,
end_time: DOM.timeEnd.value,
worker_ids: selectedWorkers, // worker_id -> worker_ids 로 명확하게 변경
user_ids: selectedWorkers, // user_id -> user_ids 로 명확하게 변경
};
for (const key in data) {

View File

@@ -792,7 +792,7 @@ const MobileReport = (function() {
const reportData = {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.task_id,
report_date: reportDate,
@@ -895,7 +895,7 @@ const MobileReport = (function() {
data: {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.task_id,
report_date: reportDate,
@@ -974,7 +974,7 @@ const MobileReport = (function() {
const state = window.DailyWorkReportState;
manualCards[id] = {
worker_id: null,
user_id: null,
report_date: getKoreaToday(),
project_id: null,
work_type_id: null,
@@ -1006,9 +1006,9 @@ const MobileReport = (function() {
<button class="m-manual-delete" onclick="MobileReport.removeManualCard('${id}')">&times;</button>
<div class="m-form-group">
<label class="m-form-label">작업자</label>
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','worker_id',this.value)">
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','user_id',this.value)">
<option value="">작업자 선택</option>
${workers.map(w => `<option value="${w.worker_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
${workers.map(w => `<option value="${w.user_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
</select>
</div>
<div class="m-form-row">
@@ -1127,7 +1127,7 @@ const MobileReport = (function() {
const mc = manualCards[id];
if (!mc) return;
const workerId = mc.worker_id || document.getElementById('m_manual_worker_' + id)?.value;
const workerId = mc.user_id || document.getElementById('m_manual_worker_' + id)?.value;
const reportDate = mc.report_date || document.getElementById('m_manual_date_' + id)?.value;
const projectId = mc.project_id || document.getElementById('m_manual_project_' + id)?.value;
const taskId = mc.task_id || document.getElementById('m_manual_task_' + id)?.value;
@@ -1148,7 +1148,7 @@ const MobileReport = (function() {
const reportData = {
report_date: reportDate,
worker_id: parseInt(workerId),
user_id: parseInt(workerId),
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(taskId),

View File

@@ -581,7 +581,7 @@ window.submitTbmWorkReport = async function(index) {
const reportData = {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (직접 작업보고서와 일관성 유지)
report_date: reportDate,
@@ -729,7 +729,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
data: {
tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (일관성 유지)
report_date: reportDate,
@@ -847,7 +847,7 @@ window.addManualWorkRow = function() {
<td>
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
<option value="">작업자 선택</option>
${workers.map(w => `<option value="${escapeHtml(String(w.worker_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
${workers.map(w => `<option value="${escapeHtml(String(w.user_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
</select>
</td>
<td>
@@ -1418,7 +1418,7 @@ window.submitManualWorkReport = async function(manualIndex) {
// 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함
const reportData = {
report_date: reportDate,
worker_id: parseInt(workerId),
user_id: parseInt(workerId),
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨
@@ -1577,7 +1577,7 @@ window.submitAllManualWorkReports = async function() {
// 서비스 레이어가 기대하는 형식으로 변환
const reportData = {
report_date: reportDate,
worker_id: parseInt(workerId),
user_id: parseInt(workerId),
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(taskId),
@@ -2282,7 +2282,7 @@ async function loadTbmTeamForDate(date) {
// 팀 구성 조회
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
if (teamRes && teamRes.success && teamRes.data) {
const teamWorkerIds = teamRes.data.map(m => m.worker_id);
const teamWorkerIds = teamRes.data.map(m => m.user_id);
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`);
return teamWorkerIds;
}
@@ -2334,16 +2334,16 @@ async function populateWorkerGrid() {
btn.type = 'button';
btn.className = 'worker-card';
btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id;
btn.dataset.id = worker.user_id;
// TBM 팀 구성에 포함된 작업자는 자동 선택
if (tbmWorkerIds.includes(worker.worker_id)) {
if (tbmWorkerIds.includes(worker.user_id)) {
btn.classList.add('selected');
selectedWorkers.add(worker.worker_id);
selectedWorkers.add(worker.user_id);
}
btn.addEventListener('click', () => {
toggleWorkerSelection(worker.worker_id, btn);
toggleWorkerSelection(worker.user_id, btn);
});
grid.appendChild(btn);
@@ -2673,12 +2673,12 @@ async function saveWorkReport() {
const failureDetails = [];
for (const workerId of selectedWorkers) {
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
const workerName = workers.find(w => w.user_id == workerId)?.worker_name || '알 수 없음';
// 서버가 기대하는 work_entries 배열 형태로 전송
const requestData = {
report_date: reportDate,
worker_id: parseInt(workerId),
user_id: parseInt(workerId),
work_entries: newWorkEntries.map(entry => ({
project_id: entry.project_id,
task_id: entry.work_type_id, // 서버에서 task_id로 기대

View File

@@ -96,7 +96,7 @@ class DailyWorkReportState extends BaseState {
*/
selectAllWorkers(select = true) {
if (select) {
this.workers.forEach(w => this.selectedWorkers.add(w.worker_id));
this.workers.forEach(w => this.selectedWorkers.add(w.user_id));
} else {
this.selectedWorkers.clear();
}

View File

@@ -110,11 +110,11 @@ function renderWorkerList(workers) {
}
container.innerHTML = workers.map(worker => `
<div class="worker-card ${selectedWorkers.has(worker.worker_id) ? 'selected' : ''}"
onclick="toggleWorkerSelection(${worker.worker_id})">
<div class="worker-card ${selectedWorkers.has(worker.user_id) ? 'selected' : ''}"
onclick="toggleWorkerSelection(${worker.user_id})">
<div class="worker-info-row">
<input type="checkbox" ${selectedWorkers.has(worker.worker_id) ? 'checked' : ''}
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.worker_id})">
<input type="checkbox" ${selectedWorkers.has(worker.user_id) ? 'checked' : ''}
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.user_id})">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-details">
<span class="worker-name">${worker.worker_name}</span>

View File

@@ -23,7 +23,7 @@ async function fetchDashboardStats() {
// 필요한 데이터 형태로 가공 (예시)
return {
today_reports_count: stats.length,
today_workers_count: new Set(stats.map(d => d.worker_id)).size,
today_workers_count: new Set(stats.map(d => d.user_id)).size,
};
} catch (error) {
console.error('대시보드 통계 데이터 로드 실패:', error);

View File

@@ -147,7 +147,7 @@ userForm?.addEventListener('submit', async e => {
password: document.getElementById('password').value.trim(),
name: document.getElementById('name').value.trim(),
access_level: document.getElementById('access_level').value,
worker_id: document.getElementById('worker_id').value || null
user_id: document.getElementById('user_id').value || null
};
try {
@@ -206,13 +206,13 @@ async function loadUsers() {
list.forEach(item => {
item.access_level = accessLabels[item.access_level] || item.access_level;
item.worker_id = item.worker_id || '-';
item.user_id = item.user_id || '-';
// 행 생성
const tr = document.createElement('tr');
// 데이터 컬럼
['user_id', 'username', 'name', 'access_level', 'worker_id'].forEach(key => {
['user_id', 'username', 'name', 'access_level', 'user_id'].forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
@@ -267,7 +267,7 @@ async function loadUsers() {
}
async function loadWorkerOptions() {
const select = document.getElementById('worker_id');
const select = document.getElementById('user_id');
if (!select) return;
try {
@@ -289,8 +289,8 @@ async function loadWorkerOptions() {
if (Array.isArray(workers)) {
workers.forEach(w => {
const opt = document.createElement('option');
opt.value = w.worker_id;
opt.textContent = `${w.worker_name} (${w.worker_id})`;
opt.value = w.user_id;
opt.textContent = `${w.worker_name} (${w.user_id})`;
select.appendChild(opt);
});
}

View File

@@ -80,10 +80,10 @@ async function loadWorkers() {
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, ['worker_id', 'worker_name', 'position'], async w => {
const row = createRow(item, ['user_id', 'worker_name', 'position'], async w => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/workers/${w.worker_id}`, {
const delRes = await fetch(`${API}/workers/${w.user_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});

View File

@@ -255,11 +255,11 @@ function analyzeDashboardData() {
// 작업자별 데이터 그룹화
const workerWorkData = {};
workData.forEach(work => {
const workerId = work.worker_id;
if (!workerWorkData[workerId]) {
workerWorkData[workerId] = [];
const userId = work.user_id;
if (!workerWorkData[userId]) {
workerWorkData[userId] = [];
}
workerWorkData[workerId].push(work);
workerWorkData[userId].push(work);
});
// 전체 통계 계산
@@ -272,7 +272,7 @@ function analyzeDashboardData() {
// 작업자별 상세 분석 (개선된 버전)
const workerAnalysis = workers.map(worker => {
const workerWorks = workerWorkData[worker.worker_id] || [];
const workerWorks = workerWorkData[worker.user_id] || [];
const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
// 작업 유형 분석 (실제 이름으로)
@@ -455,7 +455,7 @@ function createWorkerRow(worker) {
<div class="update-time ${updateClass}">${updateTimeText}</div>
</td>
<td>
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.worker_id}')">
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.user_id}')">
📋 상세
</button>
</td>
@@ -479,7 +479,7 @@ function formatDateTime(date) {
// 작업자 상세 모달 표시 (안전한 버전)
function showWorkerDetailSafe(workerId) {
// 현재 분석된 데이터에서 해당 작업자 찾기
const worker = filteredWorkData.find(w => w.worker_id == workerId);
const worker = filteredWorkData.find(w => w.user_id == workerId);
if (!worker) {
showMessage('작업자 정보를 찾을 수 없습니다.', 'error');
return;

View File

@@ -246,7 +246,7 @@ async function loadWorkData(date) {
// ========== 요약 카드 업데이트 ========== //
function updateSummaryCards() {
// 오늘 작업자 수
const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size;
const todayWorkersCount = new Set(workData.map(w => w.user_id)).size;
updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명');
// 총 작업 시간
@@ -326,7 +326,7 @@ function displayWorkStatus() {
// 작업자별 상황 분석
const workerStatusList = allWorkers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
// 휴가/연차 제외한 실제 작업시간 계산
@@ -453,7 +453,7 @@ function displayWorkStatus() {
else if (worker.status === 'partial') iconKey = 'partial';
return `
<tr data-worker-id="${worker.worker_id}">
<tr data-user-id="${worker.user_id}">
<td data-label="작업자" class="worker-info">
<div class="worker-avatar">
<span>${worker.worker_name.charAt(0)}</span>
@@ -492,7 +492,7 @@ function displayWorkStatus() {
<td data-label="액션" class="worker-actions">
<div class="action-buttons">
<button class="action-btn btn-edit" onclick="openWorkerModal(${worker.worker_id}, '${worker.worker_name}')" title="작업입력">
<button class="action-btn btn-edit" onclick="openWorkerModal(${worker.user_id}, '${worker.worker_name}')" title="작업입력">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
@@ -500,7 +500,7 @@ function displayWorkStatus() {
<span class="action-text">작업입력</span>
</button>
${worker.vacationType ? `
<button class="action-btn btn-vacation" onclick="handleVacation(${worker.worker_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}">
<button class="action-btn btn-vacation" onclick="handleVacation(${worker.user_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
@@ -511,7 +511,7 @@ function displayWorkStatus() {
</button>
` : ''}
${worker.status === 'overtime-warning' ? `
<button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.worker_id})" title="정상확인">
<button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.user_id})" title="정상확인">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
@@ -563,16 +563,16 @@ function displayWorkersAsCards(workers) {
elements.workersContainer.innerHTML = `
<div class="workers-grid grid grid-cols-4">
${workers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
// 휴가/연차 제외한 실제 작업시간 계산
const actualWorkHours = todayWork
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const hasError = todayWork.some(w => w.work_status_id === 2);
// 정규 작업과 에러 작업 건수 분리
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
@@ -631,7 +631,7 @@ function displayWorkersAsList(workers) {
</thead>
<tbody>
${workers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const hasError = todayWork.some(w => w.work_status_id === 2);
@@ -852,7 +852,7 @@ async function processVacation(workerId, vacationType, hours) {
// 휴가용 작업 보고서 생성 (특별한 작업 유형으로)
const vacationReport = {
report_date: selectedDate,
worker_id: workerId,
user_id: workerId,
project_id: 1, // 기본 프로젝트 (휴가용)
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
work_status_id: 1, // 정상 상태
@@ -887,10 +887,10 @@ async function processOvertimeConfirmation(workerId) {
// 새로운 근태 관리 API 사용
const overtimeData = {
worker_id: workerId,
user_id: workerId,
date: selectedDate
};
const response = await window.apiCall('/attendance/overtime/approve', 'POST', overtimeData);
if (response.success) {
@@ -1104,7 +1104,7 @@ async function loadModalData() {
async function loadModalExistingWork() {
try {
const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`);
const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&user_id=${currentModalWorker.id}`);
modalExistingWork = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('기존 작업 로드 오류:', error);
@@ -1258,7 +1258,7 @@ async function saveModalNewWork() {
const workData = {
report_date: currentModalWorker.date,
worker_id: currentModalWorker.id,
user_id: currentModalWorker.id,
work_entries: [{
project_id: parseInt(projectId),
task_id: parseInt(workTypeId), // work_type_id를 task_id로 매핑
@@ -1332,7 +1332,7 @@ async function handleModalVacation(vacationType) {
try {
// 새로운 근태 관리 API 사용
const vacationData = {
worker_id: currentModalWorker.id,
user_id: currentModalWorker.id,
date: currentModalWorker.date,
vacation_type: vacation.code
};

View File

@@ -71,7 +71,7 @@ function updateProfileUI(user) {
document.getElementById('username').textContent = user.username || '-';
document.getElementById('fullName').textContent = user.name || '-';
document.getElementById('accessLevel').textContent = accessLevelMap[user.access_level] || user.access_level || '-';
document.getElementById('workerId').textContent = user.worker_id || '연결되지 않음';
document.getElementById('workerId').textContent = user.user_id || '연결되지 않음';
// 날짜 포맷팅
if (user.created_at) {

View File

@@ -80,7 +80,7 @@ export function updateFilterOptions(masterData) {
return html;
};
DOM.projectFilter.innerHTML = createOptions(masterData.projects, 'project_id', 'project_name');
DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'worker_id', 'worker_name');
DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'user_id', 'worker_name');
DOM.taskFilter.innerHTML = createOptions(masterData.tasks, 'task_id', 'category');
}

View File

@@ -678,7 +678,7 @@ function showUserEditForm(user) {
</div>
<div class="form-group">
<label for="edit-worker-id">작업자 ID</label>
<input type="number" id="edit-worker-id" value="${user.worker_id || ''}">
<input type="number" id="edit-worker-id" value="${user.user_id || ''}">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
@@ -711,9 +711,9 @@ async function updateUser(userId) {
role: document.getElementById('edit-role').value,
access_level: document.getElementById('edit-role').value,
is_active: parseInt(document.getElementById('edit-is-active').value),
worker_id: document.getElementById('edit-worker-id').value || null
user_id: document.getElementById('edit-worker-id').value || null
};
const response = await apiRequest(`/api/system/users/${userId}`, 'PUT', formData);
if (response.success) {
@@ -840,7 +840,7 @@ async function createUser() {
email: document.getElementById('create-email').value || null,
role: document.getElementById('create-role').value,
access_level: document.getElementById('create-role').value,
worker_id: document.getElementById('create-worker-id').value || null
user_id: document.getElementById('create-worker-id').value || null
};
const response = await apiRequest('/api/system/users', 'POST', formData);

View File

@@ -15,8 +15,8 @@
sessionDate: null,
leaderId: null,
leaderName: '',
workers: new Set(), // worker_id Set
workerNames: {}, // { worker_id: worker_name }
workers: new Set(), // user_id Set
workerNames: {}, // { user_id: worker_name }
projectId: null,
projectName: '',
workTypeId: null,
@@ -41,10 +41,10 @@
W.sessionDate = window.TbmUtils.getTodayKST();
var user = window.TbmState.getUser();
if (user) {
if (user.worker_id) {
var worker = window.TbmState.allWorkers.find(function(w) { return w.worker_id === user.worker_id; });
if (user.user_id) {
var worker = window.TbmState.allWorkers.find(function(w) { return w.user_id === user.user_id; });
if (worker) {
W.leaderId = worker.worker_id;
W.leaderId = worker.user_id;
W.leaderName = worker.worker_name;
} else {
W.leaderName = user.name || '';
@@ -203,7 +203,7 @@
W.todayAssignments = {};
res.data.forEach(function(a) {
if (a.sessions && a.sessions.length > 0) {
W.todayAssignments[a.worker_id] = a;
W.todayAssignments[a.user_id] = a;
}
});
} else {
@@ -216,14 +216,14 @@
}
var workerCards = workers.map(function(w) {
var selected = W.workers.has(w.worker_id) ? ' selected' : '';
var assignment = W.todayAssignments[w.worker_id];
var selected = W.workers.has(w.user_id) ? ' selected' : '';
var assignment = W.todayAssignments[w.user_id];
var assigned = assignment && assignment.total_hours >= 8;
var partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
var badgeHtml = '';
var disabledClass = '';
var onclick = 'toggleWorker(' + w.worker_id + ')';
var onclick = 'toggleWorker(' + w.user_id + ')';
if (assigned) {
// 종일 배정됨 - 선택 불가
@@ -238,7 +238,7 @@
return '<div class="worker-card' + selected + disabledClass + '"' +
(onclick ? ' onclick="' + onclick + '"' : '') +
' data-wid="' + w.worker_id + '"' +
' data-wid="' + w.user_id + '"' +
' style="' + (assigned ? 'opacity:0.5; pointer-events:none;' : '') + '">' +
'<div class="worker-check">&#10003;</div>' +
'<div class="worker-info">' +
@@ -272,7 +272,7 @@
delete W.workerNames[workerId];
} else {
W.workers.add(workerId);
var w = window.TbmState.allWorkers.find(function(x) { return x.worker_id === workerId; });
var w = window.TbmState.allWorkers.find(function(x) { return x.user_id === workerId; });
if (w) W.workerNames[workerId] = w.worker_name;
}
var card = document.querySelector('[data-wid="' + workerId + '"]');
@@ -284,7 +284,7 @@
window.toggleAllWorkers = function() {
var workers = window.TbmState.allWorkers;
var availableWorkers = workers.filter(function(w) {
var a = W.todayAssignments && W.todayAssignments[w.worker_id];
var a = W.todayAssignments && W.todayAssignments[w.user_id];
return !(a && a.total_hours >= 8);
});
if (W.workers.size === availableWorkers.length) {
@@ -292,8 +292,8 @@
W.workerNames = {};
} else {
availableWorkers.forEach(function(w) {
W.workers.add(w.worker_id);
W.workerNames[w.worker_id] = w.worker_name;
W.workers.add(w.user_id);
W.workerNames[w.user_id] = w.worker_name;
});
}
renderStepWorkers(document.getElementById('stepContainer'));
@@ -522,7 +522,7 @@
// 1. TBM 세션 생성
var sessionData = {
session_date: W.sessionDate,
leader_id: leaderId
leader_user_id: leaderId
};
var response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
@@ -536,7 +536,7 @@
var members = [];
W.workers.forEach(function(wid) {
members.push({
worker_id: wid,
user_id: wid,
project_id: W.projectId,
work_type_id: W.workTypeId,
task_id: null,

View File

@@ -124,10 +124,10 @@
function isMySession(s) {
var userId = currentUser.user_id;
var workerId = currentUser.worker_id;
var workerId = currentUser.user_id;
var userName = currentUser.name;
return (userId && String(s.created_by) === String(userId)) ||
(workerId && String(s.leader_id) === String(workerId)) ||
(workerId && String(s.leader_user_id) === String(workerId)) ||
(userName && s.created_by_name === userName);
}
@@ -645,7 +645,7 @@
var wpId = document.getElementById('de_wp_' + i).value || null;
members.push({
worker_id: m.worker_id,
user_id: m.user_id,
project_id: m.project_id || deSession.project_id || null,
work_type_id: m.work_type_id || deSession.work_type_id || null,
task_id: taskId ? parseInt(taskId) : null,
@@ -798,7 +798,7 @@
if (type === 'early' && (!hours || hours <= 0)) {
window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return;
}
attendanceData.push({ worker_id: completeTeamMembers[i].worker_id, attendance_type: type, attendance_hours: hours });
attendanceData.push({ user_id: completeTeamMembers[i].user_id, attendance_type: type, attendance_hours: hours });
}
var btn = document.getElementById('completeSheetBtn');
@@ -996,7 +996,7 @@
// 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지)
await window.TbmAPI.updateTeamMember(deSessionId, {
worker_id: m.worker_id,
user_id: m.user_id,
project_id: m.project_id || null,
work_type_id: m.work_type_id || null,
task_id: m.task_id || null,
@@ -1009,7 +1009,7 @@
// 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능)
await window.TbmAPI.splitAssignment(deSessionId, {
worker_id: m.worker_id,
user_id: m.user_id,
work_hours: remainHoursKeep,
project_id: newProjectId,
work_type_id: newWorkTypeId
@@ -1033,7 +1033,7 @@
// transfer API 호출
var res = await window.TbmAPI.transfer({
transfer_type: 'send',
worker_id: m.worker_id,
user_id: m.user_id,
source_session_id: deSessionId,
dest_session_id: splitTargetSessionId,
hours: remainHours,
@@ -1094,7 +1094,7 @@
if (!myDraftSession) {
btnHtml = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>';
} else {
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.worker_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.user_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
}
html += '<div class="pull-member-item">' +
@@ -1129,7 +1129,7 @@
};
window.startPull = async function(workerId, workerName, maxHours) {
pullWorker = { worker_id: workerId, worker_name: workerName, max_hours: maxHours };
pullWorker = { user_id: workerId, worker_name: workerName, max_hours: maxHours };
document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기';
document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능';
document.getElementById('pullHoursInput').value = maxHours;
@@ -1167,7 +1167,7 @@
var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null;
var res = await window.TbmAPI.transfer({
transfer_type: 'pull',
worker_id: pullWorker.worker_id,
user_id: pullWorker.user_id,
source_session_id: pullSessionId,
dest_session_id: myDraftSession.session_id,
hours: hours,
@@ -1230,13 +1230,13 @@
// 현재 세션 리더를 제외한 반장/그룹장 목록
var leaders = workers.filter(function(w) {
return (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.worker_id !== handoverSession.leader_id;
w.user_id !== handoverSession.leader_user_id;
});
var leaderSelect = document.getElementById('handoverLeaderId');
leaderSelect.innerHTML = '<option value="">반장 선택...</option>' +
leaders.map(function(w) {
return '<option value="' + w.worker_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
return '<option value="' + w.user_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
}).join('');
// 인계할 팀원 체크리스트
@@ -1246,7 +1246,7 @@
} else {
listEl.innerHTML = team.map(function(m) {
return '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem; cursor:pointer;">' +
'<input type="checkbox" class="handover-worker-cb" value="' + m.worker_id + '" checked style="width:16px; height:16px;">' +
'<input type="checkbox" class="handover-worker-cb" value="' + m.user_id + '" checked style="width:16px; height:16px;">' +
'<span style="font-weight:500; font-size:0.875rem;">' + esc(m.worker_name) + '</span>' +
'<span style="font-size:0.75rem; color:#6b7280; margin-left:auto;">' + (m.job_type || '') + '</span>' +
'</label>';
@@ -1301,13 +1301,13 @@
var handoverData = {
session_id: handoverSessionId,
from_leader_id: handoverSession.leader_id,
to_leader_id: toLeaderId,
from_leader_user_id: handoverSession.leader_user_id,
to_leader_user_id: toLeaderId,
handover_date: today,
handover_time: now,
reason: '모바일 인계',
handover_notes: notes,
worker_ids: workerIds
user_ids: workerIds
};
var res = await window.TbmAPI.saveHandover(handoverData);

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

View File

@@ -18,7 +18,7 @@ class TbmAPI {
// 현재 로그인한 사용자 정보 가져오기
const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}');
this.state.currentUser = userInfo;
console.log('👤 로그인 사용자:', this.state.currentUser, 'worker_id:', this.state.currentUser?.worker_id);
console.log('👤 로그인 사용자:', this.state.currentUser, 'user_id:', this.state.currentUser?.user_id);
// 병렬로 데이터 로드
await Promise.all([

View File

@@ -77,7 +77,7 @@ class TbmState extends BaseState {
*/
addWorkerToList(worker) {
this.workerTaskList.push({
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
job_type: worker.job_type,
tasks: [this.createEmptyTaskLine()]

View File

@@ -81,7 +81,7 @@ async function loadWorkers() {
const selectWorker = document.getElementById('individualWorker');
workers.forEach(worker => {
const option = document.createElement('option');
option.value = worker.worker_id;
option.value = worker.user_id;
option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`;
selectWorker.appendChild(option);
});
@@ -293,7 +293,7 @@ async function autoCalculateAnnualLeave() {
}
// 작업자의 입사일 조회
const worker = workers.find(w => w.worker_id == workerId);
const worker = workers.find(w => w.user_id == workerId);
if (!worker || !worker.hire_date) {
showToast('작업자의 입사일 정보가 없습니다', 'error');
return;
@@ -308,7 +308,7 @@ async function autoCalculateAnnualLeave() {
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: workerId,
user_id: workerId,
hire_date: worker.hire_date,
year: year
})
@@ -369,7 +369,7 @@ async function submitIndividualVacation() {
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: workerId,
user_id: workerId,
vacation_type_id: typeId,
year: year,
total_days: parseFloat(totalDays),
@@ -520,7 +520,7 @@ async function previewBulkAllocation() {
const hireDate = worker.hire_date;
if (!hireDate) {
return {
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
hire_date: '-',
years_worked: '-',
@@ -534,7 +534,7 @@ async function previewBulkAllocation() {
const yearsWorked = calculateYearsWorked(hireDate, year);
return {
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
hire_date: hireDate,
years_worked: yearsWorked,
@@ -656,7 +656,7 @@ async function submitBulkAllocation() {
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: item.worker_id,
user_id: item.user_id,
hire_date: item.hire_date,
year: year
})

View File

@@ -445,7 +445,7 @@ class WorkAnalysisTableRenderer {
return workerData.map(worker => {
// 해당 작업자의 작업 데이터 필터링
const workerWork = recentWorkData ?
recentWorkData.filter(work => work.worker_id === worker.worker_id) : [];
recentWorkData.filter(work => work.user_id === worker.user_id) : [];
// 프로젝트별로 그룹화
const projectMap = new Map();

View File

@@ -47,7 +47,7 @@ async function loadReports() {
return;
}
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name]));
const nameMap = Object.fromEntries(workers.map(w => [w.user_id, w.worker_name]));
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
// const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`])); // tasks 테이블 삭제됨
@@ -56,7 +56,7 @@ async function loadReports() {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td>${nameMap[r.worker_id] || r.worker_id}</td>
<td>${nameMap[r.user_id] || r.user_id}</td>
<td><select data-id="project">
${projects.map(p =>
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
@@ -107,7 +107,7 @@ async function loadReports() {
const payload = {
date: formatDate(r.date), // 날짜 형식 변환
worker_id: r.worker_id, // 기존 작업자 ID 유지
user_id: r.user_id, // 기존 작업자 ID 유지
project_id: Number(projectId),
task_id: Number(taskId),
overtime_hours: overtimeHours ? Number(overtimeHours) : null,

View File

@@ -142,8 +142,8 @@ class WorkReportReviewManager {
}
dummyAttendance.push({
id: `att_${worker.worker_id}_${dateStr}`,
worker_id: worker.worker_id,
id: `att_${worker.user_id}_${dateStr}`,
user_id: worker.user_id,
worker_name: worker.worker_name,
date: dateStr,
attendance_type: attendanceType,
@@ -196,7 +196,7 @@ class WorkReportReviewManager {
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (workerId) params.append('worker_id', workerId);
if (workerId) params.append('user_id', workerId);
if (projectId) params.append('project_id', projectId);
// 페이지네이션 (일단 많이 가져오기)
@@ -250,26 +250,26 @@ class WorkReportReviewManager {
// 각 보고서에 대해 근무시간 검증
this.reports.forEach(report => {
const attendance = this.attendanceData.find(att =>
att.worker_id === report.worker_id && att.date === report.report_date
att.user_id === report.user_id && att.date === report.report_date
);
if (attendance) {
const expectedHours = attendance.expected_hours;
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date);
const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
report.expected_hours = expectedHours;
report.actual_hours = actualHours;
report.attendance_type = attendance.attendance_type;
report.hours_status = this.getHoursStatus(actualHours, expectedHours);
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`);
report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
} else {
// 휴가 정보가 없으면 기본 8시간으로 가정
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date);
const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
report.expected_hours = 8;
report.actual_hours = actualHours;
report.attendance_type = 'NORMAL';
report.hours_status = this.getHoursStatus(actualHours, 8);
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`);
report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
}
});
}
@@ -277,7 +277,7 @@ class WorkReportReviewManager {
getWorkerDailyHours(workerId, date) {
// 해당 작업자의 특정 날짜 총 작업시간 계산
return this.reports
.filter(r => r.worker_id === workerId && r.report_date === date)
.filter(r => r.user_id === workerId && r.report_date === date)
.reduce((sum, r) => sum + (r.work_hours || 0), 0);
}
@@ -315,9 +315,9 @@ class WorkReportReviewManager {
(Math.random() * 6 + 2); // 정상 시간 (2-8시간)
dummyData.push({
id: 1000000 + i * 100 + worker.worker_id * 10 + j, // 고유 정수 ID
id: 1000000 + i * 100 + worker.user_id * 10 + j, // 고유 정수 ID
report_date: dateStr,
worker_id: worker.worker_id,
user_id: worker.user_id,
worker_name: worker.worker_name,
project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1,
project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown',
@@ -344,7 +344,7 @@ class WorkReportReviewManager {
if (workerSelect) {
workerSelect.innerHTML = '<option value="">전체 작업자</option>';
this.workers.forEach(worker => {
workerSelect.innerHTML += `<option value="${worker.worker_id}">${worker.worker_name}</option>`;
workerSelect.innerHTML += `<option value="${worker.user_id}">${worker.worker_name}</option>`;
});
}
@@ -540,7 +540,7 @@ class WorkReportReviewManager {
const grouped = {};
this.filteredReports.forEach(report => {
const key = `${report.worker_id}_${report.report_date}`;
const key = `${report.user_id}_${report.report_date}`;
if (!grouped[key]) {
grouped[key] = [];
}
@@ -569,7 +569,7 @@ class WorkReportReviewManager {
selectWorkerDate(workerId, date) {
// 해당 작업자의 특정 날짜 보고서들을 선택
const reports = this.filteredReports.filter(r =>
r.worker_id == workerId && r.report_date === date
r.user_id == workerId && r.report_date === date
);
if (reports.length > 0) {
@@ -600,7 +600,7 @@ class WorkReportReviewManager {
// const response = await fetch(`${API}/daily-work-reports/review`, {
// method: 'POST',
// headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
// body: JSON.stringify({ worker_id: workerId, report_date: date, reviewed: true })
// body: JSON.stringify({ user_id: workerId, report_date: date, reviewed: true })
// });
// 현재는 로컬 상태만 업데이트
@@ -608,7 +608,7 @@ class WorkReportReviewManager {
// 해당 보고서들의 검토 상태 업데이트
this.reports.forEach(report => {
if (report.worker_id == workerId && report.report_date === date) {
if (report.user_id == workerId && report.report_date === date) {
report.is_reviewed = true;
}
});
@@ -627,7 +627,7 @@ class WorkReportReviewManager {
}
getWorkerName(workerId) {
const worker = this.workers.find(w => w.worker_id == workerId);
const worker = this.workers.find(w => w.user_id == workerId);
return worker ? worker.worker_name : `작업자${workerId}`;
}
@@ -680,7 +680,7 @@ class WorkReportReviewManager {
<div class="panel-actions">
${!report.is_reviewed && report.hours_status === 'NORMAL' && !allReports.some(r => r.work_status_id === 2) ?
`<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.worker_id}', '${report.report_date}')">
`<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.user_id}', '${report.report_date}')">
✅ 검토완료 처리
</button>` : ''
}

View File

@@ -179,7 +179,7 @@ function processDayData(dateStr, works) {
works.forEach(work => {
dayData.totalHours += parseFloat(work.work_hours || 0);
dayData.workers.add(work.worker_name || work.worker_id);
dayData.workers.add(work.worker_name || work.user_id);
});
const workType = classifyWorkType(dayData.totalHours);
@@ -217,7 +217,7 @@ function renderDayInfo() {
// 작업자별 상세 정보 생성
const workerDetailsHtml = Array.from(data.workers).map(worker => {
const workerWorks = data.details.filter(w => (w.worker_name || w.worker_id) === worker);
const workerWorks = data.details.filter(w => (w.worker_name || w.user_id) === worker);
const workerHours = workerWorks.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const workerWorkItemsHtml = workerWorks.map(work => `
@@ -563,7 +563,7 @@ async function deleteWorkerAllWorks(date, workerName) {
try {
if (!selectedDateData) return;
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.worker_id) === workerName);
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.user_id) === workerName);
if (workerWorks.length === 0) {
showMessage('삭제할 작업이 없습니다.', 'error');

View File

@@ -15,7 +15,7 @@ let existingWork = [];
function getUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
return {
worker_id: urlParams.get('worker_id'),
user_id: urlParams.get('user_id'),
worker_name: decodeURIComponent(urlParams.get('worker_name') || ''),
date: urlParams.get('date') || new Date().toISOString().split('T')[0]
};
@@ -88,7 +88,7 @@ async function initializePage() {
// URL 파라미터 추출
const params = getUrlParams();
currentWorkerId = parseInt(params.worker_id);
currentWorkerId = parseInt(params.user_id);
currentWorkerName = params.worker_name;
selectedDate = params.date;
@@ -189,7 +189,7 @@ async function loadWorkerInfo() {
async function loadExistingWork() {
try {
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&user_id=${currentWorkerId}`);
existingWork = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
} catch (error) {
@@ -398,7 +398,7 @@ async function saveNewWork() {
const workData = {
report_date: selectedDate,
worker_id: currentWorkerId,
user_id: currentWorkerId,
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
@@ -477,7 +477,7 @@ async function handleVacationProcess(vacationType) {
// 휴가용 작업 보고서 생성
const vacationWork = {
report_date: selectedDate,
worker_id: currentWorkerId,
user_id: currentWorkerId,
project_id: 1, // 기본 프로젝트 (휴가용)
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
work_status_id: 1, // 정상 상태

View File

@@ -331,7 +331,7 @@ function renderWorkerList() {
statusText = '사무직';
}
const safeWorkerId = parseInt(worker.worker_id) || 0;
const safeWorkerId = parseInt(worker.user_id) || 0;
const safeWorkerName = escapeHtml(worker.worker_name || '');
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
@@ -406,7 +406,7 @@ function openWorkerModal(workerId = null) {
}
if (workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId);
const worker = allWorkers.find(w => w.user_id === workerId);
if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error');
return;
@@ -416,7 +416,7 @@ function openWorkerModal(workerId = null) {
title.textContent = '작업자 정보 수정';
deleteBtn.style.display = 'inline-flex';
document.getElementById('workerId').value = worker.worker_id;
document.getElementById('workerId').value = worker.user_id;
document.getElementById('workerName').value = worker.worker_name || '';
document.getElementById('jobType').value = worker.job_type || 'worker';
document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : '';
@@ -508,7 +508,7 @@ async function saveWorker() {
// 작업자 삭제 확인
function confirmDeleteWorker(workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId);
const worker = allWorkers.find(w => w.user_id === workerId);
if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error');
return;
@@ -524,7 +524,7 @@ function confirmDeleteWorker(workerId) {
// 작업자 삭제 (모달에서)
function deleteWorker() {
if (currentEditingWorker) {
confirmDeleteWorker(currentEditingWorker.worker_id);
confirmDeleteWorker(currentEditingWorker.user_id);
}
}