// tbm.js - TBM 관리 페이지 JavaScript
// 전역 변수: TbmState 프록시 사용 (state.js에서 정의)
// allSessions, todaySessions, allWorkers, allProjects, allWorkTypes, allTasks,
// allSafetyChecks, allWorkplaces, allWorkplaceCategories, currentUser,
// currentSessionId, selectedWorkers, workerTaskList, selectedWorkersInModal,
// currentEditingTaskLine, selectedCategory, selectedWorkplace, selectedCategoryName,
// selectedWorkplaceName, isBulkMode, bulkSelectedWorkers, loadedDaysCount,
// dateGroupedSessions, allLoadedSessions → window 프록시로 접근
// UI 전용 변수 (프록시 없음)
let currentTab = 'tbm-input';
// 모달 스크롤 잠금
let scrollLockY = 0;
let scrollLockCount = 0;
function lockBodyScroll() {
scrollLockCount++;
if (scrollLockCount > 1) return; // 이미 잠금 상태
scrollLockY = window.scrollY;
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
document.body.style.top = `-${scrollLockY}px`;
document.body.classList.add('tbm-modal-open');
}
function unlockBodyScroll() {
scrollLockCount--;
if (scrollLockCount > 0) return; // 아직 열린 모달 있음
scrollLockCount = 0;
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
document.body.style.top = '';
window.scrollTo(0, scrollLockY);
document.body.classList.remove('tbm-modal-open');
}
// ==================== 유틸리티 함수 (CommonUtils 위임) ====================
// getTodayKST, formatDate → window.CommonUtils 사용 (common/utils.js)
function getTodayKST() { return window.CommonUtils.getTodayKST(); }
function formatDate(d) { return window.CommonUtils.formatDate(d); }
// ==================== 페이지 초기화 ====================
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
console.log('🛠️ TBM 관리 페이지 초기화');
// API 함수가 로드될 때까지 대기
let retryCount = 0;
while (!window.apiCall && retryCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
if (!window.apiCall) {
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
return;
}
// 오늘 날짜 설정 (서울 시간대 기준)
const today = getTodayKST();
const tbmDateEl = document.getElementById('tbmDate');
const sessionDateEl = document.getElementById('sessionDate');
if (tbmDateEl) tbmDateEl.value = today;
if (sessionDateEl) sessionDateEl.value = today;
// 이벤트 리스너 설정
setupEventListeners();
// 초기 데이터 로드
await loadInitialData();
await loadTodayOnlyTbm();
});
// 이벤트 리스너 설정
function setupEventListeners() {
// 날짜 선택기 제거됨 - 날짜별 그룹 뷰 사용
}
// 초기 데이터 로드 → TbmAPI 위임
async function loadInitialData() {
await window.TbmAPI.loadInitialData();
// TbmAPI가 TbmState에 데이터를 설정 → 프록시를 통해 전역 변수로 접근 가능
// UI 드롭다운 채우기
populateProjectSelect();
}
// ==================== 탭 전환 ====================
// 탭 전환
function switchTbmTab(tabName) {
currentTab = tabName;
// 탭 버튼 활성화 상태 변경
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 탭 컨텐츠 표시 변경
document.querySelectorAll('.tbm-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
// 탭에 따라 데이터 로드
if (tabName === 'tbm-input') {
loadTodayOnlyTbm();
} else if (tabName === 'tbm-manage') {
loadRecentTbmGroupedByDate();
}
}
window.switchTbmTab = switchTbmTab;
// ==================== TBM 입력 탭 ====================
// 오늘의 TBM만 로드 → TbmAPI 위임
async function loadTodayOnlyTbm() {
await window.TbmAPI.loadTodayOnlyTbm();
displayTodayTbmSessions();
}
window.loadTodayOnlyTbm = loadTodayOnlyTbm;
// 오늘의 TBM 세션 표시
function displayTodayTbmSessions() {
const grid = document.getElementById('todayTbmGrid');
const emptyState = document.getElementById('todayEmptyState');
const todayTotalEl = document.getElementById('todayTotalSessions');
const todayCompletedEl = document.getElementById('todayCompletedSessions');
const todayActiveEl = document.getElementById('todayActiveSessions');
if (todaySessions.length === 0) {
grid.innerHTML = '';
emptyState.style.display = 'flex';
todayTotalEl.textContent = '0';
todayCompletedEl.textContent = '0';
todayActiveEl.textContent = '0';
return;
}
emptyState.style.display = 'none';
const completedCount = todaySessions.filter(s => s.status === 'completed').length;
const activeCount = todaySessions.filter(s => s.status === 'draft').length;
todayTotalEl.textContent = todaySessions.length;
todayCompletedEl.textContent = completedCount;
todayActiveEl.textContent = activeCount;
grid.innerHTML = todaySessions.map(session => createSessionCard(session)).join('');
}
// ==================== TBM 관리 탭 ====================
// 레거시 호환 → api.js의 window alias 사용
// ==================== 날짜별 그룹 TBM 로드 (새 기능) ====================
function isAdminUser() { return window.TbmState.isAdminUser(); }
/**
* 최근 TBM을 날짜별로 그룹화하여 로드 → TbmAPI 위임
*/
async function loadRecentTbmGroupedByDate() {
await window.TbmAPI.loadRecentTbmGroupedByDate();
// TbmState에 dateGroupedSessions, allLoadedSessions가 설정됨
displayTbmGroupedByDate();
updateViewModeIndicator();
}
window.loadRecentTbmGroupedByDate = loadRecentTbmGroupedByDate;
/**
* 뷰 모드 표시 업데이트
*/
function updateViewModeIndicator() {
const indicator = document.getElementById('viewModeIndicator');
const text = document.getElementById('viewModeText');
if (indicator && text) {
if (isAdminUser()) {
indicator.style.display = 'none'; // Admin은 표시 안 함 (전체가 기본)
} else {
indicator.style.display = 'inline-flex';
text.textContent = '내 TBM';
}
}
}
/**
* 날짜별 그룹으로 TBM 표시
*/
function displayTbmGroupedByDate() {
const container = document.getElementById('tbmDateGroupsContainer');
const emptyState = document.getElementById('emptyState');
const totalSessionsEl = document.getElementById('totalSessions');
const completedSessionsEl = document.getElementById('completedSessions');
if (!container) return;
// 날짜별로 정렬 (최신순)
const sortedDates = Object.keys(dateGroupedSessions).sort((a, b) => new Date(b) - new Date(a));
if (sortedDates.length === 0 || allLoadedSessions.length === 0) {
container.innerHTML = '';
if (emptyState) emptyState.style.display = 'flex';
if (totalSessionsEl) totalSessionsEl.textContent = '0';
if (completedSessionsEl) completedSessionsEl.textContent = '0';
return;
}
if (emptyState) emptyState.style.display = 'none';
// 통계 업데이트
const completedCount = allLoadedSessions.filter(s => s.status === 'completed').length;
if (totalSessionsEl) totalSessionsEl.textContent = allLoadedSessions.length;
if (completedSessionsEl) completedSessionsEl.textContent = completedCount;
// 날짜별 그룹 HTML 생성
const today = getTodayKST();
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
container.innerHTML = sortedDates.map(date => {
const sessions = dateGroupedSessions[date];
const dateObj = new Date(date + 'T00:00:00');
const dayName = dayNames[dateObj.getDay()];
const isToday = date === today;
// 날짜 포맷팅 (YYYY-MM-DD → MM월 DD일)
const [year, month, day] = date.split('-');
const displayDate = `${parseInt(month)}월 ${parseInt(day)}일`;
return `
${sessions.map(session => createSessionCard(session)).join('')}
`;
}).join('');
}
// 날짜 그룹 토글
function toggleDateGroup(date) {
const group = document.querySelector(`.tbm-date-group[data-date="${date}"]`);
if (group) {
group.classList.toggle('collapsed');
}
}
window.toggleDateGroup = toggleDateGroup;
/**
* 더 많은 날짜 로드
*/
// loadMoreTbmDays → api.js의 window alias 사용
// 특정 날짜의 TBM 세션 목록 로드 → TbmAPI 위임
async function loadTbmSessionsByDate(date) {
await window.TbmAPI.loadTbmSessionsByDate(date);
displayTbmSessions();
}
// TBM 세션 목록 표시 (관리 탭용) - 레거시 호환 (날짜별 그룹 뷰 사용)
function displayTbmSessions() {
// 새 날짜별 그룹 뷰로 리다이렉트
if (allSessions.length > 0) {
// allSessions를 날짜별로 그룹화
dateGroupedSessions = {};
allSessions.forEach(session => {
const date = formatDate(session.session_date);
if (!dateGroupedSessions[date]) {
dateGroupedSessions[date] = [];
}
dateGroupedSessions[date].push(session);
});
allLoadedSessions = allSessions;
}
displayTbmGroupedByDate();
}
// TBM 세션 카드 생성 (공통)
function createSessionCard(session) {
const statusBadge = {
'draft': '진행중 ',
'completed': '완료 ',
'cancelled': '취소 '
}[session.status] || '';
const leaderName = escapeHtml(session.leader_name || session.created_by_name || '작업 책임자');
const leaderRole = escapeHtml(session.leader_name
? (session.leader_job_type || '작업자')
: '관리자');
const safeSessionId = parseInt(session.session_id) || 0;
// 카드 클릭 동작: draft → 세부 편집, completed → 상세 보기
const onClickAction = session.status === 'draft'
? `openTeamCompositionModal(${safeSessionId})`
: `viewTbmSession(${safeSessionId})`;
return `
프로젝트
${escapeHtml(session.project_name || '-')}
공정
${escapeHtml(session.work_type_name || '-')}
작업장
${escapeHtml(session.work_location || '-')}
팀원
${escapeHtml(session.team_member_names || '')}${session.team_member_names ? '' : '없음'}
${session.status === 'draft' ? `
` : ''}
`;
}
// 새 TBM 모달 열기 (간소화: 프로젝트+공정+작업자만)
function openNewTbmModal() {
if (window.innerWidth <= 768) {
window.location.href = '/pages/work/tbm-create.html';
return;
}
currentSessionId = null;
workerTaskList = [];
selectedWorkersForNewTbm = new Set();
todayAssignmentsMap = null; // 배정 현황 캐시 초기화
document.getElementById('modalTitle').innerHTML = '📝 새 TBM 시작';
document.getElementById('sessionId').value = '';
document.getElementById('tbmForm').reset();
const today = getTodayKST();
document.getElementById('sessionDate').value = today;
// 날짜 표시 업데이트
const [year, month, day] = today.split('-');
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const dateObj = new Date(today);
const dayName = dayNames[dateObj.getDay()];
const sessionDateDisplay = document.getElementById('sessionDateDisplay');
if (sessionDateDisplay) {
sessionDateDisplay.textContent = `${year}년 ${parseInt(month)}월 ${parseInt(day)}일 (${dayName})`;
}
// 입력자 자동 설정 (readonly)
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.user_id;
}
} else if (currentUser && currentUser.name) {
document.getElementById('leaderName').textContent = currentUser.name;
document.getElementById('leaderId').value = '';
}
// 프로젝트 드롭다운 채우기
const projSelect = document.getElementById('newTbmProjectId');
if (projSelect) {
projSelect.innerHTML = '선택 안함 ' +
allProjects.map(p => `${escapeHtml(p.project_name)} (${escapeHtml(p.job_no || '')}) `).join('');
}
// 공정 드롭다운 채우기
const wtSelect = document.getElementById('newTbmWorkTypeId');
if (wtSelect) {
wtSelect.innerHTML = '공정 선택... ' +
allWorkTypes.map(wt => `${escapeHtml(wt.name)} `).join('');
}
// 작업자 체크박스 그리드 렌더링
renderNewTbmWorkerGrid();
document.getElementById('tbmModal').style.display = 'flex';
lockBodyScroll();
}
window.openNewTbmModal = openNewTbmModal;
// 새 TBM 모달용 작업자 선택 세트
let selectedWorkersForNewTbm = new Set();
let todayAssignmentsMap = null; // 당일 배정 현황
// 작업자 그리드 렌더링
async function renderNewTbmWorkerGrid() {
const grid = document.getElementById('newTbmWorkerGrid');
if (!grid) return;
// 당일 배정 현황 로드
if (!todayAssignmentsMap) {
try {
const today = getTodayKST();
const assignments = await window.TbmAPI.loadTodayAssignments(today);
todayAssignmentsMap = {};
assignments.forEach(a => {
if (a.sessions && a.sessions.length > 0) {
todayAssignmentsMap[a.user_id] = a;
}
});
} catch(e) {
console.error('배정 현황 로드 오류:', e);
todayAssignmentsMap = {};
}
}
grid.innerHTML = allWorkers.map(w => {
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;
let badgeHtml = '';
let disabledAttr = '';
let disabledStyle = '';
if (fullyAssigned) {
const leaderNames = assignment.sessions.map(s => s.leader_name || '').join(', ');
badgeHtml = `${escapeHtml(leaderNames)} TBM (${assignment.total_hours}h) `;
disabledAttr = 'disabled';
disabledStyle = 'opacity:0.5; pointer-events:none;';
} else if (partiallyAssigned) {
const remaining = 8 - assignment.total_hours;
badgeHtml = `${remaining}h 가용 `;
}
return `
${escapeHtml(w.worker_name)}
${escapeHtml(w.job_type || '작업자')}
${badgeHtml}
`;
}).join('');
updateNewTbmWorkerCount();
}
function updateNewTbmWorkerCount() {
const countEl = document.getElementById('newTbmWorkerCount');
if (countEl) countEl.textContent = `(${selectedWorkersForNewTbm.size}명)`;
}
function toggleNewTbmWorker(workerId, checked) {
// 종일 배정된 작업자 선택 방지
const a = todayAssignmentsMap && todayAssignmentsMap[workerId];
if (a && a.total_hours >= 8) return;
if (checked) {
selectedWorkersForNewTbm.add(workerId);
} else {
selectedWorkersForNewTbm.delete(workerId);
}
// Update visual state
const label = document.querySelector(`#newTbmWorkerGrid label[data-wid="${workerId}"]`);
if (label) label.classList.toggle('selected', checked);
updateNewTbmWorkerCount();
}
window.toggleNewTbmWorker = toggleNewTbmWorker;
function selectAllNewTbmWorkers() {
allWorkers.forEach(w => {
const a = todayAssignmentsMap && todayAssignmentsMap[w.user_id];
if (a && a.total_hours >= 8) return; // 종일 배정 제외
selectedWorkersForNewTbm.add(w.user_id);
});
document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => {
if (!cb.disabled) cb.checked = true;
});
document.querySelectorAll('#newTbmWorkerGrid label').forEach(l => {
if (l.style.opacity !== '0.5') l.classList.add('selected');
});
updateNewTbmWorkerCount();
}
window.selectAllNewTbmWorkers = selectAllNewTbmWorkers;
function deselectAllNewTbmWorkers() {
selectedWorkersForNewTbm.clear();
document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => { cb.checked = false; });
document.querySelectorAll('#newTbmWorkerGrid label').forEach(l => l.classList.remove('selected'));
updateNewTbmWorkerCount();
}
window.deselectAllNewTbmWorkers = deselectAllNewTbmWorkers;
// 입력자 선택 드롭다운 채우기
function populateLeaderSelect() {
const leaderSelect = document.getElementById('leaderId');
if (!leaderSelect) return;
// 로그인한 사용자가 작업자와 연결되어 있는지 확인
if (currentUser && currentUser.user_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 = `${escapeHtml(worker.worker_name)}${jobTypeText} `;
leaderSelect.disabled = true;
console.log('✅ 입력자 자동 설정:', worker.worker_name);
} else {
// 작업자를 찾을 수 없는 경우
leaderSelect.innerHTML = '입력자를 찾을 수 없습니다 ';
leaderSelect.disabled = true;
}
} else {
// 관리자 계정 (user_id가 없음): 드롭다운으로 선택 가능
const leaders = allWorkers.filter(w =>
w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin'
);
leaderSelect.innerHTML = '입력자 선택... ' +
leaders.map(w => {
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
return `${escapeHtml(w.worker_name)}${jobTypeText} `;
}).join('');
leaderSelect.disabled = false;
console.log('✅ 관리자: 입력자 선택 가능');
}
}
// 프로젝트 선택 드롭다운 채우기
function populateProjectSelect() {
const projectSelect = document.getElementById('projectId');
if (!projectSelect) return;
projectSelect.innerHTML = '프로젝트 선택... ' +
allProjects.map(p => `
${escapeHtml(p.project_name)} (${escapeHtml(p.job_no)})
`).join('');
}
// 공정(Work Type) 선택 드롭다운 채우기
function populateWorkTypeSelect() {
const workTypeSelect = document.getElementById('workTypeId');
if (!workTypeSelect) return;
workTypeSelect.innerHTML = '공정 선택... ' +
allWorkTypes.map(wt => `
${escapeHtml(wt.name)}${wt.category ? ' (' + escapeHtml(wt.category) + ')' : ''}
`).join('');
}
// 작업장 선택 드롭다운 채우기
function populateWorkplaceSelect() {
const workLocationSelect = document.getElementById('workLocation');
if (!workLocationSelect) return;
workLocationSelect.innerHTML = '작업장 선택... ' +
allWorkplaces.map(wp => `
${escapeHtml(wp.workplace_name)}${wp.location ? ' - ' + escapeHtml(wp.location) : ''}
`).join('');
}
// 작업(Task) 선택 드롭다운 채우기 (공정 선택 시 호출)
function loadTasksByWorkType() {
const workTypeId = document.getElementById('workTypeId').value;
const taskSelect = document.getElementById('taskId');
if (!taskSelect) return;
if (!workTypeId) {
taskSelect.innerHTML = '작업 선택... ';
taskSelect.disabled = true;
return;
}
// 선택한 공정에 해당하는 작업만 필터링
const filteredTasks = allTasks.filter(task =>
task.work_type_id === parseInt(workTypeId)
);
taskSelect.disabled = false;
taskSelect.innerHTML = '작업 선택... ' +
filteredTasks.map(task => `
${escapeHtml(task.task_name)}
`).join('');
if (filteredTasks.length === 0) {
taskSelect.innerHTML = '등록된 작업이 없습니다 ';
taskSelect.disabled = true;
}
}
window.loadTasksByWorkType = loadTasksByWorkType;
// TBM 모달 닫기
function closeTbmModal() {
document.getElementById('tbmModal').style.display = 'none';
unlockBodyScroll();
}
window.closeTbmModal = closeTbmModal;
// TBM 세션 저장 (간소화: 프로젝트+공정+작업자, task/workplace=null)
async function saveTbmSession() {
console.log('💾 TBM 저장 시작...');
let leaderId = parseInt(document.getElementById('leaderId').value);
if (!leaderId || isNaN(leaderId)) {
if (!currentUser.user_id) {
console.log('📝 관리자 계정: leader_user_id를 NULL로 설정');
leaderId = null;
} else {
console.error('❌ 입력자 설정 오류');
showToast('입력자 정보가 올바르지 않습니다.', 'error');
return;
}
}
const sessionData = {
session_date: document.getElementById('sessionDate').value,
leader_user_id: leaderId
};
if (!sessionData.session_date) {
showToast('TBM 날짜를 확인해주세요.', 'error');
return;
}
const editingSessionId = document.getElementById('sessionId').value;
// 수정 모드일 때는 기존 openTeamCompositionModal의 workerTaskList를 사용
if (editingSessionId) {
// 기존 수정 모드 로직 (openTeamCompositionModal 경유)
if (workerTaskList.length === 0) {
showToast('최소 1명 이상의 작업자를 추가해주세요.', 'error');
return;
}
const members = [];
for (const workerData of workerTaskList) {
for (const taskLine of workerData.tasks) {
members.push({
user_id: workerData.user_id,
project_id: taskLine.project_id || null,
work_type_id: taskLine.work_type_id,
task_id: taskLine.task_id,
workplace_category_id: taskLine.workplace_category_id || null,
workplace_id: taskLine.workplace_id,
work_detail: taskLine.work_detail || null,
is_present: taskLine.is_present !== undefined ? taskLine.is_present : true
});
}
}
try {
await window.TbmAPI.clearTeamMembers(editingSessionId);
const teamResponse = await window.TbmAPI.addTeamMembers(editingSessionId, members);
if (teamResponse && teamResponse.success) {
showToast(`TBM이 수정되었습니다 (작업자 ${workerTaskList.length}명)`, 'success');
closeTbmModal();
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
await loadRecentTbmGroupedByDate();
}
} else {
throw new Error(teamResponse.message || '팀원 수정에 실패했습니다.');
}
} catch (error) {
console.error('❌ TBM 세션 수정 오류:', error);
showToast('TBM 세션 수정 중 오류가 발생했습니다.', 'error');
}
return;
}
// 생성 모드: 간소화된 새 TBM
const workTypeId = parseInt(document.getElementById('newTbmWorkTypeId')?.value);
const projectId = parseInt(document.getElementById('newTbmProjectId')?.value) || null;
if (!workTypeId) {
showToast('공정을 선택해주세요.', 'error');
return;
}
if (selectedWorkersForNewTbm.size === 0) {
showToast('최소 1명 이상의 작업자를 선택해주세요.', 'error');
return;
}
// 작업자별 members 생성 (task_id, workplace_id = null)
const members = [];
selectedWorkersForNewTbm.forEach(workerId => {
members.push({
user_id: workerId,
project_id: projectId,
work_type_id: workTypeId,
task_id: null,
workplace_category_id: null,
workplace_id: null,
work_detail: null,
is_present: true
});
});
try {
const response = await window.TbmAPI.createTbmSession(sessionData);
if (response && response.success) {
const createdSessionId = response.data.session_id;
console.log('✅ TBM 세션 생성 완료:', createdSessionId);
const teamResponse = await window.TbmAPI.addTeamMembers(createdSessionId, members);
if (teamResponse && teamResponse.success) {
showToast(`TBM이 생성되었습니다 (작업자 ${members.length}명)`, 'success');
closeTbmModal();
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
await loadRecentTbmGroupedByDate();
}
} else {
throw new Error(teamResponse.message || '팀원 추가에 실패했습니다.');
}
} else {
throw new Error(response.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('❌ TBM 세션 저장 오류:', error);
showToast('TBM 세션 저장 중 오류가 발생했습니다.', 'error');
}
}
window.saveTbmSession = saveTbmSession;
// ==================== 작업자 관리 ====================
// generateUUID → api-base.js 전역 사용
// 작업자 카드 리스트 렌더링
function renderWorkerTaskList() {
const listContainer = document.getElementById('workerTaskList');
const emptyState = document.getElementById('workerListEmpty');
if (workerTaskList.length === 0) {
if (emptyState) emptyState.style.display = 'flex';
listContainer.innerHTML = '';
return;
}
if (emptyState) emptyState.style.display = 'none';
listContainer.innerHTML = workerTaskList.map((workerData, workerIndex) => {
return `
👤 ${escapeHtml(workerData.worker_name)}
${escapeHtml(workerData.job_type || '작업자')}
✕ 작업자 제거
${workerData.tasks.map((taskLine, taskIndex) => renderTaskLine(workerData, workerIndex, taskLine, taskIndex)).join('')}
+
이 작업자의 추가 작업 등록
`;
}).join('');
}
// 작업 라인 렌더링
function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
const project = allProjects.find(p => p.project_id === taskLine.project_id);
const workType = allWorkTypes.find(wt => wt.id === taskLine.work_type_id);
const task = allTasks.find(t => t.task_id === taskLine.task_id);
const safeWorkerIndex = parseInt(workerIndex) || 0;
const safeTaskIndex = parseInt(taskIndex) || 0;
const projectText = escapeHtml(project ? project.project_name : '프로젝트 선택');
const workTypeText = escapeHtml(workType ? workType.name : '공정 선택 *');
const taskText = escapeHtml(task ? task.task_name : '작업 선택 *');
const workplaceText = taskLine.workplace_name
? escapeHtml(`${taskLine.workplace_category_name || ''} → ${taskLine.workplace_name}`)
: '작업장 선택 *';
return `
📁 ${projectText}
📍 ${workplaceText}
⚙️ ${workTypeText}
🔧 ${taskText}
${workerData.tasks.length > 1 ? `
− 이 작업 라인 제거
` : ''}
`;
}
window.renderWorkerTaskList = renderWorkerTaskList;
// 작업자 선택 모달 열기
function openWorkerSelectionModal() {
selectedWorkersInModal.clear();
const workerCardGrid = document.getElementById('workerCardGrid');
if (!workerCardGrid) return;
// 이미 추가된 작업자 ID 세트
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
workerCardGrid.innerHTML = allWorkers.map(worker => {
const isAdded = addedWorkerIds.has(worker.user_id);
const safeWorkerId = parseInt(worker.user_id) || 0;
return `
${isAdded ? '✓' : '☐'}
${escapeHtml(worker.worker_name)}
${escapeHtml(worker.job_type || '작업자')}${worker.department ? ' · ' + escapeHtml(worker.department) : ''}
${isAdded ? '
이미 추가됨
' : ''}
`;
}).join('');
document.getElementById('workerSelectionModal').style.display = 'flex';
lockBodyScroll();
}
window.openWorkerSelectionModal = openWorkerSelectionModal;
// 작업자 선택 토글
function toggleWorkerSelection(workerId) {
// 이미 추가된 작업자는 선택 불가
const alreadyAdded = workerTaskList.some(w => w.user_id === workerId);
if (alreadyAdded) return;
const card = document.getElementById(`worker-card-${workerId}`);
if (!card) return;
if (selectedWorkersInModal.has(workerId)) {
selectedWorkersInModal.delete(workerId);
card.style.borderColor = '#e5e7eb';
card.style.background = 'white';
card.innerHTML = card.innerHTML.replace('☑', '☐');
} else {
selectedWorkersInModal.add(workerId);
card.style.borderColor = '#3b82f6';
card.style.background = '#eff6ff';
card.innerHTML = card.innerHTML.replace('☐', '☑');
}
}
window.toggleWorkerSelection = toggleWorkerSelection;
// 전체 선택
function selectAllWorkersInModal() {
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
allWorkers.forEach(worker => {
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';
card.innerHTML = card.innerHTML.replace('☐', '☑');
}
}
});
}
window.selectAllWorkersInModal = selectAllWorkersInModal;
// 전체 해제
function deselectAllWorkersInModal() {
selectedWorkersInModal.forEach(workerId => {
const card = document.getElementById(`worker-card-${workerId}`);
if (card) {
card.style.borderColor = '#e5e7eb';
card.style.background = 'white';
card.innerHTML = card.innerHTML.replace('☑', '☐');
}
});
selectedWorkersInModal.clear();
}
window.deselectAllWorkersInModal = deselectAllWorkersInModal;
// 작업자 선택 확정
function confirmWorkerSelection() {
if (selectedWorkersInModal.size === 0) {
showToast('작업자를 선택해주세요.', 'error');
return;
}
selectedWorkersInModal.forEach(workerId => {
const worker = allWorkers.find(w => w.user_id === workerId);
if (worker) {
workerTaskList.push({
user_id: worker.user_id,
worker_name: worker.worker_name,
job_type: worker.job_type,
tasks: [
{
task_line_id: generateUUID(),
project_id: null,
work_type_id: null,
task_id: null,
workplace_category_id: null,
workplace_id: null,
workplace_category_name: '',
workplace_name: '',
work_detail: null,
is_present: true
}
]
});
}
});
renderWorkerTaskList();
closeWorkerSelectionModal();
showToast(`${selectedWorkersInModal.size}명의 작업자가 추가되었습니다.`, 'success');
}
window.confirmWorkerSelection = confirmWorkerSelection;
// 작업자 선택 모달 닫기
function closeWorkerSelectionModal() {
document.getElementById('workerSelectionModal').style.display = 'none';
unlockBodyScroll();
selectedWorkersInModal.clear();
}
window.closeWorkerSelectionModal = closeWorkerSelectionModal;
// 작업자에 작업 라인 추가
function addTaskLineToWorker(workerIndex) {
workerTaskList[workerIndex].tasks.push({
task_line_id: generateUUID(),
project_id: null,
work_type_id: null,
task_id: null,
workplace_category_id: null,
workplace_id: null,
workplace_category_name: '',
workplace_name: '',
work_detail: null,
is_present: true
});
renderWorkerTaskList();
showToast('작업 라인이 추가되었습니다.', 'success');
}
window.addTaskLineToWorker = addTaskLineToWorker;
// 작업 라인 제거
function removeTaskLine(workerIndex, taskIndex) {
workerTaskList[workerIndex].tasks.splice(taskIndex, 1);
renderWorkerTaskList();
showToast('작업 라인이 제거되었습니다.', 'info');
}
window.removeTaskLine = removeTaskLine;
// 작업자 제거
function removeWorkerFromList(workerIndex) {
const workerName = workerTaskList[workerIndex].worker_name;
workerTaskList.splice(workerIndex, 1);
renderWorkerTaskList();
showToast(`${workerName}이(가) 제거되었습니다.`, 'info');
}
window.removeWorkerFromList = removeWorkerFromList;
// ==================== 일괄 설정 ====================
// 일괄 설정 모달 열기
function openBulkSettingModal() {
if (workerTaskList.length === 0) {
showToast('먼저 작업자를 선택해주세요.', 'error');
return;
}
// 작업자 선택 영역 초기화
bulkSelectedWorkers.clear();
renderBulkWorkerSelection();
// 작업 정보 초기화
document.getElementById('bulkProjectId').value = '';
document.getElementById('bulkWorkTypeId').value = '';
document.getElementById('bulkTaskId').value = '';
document.getElementById('bulkWorkplaceCategoryId').value = '';
document.getElementById('bulkWorkplaceId').value = '';
document.getElementById('bulkProjectBtn').textContent = '📁 프로젝트 선택';
document.getElementById('bulkProjectBtn').classList.remove('btn-primary');
document.getElementById('bulkProjectBtn').classList.add('btn-secondary');
document.getElementById('bulkWorkTypeBtn').textContent = '⚙️ 공정 선택';
document.getElementById('bulkWorkTypeBtn').classList.remove('btn-primary');
document.getElementById('bulkWorkTypeBtn').classList.add('btn-secondary');
document.getElementById('bulkTaskBtn').textContent = '🔧 작업 선택';
document.getElementById('bulkTaskBtn').classList.remove('btn-primary');
document.getElementById('bulkTaskBtn').classList.add('btn-secondary');
document.getElementById('bulkTaskBtn').disabled = true;
document.getElementById('bulkWorkplaceBtn').textContent = '📍 작업장 선택';
document.getElementById('bulkWorkplaceBtn').classList.remove('btn-primary');
document.getElementById('bulkWorkplaceBtn').classList.add('btn-secondary');
document.getElementById('bulkSettingModal').style.display = 'flex';
lockBodyScroll();
}
window.openBulkSettingModal = openBulkSettingModal;
// 일괄 설정용 작업자 선택 영역 렌더링
function renderBulkWorkerSelection() {
const container = document.getElementById('bulkWorkerSelection');
if (!container) return;
container.innerHTML = workerTaskList.map((workerData, index) => {
const isSelected = bulkSelectedWorkers.has(index);
return `
${workerData.worker_name}
`;
}).join('');
}
window.renderBulkWorkerSelection = renderBulkWorkerSelection;
// 일괄 설정용 작업자 선택 토글
function toggleBulkWorkerSelection(workerIndex) {
if (bulkSelectedWorkers.has(workerIndex)) {
bulkSelectedWorkers.delete(workerIndex);
} else {
bulkSelectedWorkers.add(workerIndex);
}
renderBulkWorkerSelection();
}
window.toggleBulkWorkerSelection = toggleBulkWorkerSelection;
// 일괄 설정용 전체 선택
function selectAllForBulk() {
workerTaskList.forEach((_, index) => {
bulkSelectedWorkers.add(index);
});
renderBulkWorkerSelection();
}
window.selectAllForBulk = selectAllForBulk;
// 일괄 설정용 전체 해제
function deselectAllForBulk() {
bulkSelectedWorkers.clear();
renderBulkWorkerSelection();
}
window.deselectAllForBulk = deselectAllForBulk;
// 일괄 설정 모달 닫기
function closeBulkSettingModal() {
document.getElementById('bulkSettingModal').style.display = 'none';
unlockBodyScroll();
isBulkMode = false;
}
window.closeBulkSettingModal = closeBulkSettingModal;
// 일괄 설정용 항목 선택
function openBulkItemSelect(type) {
isBulkMode = true;
const modal = document.getElementById('itemSelectModal');
const titleEl = document.getElementById('itemSelectModalTitle');
const listEl = document.getElementById('itemSelectList');
let title = '';
let items = [];
if (type === 'project') {
title = '프로젝트 선택';
// 활성 프로젝트만 표시
const activeProjects = allProjects.filter(p =>
p.is_active === 1 || p.is_active === true || p.is_active === '1'
);
items = activeProjects.map(p => ({
id: p.project_id,
name: p.project_name,
icon: '📁'
}));
} else if (type === 'workType') {
title = '공정 선택';
items = allWorkTypes.map(wt => ({
id: wt.id,
name: wt.name,
icon: '⚙️'
}));
} else if (type === 'task') {
title = '작업 선택';
const currentWorkTypeId = parseInt(document.getElementById('bulkWorkTypeId').value);
if (!currentWorkTypeId) {
showToast('공정을 먼저 선택해주세요.', 'error');
return;
}
const filteredTasks = allTasks.filter(t => t.work_type_id === currentWorkTypeId);
items = filteredTasks.map(t => ({
id: t.task_id,
name: t.task_name,
icon: '🔧'
}));
}
titleEl.textContent = title;
listEl.innerHTML = items.length > 0 ? items.map(item => `
${item.icon} ${item.name}
`).join('') : '선택 가능한 항목이 없습니다
';
modal.style.display = 'flex';
lockBodyScroll();
}
window.openBulkItemSelect = openBulkItemSelect;
// 일괄 설정용 항목 선택 처리
function selectBulkItem(type, itemId, itemName) {
if (type === 'project') {
document.getElementById('bulkProjectId').value = itemId;
document.getElementById('bulkProjectBtn').textContent = `📁 ${itemName}`;
document.getElementById('bulkProjectBtn').classList.remove('btn-secondary');
document.getElementById('bulkProjectBtn').classList.add('btn-primary');
} else if (type === 'workType') {
document.getElementById('bulkWorkTypeId').value = itemId;
document.getElementById('bulkWorkTypeBtn').textContent = `⚙️ ${itemName}`;
document.getElementById('bulkWorkTypeBtn').classList.remove('btn-secondary');
document.getElementById('bulkWorkTypeBtn').classList.add('btn-primary');
// 공정 변경 시 작업 초기화
document.getElementById('bulkTaskId').value = '';
document.getElementById('bulkTaskBtn').textContent = '🔧 작업 선택';
document.getElementById('bulkTaskBtn').classList.remove('btn-primary');
document.getElementById('bulkTaskBtn').classList.add('btn-secondary');
document.getElementById('bulkTaskBtn').disabled = false;
} else if (type === 'task') {
document.getElementById('bulkTaskId').value = itemId;
document.getElementById('bulkTaskBtn').textContent = `🔧 ${itemName}`;
document.getElementById('bulkTaskBtn').classList.remove('btn-secondary');
document.getElementById('bulkTaskBtn').classList.add('btn-primary');
}
closeItemSelectModal();
isBulkMode = false;
}
window.selectBulkItem = selectBulkItem;
// 일괄 설정용 작업장 선택
function openBulkWorkplaceSelect() {
isBulkMode = true;
loadWorkplaceCategories();
document.getElementById('workplaceSelectModal').style.display = 'flex';
lockBodyScroll();
}
window.openBulkWorkplaceSelect = openBulkWorkplaceSelect;
// 일괄 설정 적용
function applyBulkSettings() {
if (bulkSelectedWorkers.size === 0) {
showToast('작업자를 선택해주세요.', 'error');
return;
}
const projectId = document.getElementById('bulkProjectId').value;
const workTypeId = document.getElementById('bulkWorkTypeId').value;
const taskId = document.getElementById('bulkTaskId').value;
const workplaceCategoryId = document.getElementById('bulkWorkplaceCategoryId').value;
const workplaceId = document.getElementById('bulkWorkplaceId').value;
if (!workTypeId || !taskId || !workplaceId) {
showToast('공정, 작업, 작업장은 필수 항목입니다.', 'error');
return;
}
// 선택된 작업자들의 첫 번째 작업 라인에 적용
let appliedCount = 0;
bulkSelectedWorkers.forEach(workerIndex => {
const workerData = workerTaskList[workerIndex];
if (workerData && workerData.tasks.length > 0) {
workerData.tasks[0].project_id = projectId ? parseInt(projectId) : null;
workerData.tasks[0].work_type_id = parseInt(workTypeId);
workerData.tasks[0].task_id = parseInt(taskId);
workerData.tasks[0].workplace_category_id = workplaceCategoryId ? parseInt(workplaceCategoryId) : null;
workerData.tasks[0].workplace_id = parseInt(workplaceId);
workerData.tasks[0].workplace_category_name = selectedCategoryName;
workerData.tasks[0].workplace_name = selectedWorkplaceName;
appliedCount++;
}
});
renderWorkerTaskList();
closeBulkSettingModal();
showToast(`${appliedCount}명의 작업자에게 일괄 설정이 적용되었습니다.`, 'success');
}
window.applyBulkSettings = applyBulkSettings;
// ==================== 항목 선택 (프로젝트/공정/작업) ====================
// 항목 선택 모달 열기
function openItemSelect(type, workerIndex, taskIndex) {
currentEditingTaskLine = { workerIndex, taskIndex };
const modal = document.getElementById('itemSelectModal');
const titleEl = document.getElementById('itemSelectModalTitle');
const listEl = document.getElementById('itemSelectList');
let title = '';
let items = [];
if (type === 'project') {
title = '프로젝트 선택';
// 활성 프로젝트만 표시
const activeProjects = allProjects.filter(p =>
p.is_active === 1 || p.is_active === true || p.is_active === '1'
);
items = activeProjects.map(p => ({
id: p.project_id,
name: p.project_name,
icon: '📁'
}));
} else if (type === 'workType') {
title = '공정 선택';
items = allWorkTypes.map(wt => ({
id: wt.id,
name: wt.name,
icon: '⚙️'
}));
} else if (type === 'task') {
title = '작업 선택';
const currentWorkTypeId = workerTaskList[workerIndex].tasks[taskIndex].work_type_id;
if (!currentWorkTypeId) {
showToast('공정을 먼저 선택해주세요.', 'error');
return;
}
const filteredTasks = allTasks.filter(t => t.work_type_id === currentWorkTypeId);
items = filteredTasks.map(t => ({
id: t.task_id,
name: t.task_name,
icon: '🔧'
}));
}
titleEl.textContent = title;
listEl.innerHTML = items.length > 0 ? items.map(item => `
${item.icon} ${item.name}
`).join('') : '선택 가능한 항목이 없습니다
';
modal.style.display = 'flex';
lockBodyScroll();
}
window.openItemSelect = openItemSelect;
// 항목 선택
function selectItem(type, itemId) {
// 일괄 모드면 여기서 처리하지 않음
if (isBulkMode) return;
if (!currentEditingTaskLine) return;
const { workerIndex, taskIndex } = currentEditingTaskLine;
const taskLine = workerTaskList[workerIndex].tasks[taskIndex];
if (type === 'project') {
taskLine.project_id = itemId;
} else if (type === 'workType') {
taskLine.work_type_id = itemId;
// 공정 변경 시 작업 초기화
taskLine.task_id = null;
} else if (type === 'task') {
taskLine.task_id = itemId;
}
renderWorkerTaskList();
closeItemSelectModal();
}
window.selectItem = selectItem;
// 항목 선택 모달 닫기
function closeItemSelectModal() {
document.getElementById('itemSelectModal').style.display = 'none';
unlockBodyScroll();
currentEditingTaskLine = null;
}
window.closeItemSelectModal = closeItemSelectModal;
// ==================== 작업장 2단계 선택 ====================
// 작업장 선택 모달 열기 (작업 라인용)
async function openWorkplaceSelect(workerIndex, taskIndex) {
currentEditingTaskLine = { workerIndex, taskIndex };
await loadWorkplaceCategories();
document.getElementById('workplaceSelectModal').style.display = 'flex';
lockBodyScroll();
}
window.openWorkplaceSelect = openWorkplaceSelect;
// 작업장 선택 모달 닫기
function closeWorkplaceSelectModal() {
// 가로모드 오버레이도 닫기
const landscapeOverlay = document.getElementById('landscapeOverlay');
if (landscapeOverlay && landscapeOverlay.style.display !== 'none') {
closeLandscapeMap();
}
document.getElementById('workplaceSelectModal').style.display = 'none';
unlockBodyScroll();
document.getElementById('workplaceSelectionArea').style.display = 'none';
document.getElementById('layoutMapArea').style.display = 'none';
document.getElementById('workplaceList').style.display = 'none';
currentEditingTaskLine = null;
selectedCategory = null;
selectedWorkplace = null;
selectedCategoryName = '';
selectedWorkplaceName = '';
mapCanvas = null;
mapCtx = null;
mapImage = null;
mapRegions = [];
}
window.closeWorkplaceSelectModal = closeWorkplaceSelectModal;
// 공장 카테고리 로드
async function loadWorkplaceCategories() {
const categoryList = document.getElementById('categoryList');
if (!categoryList) return;
if (allWorkplaceCategories.length === 0) {
categoryList.innerHTML = '등록된 공장이 없습니다
';
return;
}
categoryList.innerHTML = allWorkplaceCategories.map(category => `
🏭 ${category.category_name}
`).join('');
}
window.loadWorkplaceCategories = loadWorkplaceCategories;
// 공장 카테고리 선택
async function selectCategory(categoryId, categoryName) {
selectedCategory = categoryId;
selectedCategoryName = categoryName;
selectedWorkplace = null;
selectedWorkplaceName = '';
// 카테고리 버튼 스타일 업데이트
document.querySelectorAll('[id^="category-"]').forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary');
});
const selectedBtn = document.getElementById(`category-${categoryId}`);
if (selectedBtn) {
selectedBtn.classList.remove('btn-secondary');
selectedBtn.classList.add('btn-primary');
}
// 작업장 선택 영역 표시
document.getElementById('workplaceSelectionArea').style.display = 'block';
// 해당 카테고리 정보 가져오기
const category = allWorkplaceCategories.find(c => c.category_id === categoryId);
const isMobile = window.innerWidth <= 768;
// 지도 또는 리스트 로드
if (category && category.layout_image) {
// 지도가 있는 경우 - 지도를 기본 표시
await loadWorkplaceMap(categoryId, category.layout_image);
document.getElementById('layoutMapArea').style.display = 'block';
if (isMobile) {
// 모바일: 리스트 숨기고 "리스트로 선택" 토글 표시
document.getElementById('workplaceListSection').style.display = 'none';
document.getElementById('toggleListBtn').style.display = 'inline-flex';
document.getElementById('toggleListBtn').textContent = '리스트로 선택';
// 전체화면 지도 버튼 표시
const triggerBtn = document.getElementById('landscapeTriggerBtn');
if (triggerBtn) triggerBtn.style.display = 'inline-flex';
} else {
// 데스크톱: 리스트도 함께 표시
document.getElementById('workplaceList').style.display = 'flex';
document.getElementById('workplaceListSection').style.display = 'block';
document.getElementById('toggleListBtn').style.display = 'none';
}
} else {
// 지도가 없는 경우 - 리스트만 표시
document.getElementById('layoutMapArea').style.display = 'none';
document.getElementById('toggleListBtn').style.display = 'none';
document.getElementById('workplaceList').style.display = 'flex';
document.getElementById('workplaceListSection').style.display = 'block';
}
// 해당 카테고리의 작업장 리스트 로드
await loadWorkplacesByCategory(categoryId);
// 선택 완료 버튼 비활성화 (작업장 선택 필요)
document.getElementById('confirmWorkplaceBtn').disabled = true;
}
window.selectCategory = selectCategory;
// 카테고리별 작업장 로드
async function loadWorkplacesByCategory(categoryId) {
const workplaceList = document.getElementById('workplaceList');
if (!workplaceList) return;
try {
const workplaces = await window.TbmAPI.loadWorkplacesByCategory(categoryId);
if (!workplaces || workplaces.length === 0) {
workplaceList.innerHTML = '등록된 작업장이 없습니다
';
return;
}
workplaceList.innerHTML = workplaces.map(workplace => `
📍 ${workplace.workplace_name}
`).join('');
} catch (error) {
console.error('❌ 작업장 로드 오류:', error);
workplaceList.innerHTML = '작업장을 불러오는 중 오류가 발생했습니다
';
}
}
window.loadWorkplacesByCategory = loadWorkplacesByCategory;
// 작업장 선택
function selectWorkplace(workplaceId, workplaceName) {
selectedWorkplace = workplaceId;
selectedWorkplaceName = workplaceName;
// 작업장 버튼 스타일 업데이트 (리스트)
document.querySelectorAll('[id^="workplace-"]').forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary');
});
const selectedBtn = document.getElementById(`workplace-${workplaceId}`);
if (selectedBtn) {
selectedBtn.classList.remove('btn-secondary');
selectedBtn.classList.add('btn-primary');
}
// 지도 업데이트 (지도가 로드되어 있는 경우)
if (mapCanvas && mapCtx && mapImage) {
drawWorkplaceMap();
}
// 선택 완료 버튼 활성화
document.getElementById('confirmWorkplaceBtn').disabled = false;
}
window.selectWorkplace = selectWorkplace;
// 작업장 선택 확정
function confirmWorkplaceSelection() {
if (!selectedCategory || !selectedWorkplace) {
showToast('공장과 작업장을 모두 선택해주세요.', 'error');
return;
}
// 일괄 모드인 경우
if (isBulkMode) {
document.getElementById('bulkWorkplaceCategoryId').value = selectedCategory;
document.getElementById('bulkWorkplaceId').value = selectedWorkplace;
document.getElementById('bulkWorkplaceBtn').textContent = `📍 ${selectedCategoryName} → ${selectedWorkplaceName}`;
document.getElementById('bulkWorkplaceBtn').classList.remove('btn-secondary');
document.getElementById('bulkWorkplaceBtn').classList.add('btn-primary');
closeWorkplaceSelectModal();
isBulkMode = false;
showToast('작업장이 선택되었습니다.', 'success');
return;
}
// 현재 편집 중인 작업 라인에 저장
if (currentEditingTaskLine) {
const { workerIndex, taskIndex } = currentEditingTaskLine;
const taskLine = workerTaskList[workerIndex].tasks[taskIndex];
taskLine.workplace_category_id = selectedCategory;
taskLine.workplace_id = selectedWorkplace;
taskLine.workplace_category_name = selectedCategoryName;
taskLine.workplace_name = selectedWorkplaceName;
renderWorkerTaskList();
}
closeWorkplaceSelectModal();
showToast('작업장이 선택되었습니다.', 'success');
}
window.confirmWorkplaceSelection = confirmWorkplaceSelection;
// 리스트 토글 함수 (레거시 호환)
// 리스트 토글 함수
function toggleWorkplaceList() {
const listSection = document.getElementById('workplaceListSection');
const btn = document.getElementById('toggleListBtn');
if (listSection.style.display === 'none') {
listSection.style.display = 'block';
document.getElementById('workplaceList').style.display = 'flex';
btn.textContent = '리스트 숨기기';
} else {
listSection.style.display = 'none';
btn.textContent = '리스트로 선택';
}
}
window.toggleWorkplaceList = toggleWorkplaceList;
// 작업장 지도 로드 및 렌더링
// mapRegions, mapCanvas, mapCtx, mapImage → TbmState 프록시 사용 (state.js)
async function loadWorkplaceMap(categoryId, layoutImagePath) {
try {
mapCanvas = document.getElementById('workplaceMapCanvas');
if (!mapCanvas) return;
mapCtx = mapCanvas.getContext('2d');
// 이미지 URL 생성
const baseUrl = window.API_BASE_URL || 'http://localhost:30005';
const apiBaseUrl = baseUrl.replace('/api', ''); // /api 제거
const fullImageUrl = layoutImagePath.startsWith('http')
? layoutImagePath
: `${apiBaseUrl}${layoutImagePath}`;
console.log('🖼️ 이미지 로드 시도:', fullImageUrl);
// 지도 영역 데이터 로드
mapRegions = await window.TbmAPI.loadMapRegions(categoryId);
// 이미지 로드
mapImage = new Image();
mapImage.crossOrigin = 'anonymous';
mapImage.onload = function() {
// 캔버스 크기 설정 (모바일 대응)
const maxWidth = window.innerWidth <= 768
? Math.min(window.innerWidth - 32, 600)
: 800;
const scale = mapImage.width > maxWidth ? maxWidth / mapImage.width : 1;
mapCanvas.width = mapImage.width * scale;
mapCanvas.height = mapImage.height * scale;
// 이미지와 영역 그리기
drawWorkplaceMap();
// 클릭 이벤트 리스너 추가
mapCanvas.onclick = handleMapClick;
console.log(`✅ 작업장 지도 로드 완료: ${mapRegions.length}개 영역`);
};
mapImage.onerror = function() {
console.error('❌ 지도 이미지 로드 실패');
document.getElementById('layoutMapArea').style.display = 'none';
document.getElementById('workplaceListSection').style.display = 'block';
document.getElementById('workplaceList').style.display = 'flex';
document.getElementById('toggleListBtn').style.display = 'none';
showToast('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning');
};
mapImage.src = fullImageUrl;
} catch (error) {
console.error('❌ 작업장 지도 로드 오류:', error);
document.getElementById('layoutMapArea').style.display = 'none';
document.getElementById('workplaceList').style.display = 'flex';
}
}
window.loadWorkplaceMap = loadWorkplaceMap;
// 지도 그리기 (이미지 + 영역 + 라벨)
function drawWorkplaceMap() {
if (!mapCanvas || !mapCtx || !mapImage) return;
// 이미지 그리기
mapCtx.drawImage(mapImage, 0, 0, mapCanvas.width, mapCanvas.height);
// 각 영역 그리기
mapRegions.forEach((region, index) => {
// 퍼센트를 픽셀로 변환
const x1 = (region.x_start / 100) * mapCanvas.width;
const y1 = (region.y_start / 100) * mapCanvas.height;
const x2 = (region.x_end / 100) * mapCanvas.width;
const y2 = (region.y_end / 100) * mapCanvas.height;
const width = x2 - x1;
const height = y2 - y1;
// 선택된 영역인지 확인
const isSelected = region.workplace_id === selectedWorkplace;
// 영역 테두리
mapCtx.strokeStyle = isSelected ? '#3b82f6' : '#10b981';
mapCtx.lineWidth = isSelected ? 4 : 2;
mapCtx.strokeRect(x1, y1, width, height);
// 영역 배경 (반투명)
mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.25)' : 'rgba(16, 185, 129, 0.15)';
mapCtx.fillRect(x1, y1, width, height);
// 작업장 이름 표시
if (region.workplace_name) {
mapCtx.font = 'bold 14px sans-serif';
// 텍스트 배경
const textMetrics = mapCtx.measureText(region.workplace_name);
const textPadding = 6;
mapCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.95)' : 'rgba(16, 185, 129, 0.95)';
mapCtx.fillRect(x1 + 5, y1 + 5, textMetrics.width + textPadding * 2, 24);
// 텍스트
mapCtx.fillStyle = '#ffffff';
mapCtx.fillText(region.workplace_name, x1 + 5 + textPadding, y1 + 22);
}
});
}
// 지도 클릭 이벤트 처리
function handleMapClick(event) {
if (!mapCanvas || mapRegions.length === 0) return;
const rect = mapCanvas.getBoundingClientRect();
// CSS 스케일 보정: 캔버스의 논리적 크기와 화면 표시 크기가 다를 수 있음
const scaleX = mapCanvas.width / rect.width;
const scaleY = mapCanvas.height / rect.height;
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
// 클릭한 위치에 있는 영역 찾기
for (let i = mapRegions.length - 1; i >= 0; i--) {
const region = mapRegions[i];
const x1 = (region.x_start / 100) * mapCanvas.width;
const y1 = (region.y_start / 100) * mapCanvas.height;
const x2 = (region.x_end / 100) * mapCanvas.width;
const y2 = (region.y_end / 100) * mapCanvas.height;
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 영역 클릭됨
selectWorkplace(region.workplace_id, region.workplace_name);
// 지도 다시 그리기 (선택 효과 표시)
drawWorkplaceMap();
// 리스트에서도 동기화
syncWorkplaceListSelection(region.workplace_id);
return;
}
}
}
// 리스트 선택 동기화
function syncWorkplaceListSelection(workplaceId) {
// 리스트의 버튼들도 업데이트
document.querySelectorAll('[id^="workplace-"]').forEach(btn => {
if (btn.id === `workplace-${workplaceId}`) {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-primary');
} else {
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary');
}
});
}
window.syncWorkplaceListSelection = syncWorkplaceListSelection;
// ==================== 가로모드 전체화면 지도 ====================
function openLandscapeMap() {
if (!mapImage || !mapImage.complete || mapRegions.length === 0) return;
const overlay = document.getElementById('landscapeOverlay');
const inner = document.getElementById('landscapeInner');
const lCanvas = document.getElementById('landscapeCanvas');
if (!overlay || !lCanvas) return;
overlay.style.display = 'flex';
lockBodyScroll();
// 물리적 가로모드 여부 판단
const isPhysicalLandscape = window.innerWidth > window.innerHeight;
inner.className = 'landscape-inner ' + (isPhysicalLandscape ? 'no-rotate' : 'rotated');
// 가용 영역 계산 (헤더 52px, 패딩 여유)
const headerH = 52;
const pad = 16;
let availW, availH;
if (isPhysicalLandscape) {
availW = window.innerWidth - pad * 2;
availH = window.innerHeight - headerH - pad * 2;
} else {
// 회전: 가로↔세로 스왑
availW = window.innerHeight - pad * 2;
availH = window.innerWidth - headerH - pad * 2;
}
// 이미지 비율 유지 캔버스 크기
const imgRatio = mapImage.naturalWidth / mapImage.naturalHeight;
let cw, ch;
if (availW / availH > imgRatio) {
ch = availH;
cw = ch * imgRatio;
} else {
cw = availW;
ch = cw / imgRatio;
}
lCanvas.width = Math.round(cw);
lCanvas.height = Math.round(ch);
drawLandscapeMap();
// 이벤트 리스너
lCanvas.ontouchstart = handleLandscapeTouchStart;
lCanvas.onclick = handleLandscapeClick;
}
window.openLandscapeMap = openLandscapeMap;
function drawLandscapeMap() {
const lCanvas = document.getElementById('landscapeCanvas');
if (!lCanvas || !mapImage) return;
const lCtx = lCanvas.getContext('2d');
lCtx.drawImage(mapImage, 0, 0, lCanvas.width, lCanvas.height);
mapRegions.forEach(region => {
const x1 = (region.x_start / 100) * lCanvas.width;
const y1 = (region.y_start / 100) * lCanvas.height;
const x2 = (region.x_end / 100) * lCanvas.width;
const y2 = (region.y_end / 100) * lCanvas.height;
const w = x2 - x1;
const h = y2 - y1;
const isSelected = region.workplace_id === selectedWorkplace;
lCtx.strokeStyle = isSelected ? '#3b82f6' : '#10b981';
lCtx.lineWidth = isSelected ? 4 : 2;
lCtx.strokeRect(x1, y1, w, h);
lCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.25)' : 'rgba(16, 185, 129, 0.15)';
lCtx.fillRect(x1, y1, w, h);
if (region.workplace_name) {
lCtx.font = 'bold 14px sans-serif';
const tm = lCtx.measureText(region.workplace_name);
const tp = 6;
lCtx.fillStyle = isSelected ? 'rgba(59, 130, 246, 0.95)' : 'rgba(16, 185, 129, 0.95)';
lCtx.fillRect(x1 + 5, y1 + 5, tm.width + tp * 2, 24);
lCtx.fillStyle = '#ffffff';
lCtx.textAlign = 'left';
lCtx.textBaseline = 'alphabetic';
lCtx.fillText(region.workplace_name, x1 + 5 + tp, y1 + 22);
}
});
}
function getLandscapeCoords(clientX, clientY) {
const lCanvas = document.getElementById('landscapeCanvas');
if (!lCanvas) return null;
const rect = lCanvas.getBoundingClientRect();
const inner = document.getElementById('landscapeInner');
const isRotated = inner.classList.contains('rotated');
if (!isRotated) {
// 회전 없음 - 일반 좌표
const scaleX = lCanvas.width / rect.width;
const scaleY = lCanvas.height / rect.height;
return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
};
}
// 90° 시계방향 회전의 역변환
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = clientX - centerX;
const dy = clientY - centerY;
// 역회전 (반시계 90°)
const inverseDx = dy;
const inverseDy = -dx;
// 회전 전 실제 크기: rect가 회전된 후이므로 width↔height 스왑
const unrotatedW = rect.height;
const unrotatedH = rect.width;
const canvasX = (inverseDx + unrotatedW / 2) / unrotatedW * lCanvas.width;
const canvasY = (inverseDy + unrotatedH / 2) / unrotatedH * lCanvas.height;
return { x: canvasX, y: canvasY };
}
function handleLandscapeTouchStart(e) {
e.preventDefault(); // 고스트 클릭 방지
const touch = e.touches[0];
const coords = getLandscapeCoords(touch.clientX, touch.clientY);
if (coords) doLandscapeHitTest(coords.x, coords.y);
}
function handleLandscapeClick(e) {
const coords = getLandscapeCoords(e.clientX, e.clientY);
if (coords) doLandscapeHitTest(coords.x, coords.y);
}
function doLandscapeHitTest(cx, cy) {
const lCanvas = document.getElementById('landscapeCanvas');
if (!lCanvas) return;
for (let i = mapRegions.length - 1; i >= 0; i--) {
const region = mapRegions[i];
const x1 = (region.x_start / 100) * lCanvas.width;
const y1 = (region.y_start / 100) * lCanvas.height;
const x2 = (region.x_end / 100) * lCanvas.width;
const y2 = (region.y_end / 100) * lCanvas.height;
if (cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2) {
selectWorkplace(region.workplace_id, region.workplace_name);
drawWorkplaceMap();
syncWorkplaceListSelection(region.workplace_id);
// 하이라이트 후 자동 닫기
drawLandscapeMap();
setTimeout(() => closeLandscapeMap(), 300);
return;
}
}
}
function closeLandscapeMap() {
const overlay = document.getElementById('landscapeOverlay');
if (overlay) overlay.style.display = 'none';
const lCanvas = document.getElementById('landscapeCanvas');
if (lCanvas) {
lCanvas.ontouchstart = null;
lCanvas.onclick = null;
}
unlockBodyScroll();
}
window.closeLandscapeMap = closeLandscapeMap;
// ==================== 기존 팀 구성 모달 (백업) ====================
// 팀 구성 모달 열기
// 팀 구성 수정 (TBM 수정 모달 열기)
async function openTeamCompositionModal(sessionId) {
currentSessionId = sessionId;
try {
// 세션 정보 로드
const session = await window.TbmAPI.getSession(sessionId);
if (!session) {
showToast('TBM 정보를 불러올 수 없습니다.', 'error');
return;
}
// 팀원 정보 로드
const teamMembers = await window.TbmAPI.getTeamMembers(sessionId);
if (!teamMembers) {
showToast('팀원 정보를 불러올 수 없습니다.', 'error');
return;
}
// workerTaskList 구성
workerTaskList = [];
const workerMap = new Map();
// 팀원별로 작업 그룹화
teamMembers.forEach(member => {
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.user_id).tasks.push({
task_line_id: generateUUID(),
project_id: member.project_id,
work_type_id: member.work_type_id,
task_id: member.task_id,
workplace_category_id: member.workplace_category_id,
workplace_id: member.workplace_id,
workplace_category_name: member.workplace_category_name,
workplace_name: member.workplace_name,
work_detail: member.work_detail,
is_present: member.is_present !== undefined ? member.is_present : true
});
});
workerTaskList = Array.from(workerMap.values());
// 모달 열기
document.getElementById('modalTitle').textContent = '팀 구성 수정';
document.getElementById('sessionId').value = sessionId;
document.getElementById('sessionDate').value = session.session_date;
// 입력자 표시
if (session.leader_name) {
document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`;
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 = '';
}
renderWorkerTaskList();
document.getElementById('tbmModal').style.display = 'flex';
lockBodyScroll();
} catch (error) {
console.error('❌ 팀 구성 로드 오류:', error);
showToast('팀 구성을 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.openTeamCompositionModal = openTeamCompositionModal;
// 선택된 작업자 업데이트
function updateSelectedWorkers() {
selectedWorkers.clear();
document.querySelectorAll('.worker-checkbox:checked').forEach(cb => {
selectedWorkers.add(parseInt(cb.dataset.userId));
});
const selectedCount = document.getElementById('selectedCount');
const selectedList = document.getElementById('selectedWorkersList');
selectedCount.textContent = selectedWorkers.size;
if (selectedWorkers.size === 0) {
selectedList.innerHTML = '작업자를 선택해주세요
';
} else {
const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
const worker = allWorkers.find(w => w.user_id === id);
return worker ? `
${worker.worker_name}
×
` : '';
});
selectedList.innerHTML = selectedWorkersArray.join('');
}
}
window.updateSelectedWorkers = updateSelectedWorkers;
// 작업자 제거
function removeWorker(workerId) {
const checkbox = document.querySelector(`.worker-checkbox[data-user-id="${workerId}"]`);
if (checkbox) {
checkbox.checked = false;
updateSelectedWorkers();
}
}
window.removeWorker = removeWorker;
// 전체 선택
function selectAllWorkers() {
document.querySelectorAll('.worker-checkbox').forEach(cb => {
cb.checked = true;
});
updateSelectedWorkers();
}
window.selectAllWorkers = selectAllWorkers;
// 전체 해제
function deselectAllWorkers() {
document.querySelectorAll('.worker-checkbox').forEach(cb => {
cb.checked = false;
});
updateSelectedWorkers();
}
window.deselectAllWorkers = deselectAllWorkers;
// 팀 구성 모달 닫기
function closeTeamModal() {
document.getElementById('teamModal').style.display = 'none';
unlockBodyScroll();
}
window.closeTeamModal = closeTeamModal;
// 팀 구성 저장
async function saveTeamComposition() {
if (selectedWorkers.size === 0) {
showToast('최소 1명 이상의 작업자를 선택해주세요.', 'error');
return;
}
const members = Array.from(selectedWorkers).map(workerId => ({
user_id: workerId
}));
try {
const response = await window.TbmAPI.addTeamMembers(currentSessionId, members);
if (response && response.success) {
showToast(`${selectedWorkers.size}명의 팀원이 추가되었습니다.`, 'success');
closeTeamModal();
// 목록 새로고침
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
const date = document.getElementById('tbmDate').value;
await loadTbmSessionsByDate(date);
}
} else {
throw new Error(response.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('❌ 팀 구성 저장 오류:', error);
showToast('팀 구성 저장 중 오류가 발생했습니다.', 'error');
}
}
window.saveTeamComposition = saveTeamComposition;
// 안전 체크 모달 열기 (기본 + 날씨별 + 작업별)
async function openSafetyCheckModal(sessionId) {
currentSessionId = sessionId;
try {
// 필터링된 체크리스트 조회 (기본 + 날씨 + 작업별)
const filteredData = await window.TbmAPI.getFilteredSafetyChecks(sessionId);
const { basic, weather, task, weatherInfo } = filteredData;
const categoryNames = {
'PPE': '개인 보호 장비',
'EQUIPMENT': '장비 점검',
'ENVIRONMENT': '작업 환경',
'EMERGENCY': '비상 대응',
'WEATHER': '날씨',
'TASK': '작업'
};
const weatherIcons = {
clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥',
cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷'
};
const container = document.getElementById('safetyChecklistContainer');
let html = '';
// 1. 기본 사항 섹션
if (basic && basic.length > 0) {
const basicGrouped = groupChecksByCategory(basic);
html += `
📋 기본 안전 사항 (${basic.length}개)
${renderCategoryGroups(basicGrouped, categoryNames)}
`;
}
// 2. 날씨별 섹션
if (weather && weather.length > 0) {
const weatherConditions = weatherInfo?.weather_conditions || [];
const conditionNames = weatherConditions.map(c => {
const icon = weatherIcons[c] || '🌤️';
return `${icon} ${getWeatherConditionName(c)}`;
}).join(', ') || '맑음';
html += `
🌤️ 오늘 날씨 관련 (${conditionNames}) - ${weather.length}개
${renderCheckItems(weather)}
`;
}
// 3. 작업별 섹션
if (task && task.length > 0) {
const taskGrouped = groupChecksByTask(task);
html += `
🔧 작업별 안전 사항 - ${task.length}개
${renderTaskGroups(taskGrouped)}
`;
}
// 체크리스트가 없는 경우
if ((!basic || basic.length === 0) && (!weather || weather.length === 0) && (!task || task.length === 0)) {
html = `
`;
}
container.innerHTML = html;
document.getElementById('safetyModal').style.display = 'flex';
lockBodyScroll();
} catch (error) {
console.error('❌ 안전 체크 조회 오류:', error);
showToast('안전 체크 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.openSafetyCheckModal = openSafetyCheckModal;
// 카테고리별 그룹화
function groupChecksByCategory(checks) {
return checks.reduce((acc, check) => {
const category = check.check_category || 'OTHER';
if (!acc[category]) acc[category] = [];
acc[category].push(check);
return acc;
}, {});
}
// 작업별 그룹화
function groupChecksByTask(checks) {
return checks.reduce((acc, check) => {
const taskId = check.task_id || 0;
const taskName = check.task_name || '기타 작업';
if (!acc[taskId]) acc[taskId] = { name: taskName, items: [] };
acc[taskId].items.push(check);
return acc;
}, {});
}
// 날씨 조건명 반환
function getWeatherConditionName(code) {
const names = {
clear: '맑음', rain: '비', snow: '눈', heat: '폭염',
cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지'
};
return names[code] || code;
}
// 카테고리 그룹 렌더링
function renderCategoryGroups(grouped, categoryNames) {
return Object.keys(grouped).map(category => `
${categoryNames[category] || category}
${renderCheckItems(grouped[category])}
`).join('');
}
// 작업 그룹 렌더링
function renderTaskGroups(grouped) {
return Object.values(grouped).map(group => `
📋 ${group.name}
${renderCheckItems(group.items)}
`).join('');
}
// 체크 항목 렌더링
function renderCheckItems(items) {
return items.map(check => `
${check.check_item}
${check.is_required ? '* ' : ''}
${check.description ? `
${check.description}
` : ''}
`).join('');
}
// 안전 체크 모달 닫기
function closeSafetyModal() {
document.getElementById('safetyModal').style.display = 'none';
unlockBodyScroll();
}
window.closeSafetyModal = closeSafetyModal;
// 안전 체크리스트 저장
async function saveSafetyChecklist() {
const records = [];
document.querySelectorAll('.safety-check').forEach(cb => {
records.push({
check_id: parseInt(cb.dataset.checkId),
is_checked: cb.checked
});
});
try {
await window.TbmAPI.saveSafetyChecks(currentSessionId, records);
showToast('안전 체크가 완료되었습니다.', 'success');
closeSafetyModal();
} catch (error) {
console.error('❌ 안전 체크 저장 오류:', error);
showToast('안전 체크 저장 중 오류가 발생했습니다.', 'error');
}
}
window.saveSafetyChecklist = saveSafetyChecklist;
// TBM 완료 모달용 팀원 데이터
let completeModalTeam = [];
// TBM 완료 모달 열기
async function openCompleteTbmModal(sessionId) {
currentSessionId = sessionId;
const now = new Date();
const timeString = now.toTimeString().slice(0, 5);
document.getElementById('endTime').value = timeString;
document.getElementById('completeModal').style.display = 'flex';
lockBodyScroll();
// 팀원 조회 → 근태 선택 렌더링
try {
completeModalTeam = await window.TbmAPI.getTeamMembers(sessionId);
renderCompleteAttendanceList();
} catch (e) {
console.error('팀원 조회 오류:', e);
document.getElementById('completeAttendanceList').innerHTML =
'팀원 목록을 불러올 수 없습니다.
';
}
}
window.openCompleteTbmModal = openCompleteTbmModal;
function renderCompleteAttendanceList() {
const container = document.getElementById('completeAttendanceList');
if (completeModalTeam.length === 0) {
container.innerHTML = '팀원이 없습니다.
';
return;
}
let html = '';
container.innerHTML = html;
}
window.onCompleteAttChange = function(idx) {
const sel = document.getElementById('catt_type_' + idx);
const inp = document.getElementById('catt_hours_' + idx);
const hint = document.getElementById('catt_hint_' + idx);
const val = sel.value;
if (val === 'overtime') {
inp.style.display = 'inline-block';
inp.placeholder = '+h';
inp.value = '';
hint.textContent = '';
} else if (val === 'early') {
inp.style.display = 'inline-block';
inp.placeholder = '시간';
inp.value = '';
hint.textContent = '';
} else {
inp.style.display = 'none';
inp.value = '';
const labels = { regular: '8h', annual: '자동처리', half: '4h', quarter: '6h' };
hint.textContent = labels[val] || '';
}
};
// 완료 모달 닫기
function closeCompleteModal() {
document.getElementById('completeModal').style.display = 'none';
unlockBodyScroll();
}
window.closeCompleteModal = closeCompleteModal;
// TBM 세션 완료
async function completeTbmSession() {
const endTime = document.getElementById('endTime').value;
// 근태 데이터 수집
const attendanceData = [];
for (let i = 0; i < completeModalTeam.length; i++) {
const type = document.getElementById('catt_type_' + i).value;
const hoursVal = document.getElementById('catt_hours_' + i).value;
const hours = hoursVal ? parseFloat(hoursVal) : null;
if (type === 'overtime' && (!hours || hours <= 0)) {
showToast(`${completeModalTeam[i].worker_name}의 추가 시간을 입력해주세요.`, 'error');
return;
}
if (type === 'early' && (!hours || hours <= 0)) {
showToast(`${completeModalTeam[i].worker_name}의 근무 시간을 입력해주세요.`, 'error');
return;
}
attendanceData.push({
user_id: completeModalTeam[i].user_id,
attendance_type: type,
attendance_hours: hours
});
}
const btn = document.getElementById('completeModalBtn');
if (btn) { btn.disabled = true; btn.textContent = '처리 중...'; }
try {
const response = await window.apiCall(
`/tbm/sessions/${currentSessionId}/complete`,
'POST',
{ end_time: endTime, attendance_data: attendanceData }
);
if (response && response.success) {
showToast('TBM이 완료되었습니다.', 'success');
closeCompleteModal();
// 목록 새로고침
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
const date = document.getElementById('tbmDate').value;
await loadTbmSessionsByDate(date);
}
} else {
throw new Error(response.message || '완료 처리에 실패했습니다.');
}
} catch (error) {
console.error('❌ TBM 완료 처리 오류:', error);
showToast('TBM 완료 처리 중 오류가 발생했습니다.', 'error');
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = '✓ 완료'; }
}
}
window.completeTbmSession = completeTbmSession;
// TBM 세션 상세 보기
async function viewTbmSession(sessionId) {
try {
// 세션 정보, 팀 구성, 안전 체크 동시 조회
const [session, team, safety] = await Promise.all([
window.TbmAPI.getSession(sessionId),
window.TbmAPI.getTeamMembers(sessionId),
window.TbmAPI.getSafetyChecks(sessionId)
]);
if (!session) {
showToast('세션 정보를 불러올 수 없습니다.', 'error');
return;
}
// 기본 정보 표시
const leaderDisplay = session.leader_name || session.created_by_name || '-';
const dateDisplay = formatDate(session.session_date) || '-';
const statusMap = { draft: '진행중', completed: '완료', cancelled: '취소' };
const statusText = statusMap[session.status] || session.status;
const basicInfo = document.getElementById('detailBasicInfo');
basicInfo.innerHTML = `
입력자
${escapeHtml(leaderDisplay)}
날짜
${escapeHtml(dateDisplay)}
상태
${escapeHtml(statusText)}
팀원 (${parseInt(session.team_member_count) || team.length}명)
${escapeHtml(session.team_member_names || team.map(t => t.worker_name).join(', ') || '없음')}
${session.project_name ? `
프로젝트
${escapeHtml(session.project_name)}
` : ''}
${session.work_location ? `
작업장
${escapeHtml(session.work_location)}
` : ''}
`;
// 팀 구성 표시 (작업자별 작업 정보 포함)
const teamContainer = document.getElementById('detailTeamMembers');
if (team.length === 0) {
teamContainer.innerHTML = '등록된 팀원이 없습니다.
';
} else {
// 작업자별로 그룹화
const workerMap = new Map();
team.forEach(member => {
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.user_id).tasks.push(member);
});
teamContainer.style.display = 'flex';
teamContainer.style.flexDirection = 'column';
teamContainer.style.gap = '0.75rem';
teamContainer.style.gridTemplateColumns = '';
teamContainer.innerHTML = Array.from(workerMap.values()).map(worker => `
${escapeHtml(worker.worker_name)}
${escapeHtml(worker.job_type || '')}
${!worker.is_present ? '
결석 ' : ''}
${worker.tasks.map(t => `
${t.project_name ? `${escapeHtml(t.project_name)} ` : ''}
${t.work_type_name ? `${escapeHtml(t.work_type_name)} ` : ''}
${t.task_name ? `${escapeHtml(t.task_name)} ` : ''}
${t.workplace_name ? `${escapeHtml((t.workplace_category_name ? t.workplace_category_name + ' > ' : '') + t.workplace_name)} ` : ''}
`).join('')}
`).join('');
}
// 안전 체크 표시
const safetyChecks = document.getElementById('detailSafetyChecks');
if (safety.length === 0) {
safetyChecks.innerHTML = '안전 체크 기록이 없습니다.
';
} else {
// 카테고리별 그룹화
const grouped = {};
safety.forEach(check => {
if (!grouped[check.check_category]) {
grouped[check.check_category] = [];
}
grouped[check.check_category].push(check);
});
const categoryNames = {
'PPE': '개인 보호 장비',
'EQUIPMENT': '장비 점검',
'ENVIRONMENT': '작업 환경',
'EMERGENCY': '비상 대응'
};
safetyChecks.innerHTML = Object.keys(grouped).map(category => `
${categoryNames[category] || category}
${grouped[category].map(check => `
${check.is_checked ? '✅' : '❌'}
${check.check_item}
`).join('')}
`).join('');
}
// 푸터 버튼 동적 생성
const footer = document.getElementById('detailModalFooter');
const safeId = parseInt(session.session_id) || 0;
console.log('📋 TBM 상세 - session_id:', safeId, 'status:', session.status);
if (session.status === 'draft') {
footer.innerHTML = `
삭제
수정
분할
빼오기
닫기
`;
} else {
footer.innerHTML = `
닫기
`;
}
document.getElementById('detailModal').style.display = 'flex';
lockBodyScroll();
} catch (error) {
console.error('❌ TBM 상세 조회 오류:', error);
showToast('상세 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.viewTbmSession = viewTbmSession;
// TBM 삭제 확인
function confirmDeleteTbm(sessionId) {
if (!confirm('이 TBM을 삭제하시겠습니까?\n삭제 후 복구할 수 없습니다.')) return;
deleteTbmSession(sessionId);
}
window.confirmDeleteTbm = confirmDeleteTbm;
// TBM 세션 삭제 → TbmAPI 위임
async function deleteTbmSession(sessionId) {
try {
await window.TbmAPI.deleteSession(sessionId);
showToast('TBM이 삭제되었습니다.', 'success');
closeDetailModal();
if (currentTab === 'tbm-input') {
await loadTodayOnlyTbm();
} else {
await loadRecentTbmGroupedByDate();
}
} catch (error) {
console.error('❌ TBM 삭제 오류:', error);
showToast(error?.message || 'TBM 삭제 중 오류가 발생했습니다.', 'error');
}
}
window.deleteTbmSession = deleteTbmSession;
// 상세보기 모달 닫기
function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none';
unlockBodyScroll();
}
window.closeDetailModal = closeDetailModal;
// 작업 인계 모달 열기
async function openHandoverModal(sessionId) {
currentSessionId = sessionId;
// 세션 정보와 팀 구성 조회
try {
const [session, team] = await Promise.all([
window.TbmAPI.getSession(sessionId),
window.TbmAPI.getTeamMembers(sessionId)
]);
if (!session) {
showToast('세션 정보를 불러올 수 없습니다.', 'error');
return;
}
// 현재 세션의 팀장을 제외한 리더 목록
const toLeaderSelect = document.getElementById('toLeaderId');
const otherLeaders = allWorkers.filter(w =>
(w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.user_id !== session.leader_user_id
);
toLeaderSelect.innerHTML = '인수자 선택... ' +
otherLeaders.map(w => `
${w.worker_name} (${w.job_type || ''})
`).join('');
// 인계할 팀원 목록
const handoverTeamList = document.getElementById('handoverTeamList');
if (team.length === 0) {
handoverTeamList.innerHTML = '팀 구성이 없습니다.
';
} else {
handoverTeamList.innerHTML = team.map(member => `
${member.worker_name}
${member.job_type || ''}
`).join('');
}
// 기본값 설정
document.getElementById('handoverSessionId').value = sessionId;
const today = getTodayKST();
const now = new Date().toTimeString().slice(0, 5);
document.getElementById('handoverDate').value = today;
document.getElementById('handoverTime').value = now;
document.getElementById('handoverReason').value = '';
document.getElementById('handoverNotes').value = '';
document.getElementById('handoverModal').style.display = 'flex';
lockBodyScroll();
} catch (error) {
console.error('❌ 인계 모달 열기 오류:', error);
showToast('인계 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
window.openHandoverModal = openHandoverModal;
// 인계 모달 닫기
function closeHandoverModal() {
document.getElementById('handoverModal').style.display = 'none';
unlockBodyScroll();
}
window.closeHandoverModal = closeHandoverModal;
// 작업 인계 저장
async function saveHandover() {
const sessionId = currentSessionId;
const toLeaderId = parseInt(document.getElementById('toLeaderId').value);
const reason = document.getElementById('handoverReason').value;
const handoverDate = document.getElementById('handoverDate').value;
const handoverTime = document.getElementById('handoverTime').value;
const handoverNotes = document.getElementById('handoverNotes').value;
if (!toLeaderId || !reason || !handoverDate) {
showToast('필수 항목을 입력해주세요.', 'error');
return;
}
// 인계할 작업자 목록
const workerIds = [];
document.querySelectorAll('.handover-worker-checkbox:checked').forEach(cb => {
workerIds.push(parseInt(cb.value));
});
if (workerIds.length === 0) {
showToast('인계할 팀원을 최소 1명 이상 선택해주세요.', 'error');
return;
}
try {
// 세션 정보 조회 (from_leader_user_id 가져오기)
const sessionData = await window.TbmAPI.getSession(sessionId);
const fromLeaderId = sessionData?.leader_user_id;
if (!fromLeaderId) {
showToast('세션 정보를 찾을 수 없습니다.', 'error');
return;
}
const handoverData = {
session_id: sessionId,
from_leader_user_id: fromLeaderId,
to_leader_user_id: toLeaderId,
handover_date: handoverDate,
handover_time: handoverTime,
reason: reason,
handover_notes: handoverNotes,
user_ids: workerIds
};
const response = await window.TbmAPI.saveHandover(handoverData);
if (response && response.success) {
showToast('작업 인계가 요청되었습니다.', 'success');
closeHandoverModal();
} else {
throw new Error(response.message || '인계 요청에 실패했습니다.');
}
} catch (error) {
console.error('❌ 작업 인계 저장 오류:', error);
showToast('작업 인계 중 오류가 발생했습니다.', 'error');
}
}
window.saveHandover = saveHandover;
// ==================== 데스크탑 분할 기능 ====================
let splitModalSessionId = null;
let splitModalTeam = [];
async function openDesktopSplitModal(sessionId) {
splitModalSessionId = sessionId;
try {
splitModalTeam = await window.TbmAPI.getTeamMembers(sessionId);
if (splitModalTeam.length === 0) {
showToast('팀원이 없습니다.', 'error'); return;
}
const modal = document.getElementById('splitModal');
if (!modal) { showToast('분할 모달을 찾을 수 없습니다.', 'error'); return; }
const list = document.getElementById('splitMemberList');
list.innerHTML = splitModalTeam.map((m, i) => {
const hours = m.work_hours != null ? parseFloat(m.work_hours) : 8;
return `
`;
}).join('');
modal.style.display = 'flex';
lockBodyScroll();
} catch(e) {
console.error('분할 모달 오류:', e);
showToast('팀원 조회 오류', 'error');
}
}
window.openDesktopSplitModal = openDesktopSplitModal;
async function executeSplit(memberIdx) {
const m = splitModalTeam[memberIdx];
const currentHours = m.work_hours != null ? parseFloat(m.work_hours) : 8;
const splitHours = parseFloat(document.getElementById(`split_hours_${memberIdx}`).value);
if (!splitHours || splitHours <= 0 || splitHours >= currentHours) {
showToast(`올바른 시간 입력 (0 < 시간 < ${currentHours})`, 'error'); return;
}
try {
await window.TbmAPI.updateTeamMember(splitModalSessionId, {
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, {
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');
closeSplitModal();
if (currentTab === 'tbm-input') await loadTodayOnlyTbm(); else await loadRecentTbmGroupedByDate();
} catch(e) {
console.error('분할 오류:', e);
showToast('분할 처리 중 오류', 'error');
}
}
window.executeSplit = executeSplit;
function closeSplitModal() {
const modal = document.getElementById('splitModal');
if (modal) modal.style.display = 'none';
unlockBodyScroll();
}
window.closeSplitModal = closeSplitModal;
// ==================== 데스크탑 빼오기 기능 ====================
let pullModalSessionId = null;
async function openDesktopPullModal(targetSessionId) {
pullModalSessionId = targetSessionId;
try {
const todayStr = getTodayKST();
const sessions = await window.TbmAPI.fetchSessionsByDate(todayStr);
const otherSessions = sessions.filter(s => s.session_id !== targetSessionId && s.status === 'draft');
const modal = document.getElementById('pullModal');
if (!modal) { showToast('빼오기 모달을 찾을 수 없습니다.', 'error'); return; }
const list = document.getElementById('pullSessionList');
if (otherSessions.length === 0) {
list.innerHTML = '빼올 수 있는 다른 TBM이 없습니다.
';
} else {
list.innerHTML = otherSessions.map(s => {
const leader = escapeHtml(s.leader_name || s.created_by_name || '미지정');
const count = parseInt(s.team_member_count) || 0;
return `
`;
}).join('');
}
modal.style.display = 'flex';
lockBodyScroll();
} catch(e) {
console.error('빼오기 모달 오류:', e);
showToast('빼오기 데이터 로드 오류', 'error');
}
}
window.openDesktopPullModal = openDesktopPullModal;
async function togglePullSessionMembers(sessionId, el) {
const container = document.getElementById(`pullMembers_${sessionId}`);
if (container.style.display !== 'none') {
container.style.display = 'none'; return;
}
try {
const members = await window.TbmAPI.getTeamMembers(sessionId);
container.innerHTML = members.map(m => {
const hours = m.work_hours != null ? parseFloat(m.work_hours) : 8;
return `
`;
}).join('') || '팀원 없음
';
container.style.display = 'block';
} catch(e) {
container.innerHTML = '로드 오류
';
container.style.display = 'block';
}
}
window.togglePullSessionMembers = togglePullSessionMembers;
async function executePull(sourceSessionId, workerId, workerName) {
const hoursInput = document.getElementById(`pull_h_${sourceSessionId}_${workerId}`);
const hours = parseFloat(hoursInput?.value);
if (!hours || hours <= 0) { showToast('시간을 입력하세요', 'error'); return; }
try {
const res = await window.TbmAPI.transfer({
transfer_type: 'pull',
user_id: workerId,
source_session_id: sourceSessionId,
dest_session_id: pullModalSessionId,
hours: hours
});
showToast(`${workerName} ${hours}h 빼오기 완료` + (res.data?.warning ? ` (${res.data.warning})` : ''), 'success');
closePullModal();
if (currentTab === 'tbm-input') await loadTodayOnlyTbm(); else await loadRecentTbmGroupedByDate();
} catch(e) {
console.error('빼오기 오류:', e);
showToast(e.message || '빼오기 처리 오류', 'error');
}
}
window.executePull = executePull;
function closePullModal() {
const modal = document.getElementById('pullModal');
if (modal) modal.style.display = 'none';
unlockBodyScroll();
}
window.closePullModal = closePullModal;
// showToast → api-base.js 전역 사용