// 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 () => { // 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 `
${displayDate} ${dayName}요일 ${isToday ? '오늘' : ''} ${sessions.length}건
${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 `

${leaderName} ${leaderRole}

${statusBadge}
📅 ${escapeHtml(formatDate(session.session_date))} ${session.start_time ? '| ' + escapeHtml(session.start_time) : ''}
프로젝트 ${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})`; } // 입력자 자동 설정 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 { // 어드민: 작업자 목록에 없음 → 이름 표시, leaderId 비움 document.getElementById('leaderName').textContent = currentUser.name || '관리자'; document.getElementById('leaderId').value = ''; } } 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 => ``).join(''); } // 공정 드롭다운 채우기 const wtSelect = document.getElementById('newTbmWorkTypeId'); if (wtSelect) { wtSelect.innerHTML = '' + allWorkTypes.map(wt => ``).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 assigned = assignment && assignment.sessions && assignment.sessions.length > 0; let badgeHtml = ''; let disabledAttr = ''; let disabledStyle = ''; if (assigned) { const leaderNames = assignment.sessions.map(s => s.leader_name || '').join(', '); badgeHtml = `배정됨 - ${escapeHtml(leaderNames)} TBM`; disabledAttr = 'disabled'; disabledStyle = 'opacity:0.5; pointer-events:none;'; } return ` `; }).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.sessions && a.sessions.length > 0) 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.sessions && a.sessions.length > 0) 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; // 로그인한 사용자가 작업자와 연결되어 있는지 확인 const isWorker = currentUser && currentUser.user_id && allWorkers.find(w => w.user_id === currentUser.user_id); if (isWorker) { // 작업자와 연결된 경우: 자동으로 선택하고 비활성화 const worker = isWorker; const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : ''; leaderSelect.innerHTML = ``; leaderSelect.disabled = true; } else { // 관리자 또는 작업자 목록에 없는 계정: 드롭다운으로 선택 가능 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 ``; }).join(''); leaderSelect.disabled = false; } } // 프로젝트 선택 드롭다운 채우기 function populateProjectSelect() { const projectSelect = document.getElementById('projectId'); if (!projectSelect) return; projectSelect.innerHTML = '' + allProjects.map(p => ` `).join(''); } // 공정(Work Type) 선택 드롭다운 채우기 function populateWorkTypeSelect() { const workTypeSelect = document.getElementById('workTypeId'); if (!workTypeSelect) return; workTypeSelect.innerHTML = '' + allWorkTypes.map(wt => ` `).join(''); } // 작업장 선택 드롭다운 채우기 function populateWorkplaceSelect() { const workLocationSelect = document.getElementById('workLocation'); if (!workLocationSelect) return; workLocationSelect.innerHTML = '' + allWorkplaces.map(wp => ` `).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 => ` `).join(''); if (filteredTasks.length === 0) { taskSelect.innerHTML = ''; taskSelect.disabled = true; } } window.loadTasksByWorkType = loadTasksByWorkType; // TBM 모달 닫기 function closeTbmModal() { document.getElementById('tbmModal').style.display = 'none'; unlockBodyScroll(); // 생성 모드로 복원 const createSection = document.getElementById('newTbmWorkerGrid')?.closest('.tbm-form-section'); const editSection = document.getElementById('workerTaskListSection'); if (createSection) createSection.style.display = ''; if (editSection) editSection.style.display = 'none'; } window.closeTbmModal = closeTbmModal; // TBM 세션 저장 (간소화: 프로젝트+공정+작업자, task/workplace=null) async function saveTbmSession() { let leaderId = parseInt(document.getElementById('leaderId').value); if (!leaderId || isNaN(leaderId)) { // 어드민이거나 작업자 목록에 없는 사용자: leaderId null로 저장 가능 leaderId = null; } 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 }); }); let createdSessionId = null; try { const response = await window.TbmAPI.createTbmSession(sessionData); if (response && response.success) { createdSessionId = response.data.session_id; 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); // 409 중복 배정 에러 처리 if (error.duplicates && error.duplicates.length > 0) { // 고아 세션 삭제 if (createdSessionId) { try { await window.TbmAPI.deleteSession(createdSessionId); } catch(e) {} } // 중복 작업자 자동 해제 const dupIds = new Set(error.duplicates.map(d => d.user_id)); dupIds.forEach(uid => { selectedWorkersForNewTbm.delete(uid); }); // 배정 현황 캐시 갱신 후 그리드 새로고침 todayAssignmentsMap = null; await renderNewTbmWorkerGrid(); showToast(error.message, 'error'); } else { 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 `
${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 => ` `).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 => ` `).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 => ` `).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 => ` `).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}`; // 지도 영역 데이터 로드 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; }; 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 = ''; } // 생성 모드 섹션 숨기고 편집 모드 섹션 표시 const createSection = document.getElementById('newTbmWorkerGrid')?.closest('.tbm-form-section'); const editSection = document.getElementById('workerTaskListSection'); if (createSection) createSection.style.display = 'none'; if (editSection) editSection.style.display = 'block'; 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 => `
`).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 = '' + ''; completeModalTeam.forEach((m, i) => { html += ``; }); html += '
작업자직종근태추가
${m.worker_name || ''} ${m.job_type || '-'}
'; 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; 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 => ` `).join(''); // 인계할 팀원 목록 const handoverTeamList = document.getElementById('handoverTeamList'); if (team.length === 0) { handoverTeamList.innerHTML = '

팀 구성이 없습니다.

'; } else { handoverTeamList.innerHTML = team.map(member => ` `).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 `
${escapeHtml(m.worker_name)} (${hours}h)
`; }).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 `
${leader} (${count}명)
`; }).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 `
${escapeHtml(m.worker_name)} (${hours}h)
`; }).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 전역 사용