// 작업장 현황 JavaScript let selectedCategory = null; let workplaceData = []; let mapRegions = []; // 작업장 영역 데이터 let canvas = null; let ctx = null; let canvasImage = null; // 금일 TBM 작업자 데이터 let todayWorkers = []; // 금일 출입 신청 데이터 let todayVisitors = []; // ==================== 초기화 ==================== document.addEventListener('DOMContentLoaded', async () => { await loadCategories(); // 이벤트 리스너 document.getElementById('categorySelect').addEventListener('change', onCategoryChange); document.getElementById('refreshMapBtn').addEventListener('click', refreshMapData); // 기본값으로 제1공장 선택 await selectFirstCategory(); }); // ==================== 카테고리 (공장) 로드 ==================== async function loadCategories() { try { const response = await window.apiCall('/workplaces/categories', 'GET'); if (response && response.success) { const categories = response.data || []; const select = document.getElementById('categorySelect'); categories.forEach(cat => { const option = document.createElement('option'); option.value = cat.category_id; option.textContent = cat.category_name; option.dataset.layoutImage = cat.layout_image; select.appendChild(option); }); } } catch (error) { console.error('카테고리 로드 오류:', error); } } /** * 첫 번째 카테고리 자동 선택 */ async function selectFirstCategory() { const select = document.getElementById('categorySelect'); if (select.options.length > 1) { // 첫 번째 옵션 선택 (인덱스 0은 "공장을 선택하세요") select.selectedIndex = 1; // 변경 이벤트 트리거 await onCategoryChange({ target: select }); } } // ==================== 공장 선택 ==================== async function onCategoryChange(e) { const categoryId = e.target.value; if (!categoryId) { document.getElementById('workplaceMapContainer').style.display = 'none'; document.getElementById('mapPlaceholder').style.display = 'flex'; return; } const selectedOption = e.target.options[e.target.selectedIndex]; const layoutImage = selectedOption.dataset.layoutImage; selectedCategory = { category_id: categoryId, category_name: selectedOption.textContent, layout_image: layoutImage }; // 지도 로드 await loadWorkplaceMap(); // 금일 작업 데이터 로드 await loadTodayData(); // 지도 렌더링 renderMap(); } // ==================== 작업장 지도 로드 ==================== async function loadWorkplaceMap() { try { // 작업장 데이터 로드 const response = await window.apiCall(`/workplaces?category_id=${selectedCategory.category_id}`, 'GET'); if (response && response.success) { workplaceData = response.data || []; } // 작업장 영역 데이터 로드 (map-regions API) const regionsResponse = await window.apiCall(`/workplaces/categories/${selectedCategory.category_id}/map-regions`, 'GET'); if (regionsResponse && regionsResponse.success) { mapRegions = regionsResponse.data || []; console.log('[지도] 로드된 영역:', mapRegions); } // 이미지 로드 await loadMapImage(); // 지도 컨테이너 표시 document.getElementById('mapPlaceholder').style.display = 'none'; document.getElementById('workplaceMapContainer').style.display = 'block'; } catch (error) { console.error('작업장 데이터 로드 오류:', error); } } async function loadMapImage() { return new Promise((resolve, reject) => { const img = new Image(); const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', ''); const fullImageUrl = selectedCategory.layout_image.startsWith('http') ? selectedCategory.layout_image : `${baseUrl}${selectedCategory.layout_image}`; img.onload = () => { canvasImage = img; // 캔버스 초기화 canvas = document.getElementById('workplaceMapCanvas'); canvas.width = img.width; canvas.height = img.height; ctx = canvas.getContext('2d'); // 클릭 이벤트 canvas.addEventListener('click', onMapClick); resolve(); }; img.onerror = () => { console.error('이미지 로드 실패:', fullImageUrl); reject(); }; img.src = fullImageUrl; }); } // ==================== 금일 데이터 로드 ==================== async function loadTodayData() { // 로컬 시간대 기준으로 오늘 날짜 구하기 (UTC가 아닌 한국 시간 기준) const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const today = `${year}-${month}-${day}`; console.log('[대시보드] 조회 날짜 (로컬):', today); // TBM 작업자 데이터 로드 await loadTodayWorkers(today); // 출입 신청 데이터 로드 await loadTodayVisitors(today); } async function loadTodayWorkers(date) { try { const response = await window.apiCall(`/tbm/sessions/date/${date}`, 'GET'); if (response && response.success) { const sessions = response.data || []; todayWorkers = []; // 각 세션의 작업 정보 추가 sessions.forEach(session => { if (session.workplace_id) { const memberCount = session.team_member_count || 0; const leaderCount = session.leader_id ? 1 : 0; const totalCount = memberCount + leaderCount; todayWorkers.push({ workplace_id: session.workplace_id, task_name: session.task_name || '작업', work_location: session.work_location || '', member_count: totalCount, project_name: session.project_name || '' }); console.log(`[TBM] 작업 추가: ${session.work_location || session.workplace_id} - ${session.task_name} (${totalCount}명)`); } }); console.log('로드된 작업자:', todayWorkers); } } catch (error) { console.error('TBM 작업자 데이터 로드 오류:', error); } } async function loadTodayVisitors(date) { try { // 날짜 형식 확인 (YYYY-MM-DD) const formattedDate = date.split('T')[0]; const response = await window.apiCall(`/workplace-visits/requests`, 'GET'); if (response && response.success) { const requests = response.data || []; // 금일 날짜와 승인된 요청 필터링 todayVisitors = requests.filter(req => { // UTC 변환 없이 로컬 날짜로 비교 const visitDateObj = new Date(req.visit_date); const visitYear = visitDateObj.getFullYear(); const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0'); const visitDay = String(visitDateObj.getDate()).padStart(2, '0'); const visitDate = `${visitYear}-${visitMonth}-${visitDay}`; return visitDate === formattedDate && (req.status === 'approved' || req.status === 'training_completed'); }).map(req => ({ workplace_id: req.workplace_id, visitor_company: req.visitor_company, visitor_count: req.visitor_count, visit_time: req.visit_time, purpose_name: req.purpose_name, status: req.status })); console.log('로드된 방문자:', todayVisitors); } } catch (error) { console.error('출입 신청 데이터 로드 오류:', error); } } // ==================== 지도 렌더링 ==================== function renderMap() { if (!canvas || !ctx || !canvasImage) return; // 이미지 그리기 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(canvasImage, 0, 0); // 모든 작업장 영역 표시 mapRegions.forEach(region => { // 해당 작업장의 작업자/방문자 인원 계산 const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id); const totalWorkerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0); const totalVisitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0); // 영역 그리기 drawWorkplaceRegion(region, totalWorkerCount, totalVisitorCount); }); } function drawWorkplaceRegion(region, workerCount, visitorCount) { // 사각형 좌표 변환 const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; const x2 = (region.x_end / 100) * canvas.width; const y2 = (region.y_end / 100) * canvas.height; const width = x2 - x1; const height = y2 - y1; const centerX = x1 + width / 2; const centerY = y1 + height / 2; // 색상 결정 let fillColor, strokeColor; const hasActivity = workerCount > 0 || visitorCount > 0; if (workerCount > 0 && visitorCount > 0) { // 둘 다 있음 - 초록색 fillColor = 'rgba(34, 197, 94, 0.3)'; strokeColor = 'rgb(34, 197, 94)'; } else if (workerCount > 0) { // 내부 작업자만 - 파란색 fillColor = 'rgba(59, 130, 246, 0.3)'; strokeColor = 'rgb(59, 130, 246)'; } else if (visitorCount > 0) { // 외부 방문자만 - 보라색 fillColor = 'rgba(168, 85, 247, 0.3)'; strokeColor = 'rgb(168, 85, 247)'; } else { // 인원 없음 - 회색 테두리만 fillColor = 'rgba(0, 0, 0, 0)'; // 투명 strokeColor = 'rgb(156, 163, 175)'; // 회색 } // 사각형 그리기 ctx.save(); ctx.fillStyle = fillColor; ctx.fillRect(x1, y1, width, height); ctx.strokeStyle = strokeColor; ctx.lineWidth = hasActivity ? 3 : 2; ctx.strokeRect(x1, y1, width, height); ctx.restore(); // 인원수 표시 (인원이 있을 때만) if (hasActivity) { ctx.save(); ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // 배경 원 ctx.beginPath(); ctx.arc(centerX, centerY, 20, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = strokeColor; ctx.lineWidth = 2; ctx.stroke(); // 텍스트 const totalCount = workerCount + visitorCount; ctx.fillStyle = strokeColor; ctx.fillText(totalCount.toString(), centerX, centerY); ctx.restore(); } else { // 인원이 없을 때는 작업장 이름만 표시 ctx.save(); ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgb(107, 114, 128)'; ctx.fillText(region.workplace_name, centerX, centerY); ctx.restore(); } } // ==================== 지도 클릭 ==================== function onMapClick(e) { const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width * canvas.width; const y = (e.clientY - rect.top) / rect.height * canvas.height; // 클릭한 위치의 작업장 영역 찾기 for (const region of mapRegions) { if (isPointInRegion(x, y, region)) { // 작업장 정보를 찾아서 모달 표시 const workplace = workplaceData.find(w => w.workplace_id === region.workplace_id); if (workplace) { showWorkplaceDetail({ ...workplace, ...region }); } else { // 작업장 정보가 없으면 region 데이터만 사용 showWorkplaceDetail(region); } break; } } } function isPointInRegion(x, y, region) { // 사각형 영역 내부 체크 const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; const x2 = (region.x_end / 100) * canvas.width; const y2 = (region.y_end / 100) * canvas.height; return x >= x1 && x <= x2 && y >= y1 && y <= y2; } // ==================== 작업장 상세 정보 모달 ==================== function showWorkplaceDetail(workplace) { const workers = todayWorkers.filter(w => w.workplace_id === workplace.workplace_id); const visitors = todayVisitors.filter(v => v.workplace_id === workplace.workplace_id); // 모달 제목 document.getElementById('modalWorkplaceName').textContent = `${selectedCategory.category_name} - ${workplace.workplace_name}`; // 내부 작업자 목록 const workersList = document.getElementById('internalWorkersList'); if (workers.length === 0) { workersList.innerHTML = '

금일 작업 예정 인원이 없습니다.

'; } else { let html = '
'; workers.forEach(worker => { html += `
${worker.task_name} ${worker.member_count}명
${worker.work_location ? `
📍 ${worker.work_location}
` : ''} ${worker.project_name ? `
📁 ${worker.project_name}
` : ''}
`; }); html += '
'; workersList.innerHTML = html; } // 외부 방문자 목록 const visitorsList = document.getElementById('externalVisitorsList'); if (visitors.length === 0) { visitorsList.innerHTML = '

금일 방문 예정 인원이 없습니다.

'; } else { let html = '
'; visitors.forEach(visitor => { const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨'; const statusColor = visitor.status === 'training_completed' ? 'var(--green-500)' : 'var(--yellow-500)'; html += `
${visitor.visitor_company} ${visitor.visitor_count}명
${statusText}
⏰ 방문 시간: ${visitor.visit_time}
📋 목적: ${visitor.purpose_name}
`; }); html += '
'; visitorsList.innerHTML = html; } // 모달 표시 document.getElementById('workplaceDetailModal').style.display = 'flex'; } function closeWorkplaceModal() { document.getElementById('workplaceDetailModal').style.display = 'none'; } // ==================== 새로고침 ==================== async function refreshMapData() { if (!selectedCategory) return; await loadTodayData(); renderMap(); } // 전역 함수로 노출 window.closeWorkplaceModal = closeWorkplaceModal;