// 작업장 관리 페이지 JavaScript // // 참고: 이 파일은 점진적 마이그레이션 중입니다. // 새로운 모듈 시스템: /js/workplace-management/ // - state.js: 전역 상태 관리 // - utils.js: 유틸리티 함수 // - api.js: API 클라이언트 // - index.js: 메인 컨트롤러 // 전역 변수 (모듈 시스템이 없을 때만 사용) let categories = window.WorkplaceState?.categories || []; let workplaces = window.WorkplaceState?.workplaces || []; let currentCategoryId = window.WorkplaceState?.currentCategoryId || ''; let currentEditingCategory = null; let currentEditingWorkplace = null; // 페이지 초기화 (모듈 시스템이 없을 때만) document.addEventListener('DOMContentLoaded', function() { // 모듈 시스템이 이미 로드되어 있으면 초기화 건너뜀 if (window.WorkplaceController) { console.log('[workplace-management.js] 모듈 시스템 감지 - 기존 초기화 건너뜀'); return; } console.log('🏗️ 작업장 관리 페이지 초기화 시작 (레거시)'); loadAllData(); }); // 모든 데이터 로드 async function loadAllData() { // 모듈 시스템이 있으면 위임 if (window.WorkplaceController) { return window.WorkplaceController.loadAllData(); } try { await Promise.all([ loadCategories(), loadWorkplaces() ]); renderCategoryTabs(); renderWorkplaces(); updateStatistics(); } catch (error) { console.error('데이터 로딩 오류:', error); showToast('데이터를 불러오는데 실패했습니다.', 'error'); } } // ==================== 카테고리(공장) 관련 ==================== // 카테고리 목록 로드 async function loadCategories() { // 모듈 시스템이 있으면 위임 if (window.WorkplaceAPI) { const result = await window.WorkplaceAPI.loadCategories(); categories = window.WorkplaceState?.categories || result; return result; } try { const response = await window.apiCall('/workplaces/categories', 'GET'); let categoryData = []; if (response && response.success && Array.isArray(response.data)) { categoryData = response.data; } else if (Array.isArray(response)) { categoryData = response; } categories = categoryData; console.log(`✅ 카테고리 ${categories.length}개 로드 완료`); } catch (error) { console.error('카테고리 로딩 오류:', error); categories = []; } } // 카테고리 탭 렌더링 function renderCategoryTabs() { const tabsContainer = document.getElementById('categoryTabs'); if (!tabsContainer) return; // 전체 탭은 항상 표시 let tabsHtml = ` `; // 각 카테고리 탭 추가 categories.forEach(category => { const count = workplaces.filter(w => w.category_id === category.category_id).length; const isActive = currentCategoryId === category.category_id; tabsHtml += ` `; }); tabsContainer.innerHTML = tabsHtml; } // 카테고리 전환 function switchCategory(categoryId) { currentCategoryId = categoryId === '' ? '' : categoryId; renderCategoryTabs(); renderWorkplaces(); // 레이아웃 지도 섹션 표시/숨김 const layoutMapSection = document.getElementById('layoutMapSection'); const selectedCategoryName = document.getElementById('selectedCategoryName'); if (currentCategoryId && layoutMapSection) { const category = categories.find(c => c.category_id == currentCategoryId); if (category) { layoutMapSection.style.display = 'block'; if (selectedCategoryName) { selectedCategoryName.textContent = category.category_name; } // 레이아웃 미리보기 업데이트 updateLayoutPreview(category); } } else if (layoutMapSection) { layoutMapSection.style.display = 'none'; } } // 레이아웃 미리보기 업데이트 async function updateLayoutPreview(category) { const previewDiv = document.getElementById('layoutMapPreview'); if (!previewDiv) return; if (category.layout_image) { // 이미지 경로를 전체 URL로 변환 const fullImageUrl = category.layout_image.startsWith('http') ? category.layout_image : `${window.API_BASE_URL || 'http://localhost:20005/api'}${category.layout_image}`.replace('/api/', '/'); // Canvas 컨테이너 생성 previewDiv.innerHTML = `

클릭하여 작업장 영역을 수정하려면 "지도 설정" 버튼을 누르세요

`; // 이미지와 영역 로드 await loadImageWithRegions(fullImageUrl, category.category_id); } else { previewDiv.innerHTML = `
🗺️

이 공장의 레이아웃 이미지가 아직 등록되지 않았습니다

"지도 설정" 버튼을 눌러 레이아웃 이미지를 업로드하고 작업장 위치를 지정하세요

`; } } // 이미지와 영역을 캔버스에 로드 async function loadImageWithRegions(imageUrl, categoryId) { const canvas = document.getElementById('previewCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = async function() { // 최대 너비 설정 (반응형) const maxWidth = 800; const scale = img.width > maxWidth ? maxWidth / img.width : 1; canvas.width = img.width * scale; canvas.height = img.height * scale; // 이미지 그리기 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 영역 데이터 로드 및 그리기 try { const response = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`, 'GET'); let regions = []; if (response && response.success && Array.isArray(response.data)) { regions = response.data; } else if (Array.isArray(response)) { regions = response; } // 각 영역 그리기 regions.forEach(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; const width = x2 - x1; const height = y2 - y1; // 영역 테두리 그리기 ctx.strokeStyle = '#10b981'; ctx.lineWidth = 2; ctx.strokeRect(x1, y1, width, height); // 영역 배경 (반투명) ctx.fillStyle = 'rgba(16, 185, 129, 0.15)'; ctx.fillRect(x1, y1, width, height); // 작업장 이름 표시 if (region.workplace_name) { ctx.fillStyle = '#10b981'; ctx.font = 'bold 14px sans-serif'; // 텍스트 배경 const textMetrics = ctx.measureText(region.workplace_name); const textPadding = 4; ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.fillRect(x1 + 5, y1 + 5, textMetrics.width + textPadding * 2, 20); // 텍스트 ctx.fillStyle = '#10b981'; ctx.fillText(region.workplace_name, x1 + 5 + textPadding, y1 + 20); } }); if (regions.length > 0) { console.log(`✅ 레이아웃 미리보기에 ${regions.length}개 영역 표시 완료`); } } catch (error) { console.error('영역 로드 오류:', error); } }; img.onerror = function() { console.error('이미지 로드 실패:', imageUrl); }; img.src = imageUrl; } // 작업장 카드에 지도 썸네일 로드 async function loadWorkplaceMapThumbnail(workplace) { const thumbnailDiv = document.getElementById(`workplace-map-${workplace.workplace_id}`); if (!thumbnailDiv) return; // 작업장 자체에 레이아웃 이미지가 있는 경우 if (workplace.layout_image) { const fullImageUrl = workplace.layout_image.startsWith('http') ? workplace.layout_image : `${window.API_BASE_URL || 'http://localhost:20005/api'}${workplace.layout_image}`.replace('/api/', '/'); // 설비 정보 로드 let equipmentCount = 0; try { const eqResponse = await window.apiCall(`/equipments/workplace/${workplace.workplace_id}`, 'GET'); if (eqResponse && eqResponse.success && Array.isArray(eqResponse.data)) { equipmentCount = eqResponse.data.filter(eq => eq.map_x_percent != null).length; } } catch (e) { console.debug('설비 정보 로드 실패'); } const canvasId = `layout-canvas-${workplace.workplace_id}`; thumbnailDiv.innerHTML = `
📍 작업장 지도 ${equipmentCount > 0 ? `설비 ${equipmentCount}개` : ''}
클릭하여 지도 관리
`; // 캔버스에 이미지 및 설비 영역 그리기 await loadWorkplaceCanvasWithEquipments(workplace.workplace_id, fullImageUrl, canvasId); return; } // 작업장에 이미지가 없으면 카테고리 지도의 영역 표시 try { // 해당 작업장의 지도 영역 정보 가져오기 const response = await window.apiCall(`/workplaces/map-regions/workplace/${workplace.workplace_id}`, 'GET'); if (!response || (!response.success && !response.region_id)) { // 영역이 정의되지 않은 경우 지도 업로드 안내 표시 thumbnailDiv.innerHTML = `
🗺️
클릭하여 지도 설정
`; return; } const region = response.success ? response.data : response; // 영역 좌표 데이터가 없으면 종료 if (!region || region.x_start === undefined || region.y_start === undefined || region.x_end === undefined || region.y_end === undefined) { return; } // 카테고리 정보에서 레이아웃 이미지 가져오기 const category = categories.find(c => c.category_id === workplace.category_id); if (!category || !category.layout_image) return; const fullImageUrl = category.layout_image.startsWith('http') ? category.layout_image : `${window.API_BASE_URL || 'http://localhost:20005/api'}${category.layout_image}`.replace('/api/', '/'); // 캔버스 생성 const canvasId = `thumbnail-canvas-${workplace.workplace_id}`; thumbnailDiv.innerHTML = `
📍 공장 지도 내 위치
클릭하여 상세 지도 설정
`; // 이미지 로드 const img = new Image(); img.onload = function() { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); // 원본 이미지에서 영역 좌표 계산 (퍼센트를 픽셀로) const x1 = (region.x_start / 100) * img.width; const y1 = (region.y_start / 100) * img.height; const x2 = (region.x_end / 100) * img.width; const y2 = (region.y_end / 100) * img.height; const regionWidth = x2 - x1; const regionHeight = y2 - y1; // 썸네일 크기 설정 (최대 너비 350px) const maxThumbWidth = 350; const scale = regionWidth > maxThumbWidth ? maxThumbWidth / regionWidth : 1; canvas.width = regionWidth * scale; canvas.height = regionHeight * scale; // 영역만 잘라서 그리기 ctx.drawImage( img, x1, y1, regionWidth, regionHeight, // 원본에서 잘라낼 영역 0, 0, canvas.width, canvas.height // 캔버스에 그릴 위치와 크기 ); // 테두리 그리기 ctx.strokeStyle = '#10b981'; ctx.lineWidth = 3; ctx.strokeRect(0, 0, canvas.width, canvas.height); }; img.onerror = function() { thumbnailDiv.innerHTML = ''; }; img.src = fullImageUrl; } catch (error) { // 오류 시 조용히 처리 (지도가 없는 작업장도 많을 수 있으므로) console.debug(`작업장 ${workplace.workplace_id}의 지도 영역 없음`); } } // 작업장 캔버스에 설비 영역 함께 그리기 async function loadWorkplaceCanvasWithEquipments(workplaceId, imageUrl, canvasId) { const img = new Image(); img.onload = async function() { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); // 썸네일 크기 설정 (최대 너비 400px) const maxThumbWidth = 400; const scale = img.width > maxThumbWidth ? maxThumbWidth / img.width : 1; canvas.width = img.width * scale; canvas.height = img.height * scale; // 이미지 그리기 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 설비 영역 로드 및 그리기 try { const response = await window.apiCall(`/equipments/workplace/${workplaceId}`, 'GET'); let equipments = []; if (response && response.success && Array.isArray(response.data)) { equipments = response.data.filter(eq => eq.map_x_percent != null); } // 설비 영역 그리기 equipments.forEach(eq => { const x = (parseFloat(eq.map_x_percent) / 100) * canvas.width; const y = (parseFloat(eq.map_y_percent) / 100) * canvas.height; const width = (parseFloat(eq.map_width_percent || 10) / 100) * canvas.width; const height = (parseFloat(eq.map_height_percent || 10) / 100) * canvas.height; // 영역 배경 ctx.fillStyle = 'rgba(16, 185, 129, 0.2)'; ctx.fillRect(x, y, width, height); // 영역 테두리 ctx.strokeStyle = '#10b981'; ctx.lineWidth = 2; ctx.strokeRect(x, y, width, height); // 설비 코드 표시 if (eq.equipment_code) { ctx.font = 'bold 10px sans-serif'; const textMetrics = ctx.measureText(eq.equipment_code); ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.fillRect(x + 2, y + 2, textMetrics.width + 6, 14); ctx.fillStyle = '#047857'; ctx.fillText(eq.equipment_code, x + 5, y + 12); } }); } catch (error) { console.debug('설비 영역 로드 실패'); } }; img.src = imageUrl; } // 카테고리 모달 열기 function openCategoryModal(categoryData = null) { const modal = document.getElementById('categoryModal'); const modalTitle = document.getElementById('categoryModalTitle'); const deleteBtn = document.getElementById('deleteCategoryBtn'); if (!modal) return; currentEditingCategory = categoryData; if (categoryData) { // 수정 모드 modalTitle.textContent = '공장 수정'; deleteBtn.style.display = 'inline-flex'; document.getElementById('categoryId').value = categoryData.category_id; document.getElementById('categoryName').value = categoryData.category_name || ''; document.getElementById('categoryDescription').value = categoryData.description || ''; document.getElementById('categoryOrder').value = categoryData.display_order || 0; } else { // 신규 등록 모드 modalTitle.textContent = '공장 추가'; deleteBtn.style.display = 'none'; document.getElementById('categoryForm').reset(); document.getElementById('categoryId').value = ''; } modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; setTimeout(() => { document.getElementById('categoryName').focus(); }, 100); } // 카테고리 모달 닫기 function closeCategoryModal() { const modal = document.getElementById('categoryModal'); if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; currentEditingCategory = null; } } // 카테고리 저장 async function saveCategory() { try { const categoryId = document.getElementById('categoryId').value; const categoryData = { category_name: document.getElementById('categoryName').value.trim(), description: document.getElementById('categoryDescription').value.trim() || null, display_order: parseInt(document.getElementById('categoryOrder').value) || 0, is_active: true }; if (!categoryData.category_name) { showToast('공장명은 필수 입력 항목입니다.', 'error'); return; } console.log('💾 저장할 카테고리 데이터:', categoryData); let response; if (categoryId) { // 수정 response = await window.apiCall(`/workplaces/categories/${categoryId}`, 'PUT', categoryData); } else { // 신규 등록 response = await window.apiCall('/workplaces/categories', 'POST', categoryData); } if (response && (response.success || response.category_id)) { const action = categoryId ? '수정' : '등록'; showToast(`공장이 성공적으로 ${action}되었습니다.`, 'success'); closeCategoryModal(); await loadAllData(); } else { throw new Error(response?.message || '저장에 실패했습니다.'); } } catch (error) { console.error('카테고리 저장 오류:', error); showToast(error.message || '카테고리 저장 중 오류가 발생했습니다.', 'error'); } } // 카테고리 삭제 async function deleteCategory() { if (!currentEditingCategory) return; if (!confirm(`"${currentEditingCategory.category_name}" 공장을 정말 삭제하시겠습니까?\n\n⚠️ 이 공장에 속한 모든 작업장의 공장 정보가 제거됩니다.`)) { return; } try { const response = await window.apiCall(`/workplaces/categories/${currentEditingCategory.category_id}`, 'DELETE'); if (response && response.success) { showToast('공장이 성공적으로 삭제되었습니다.', 'success'); closeCategoryModal(); await loadAllData(); } else { throw new Error(response?.message || '삭제에 실패했습니다.'); } } catch (error) { console.error('카테고리 삭제 오류:', error); showToast(error.message || '카테고리 삭제 중 오류가 발생했습니다.', 'error'); } } // ==================== 작업장 관련 ==================== // 작업장 목록 로드 async function loadWorkplaces() { try { const response = await window.apiCall('/workplaces', 'GET'); let workplaceData = []; if (response && response.success && Array.isArray(response.data)) { workplaceData = response.data; } else if (Array.isArray(response)) { workplaceData = response; } workplaces = workplaceData; console.log(`✅ 작업장 ${workplaces.length}개 로드 완료`); } catch (error) { console.error('작업장 로딩 오류:', error); workplaces = []; } } // 작업장 렌더링 function renderWorkplaces() { const grid = document.getElementById('workplaceGrid'); if (!grid) return; // 현재 카테고리별 필터링 const filtered = currentCategoryId === '' ? workplaces : workplaces.filter(w => w.category_id == currentCategoryId); if (filtered.length === 0) { grid.innerHTML = `
🏗️

등록된 작업장이 없습니다

"작업장 추가" 버튼을 눌러 작업장을 등록해보세요

`; return; } let gridHtml = ''; filtered.forEach(workplace => { const categoryName = workplace.category_name || '미분류'; const isActive = workplace.is_active === 1 || workplace.is_active === true; // 작업장 용도 아이콘 매핑 const purposeIcons = { '작업구역': '🔧', '설비': '⚙️', '휴게시설': '☕', '회의실': '💼', '창고': '📦', '기타': '📍' }; const purposeIcon = workplace.workplace_purpose ? purposeIcons[workplace.workplace_purpose] || '📍' : '🏗️'; gridHtml += `
${purposeIcon}

${workplace.workplace_name}

${workplace.category_id ? `🏭 ${categoryName}` : ''} ${workplace.workplace_purpose ? `${workplace.workplace_purpose}` : ''}
${workplace.description ? `

${workplace.description}

` : ''}
등록: ${formatDate(workplace.created_at)} ${workplace.updated_at !== workplace.created_at ? `수정: ${formatDate(workplace.updated_at)}` : ''}
`; }); grid.innerHTML = gridHtml; // 각 작업장의 지도 미리보기 로드 filtered.forEach(workplace => { if (workplace.category_id) { loadWorkplaceMapThumbnail(workplace); } }); } // 작업장 모달 열기 function openWorkplaceModal(workplaceData = null) { const modal = document.getElementById('workplaceModal'); const modalTitle = document.getElementById('workplaceModalTitle'); const deleteBtn = document.getElementById('deleteWorkplaceBtn'); const categorySelect = document.getElementById('workplaceCategoryId'); if (!modal) return; currentEditingWorkplace = workplaceData; // 카테고리 선택 옵션 업데이트 let categoryOptions = ''; categories.forEach(cat => { categoryOptions += ``; }); categorySelect.innerHTML = categoryOptions; if (workplaceData) { // 수정 모드 modalTitle.textContent = '작업장 수정'; deleteBtn.style.display = 'inline-flex'; document.getElementById('workplaceId').value = workplaceData.workplace_id; document.getElementById('workplaceCategoryId').value = workplaceData.category_id || ''; document.getElementById('workplaceName').value = workplaceData.workplace_name || ''; document.getElementById('workplacePurpose').value = workplaceData.workplace_purpose || ''; document.getElementById('displayPriority').value = workplaceData.display_priority || 0; document.getElementById('workplaceDescription').value = workplaceData.description || ''; } else { // 신규 등록 모드 modalTitle.textContent = '작업장 추가'; deleteBtn.style.display = 'none'; document.getElementById('workplaceForm').reset(); document.getElementById('workplaceId').value = ''; // 현재 선택된 카테고리가 있으면 자동 선택 if (currentCategoryId) { document.getElementById('workplaceCategoryId').value = currentCategoryId; } } modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; setTimeout(() => { document.getElementById('workplaceName').focus(); }, 100); } // 작업장 모달 닫기 function closeWorkplaceModal() { const modal = document.getElementById('workplaceModal'); if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; currentEditingWorkplace = null; } } // 작업장 편집 function editWorkplace(workplaceId) { const workplace = workplaces.find(w => w.workplace_id === workplaceId); if (workplace) { openWorkplaceModal(workplace); } else { showToast('작업장을 찾을 수 없습니다.', 'error'); } } // 작업장 저장 async function saveWorkplace() { try { const workplaceId = document.getElementById('workplaceId').value; const workplaceData = { category_id: document.getElementById('workplaceCategoryId').value || null, workplace_name: document.getElementById('workplaceName').value.trim(), workplace_purpose: document.getElementById('workplacePurpose').value || null, display_priority: parseInt(document.getElementById('displayPriority').value) || 0, description: document.getElementById('workplaceDescription').value.trim() || null, is_active: true }; if (!workplaceData.workplace_name) { showToast('작업장명은 필수 입력 항목입니다.', 'error'); return; } console.log('💾 저장할 작업장 데이터:', workplaceData); let response; if (workplaceId) { // 수정 response = await window.apiCall(`/workplaces/${workplaceId}`, 'PUT', workplaceData); } else { // 신규 등록 response = await window.apiCall('/workplaces', 'POST', workplaceData); } if (response && (response.success || response.workplace_id)) { const action = workplaceId ? '수정' : '등록'; showToast(`작업장이 성공적으로 ${action}되었습니다.`, 'success'); closeWorkplaceModal(); await loadAllData(); } else { throw new Error(response?.message || '저장에 실패했습니다.'); } } catch (error) { console.error('작업장 저장 오류:', error); showToast(error.message || '작업장 저장 중 오류가 발생했습니다.', 'error'); } } // 작업장 삭제 확인 function confirmDeleteWorkplace(workplaceId) { const workplace = workplaces.find(w => w.workplace_id === workplaceId); if (!workplace) { showToast('작업장을 찾을 수 없습니다.', 'error'); return; } if (confirm(`"${workplace.workplace_name}" 작업장을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 작업장은 복구할 수 없습니다.`)) { deleteWorkplaceById(workplaceId); } } // 작업장 삭제 (수정 모드에서) function deleteWorkplace() { if (currentEditingWorkplace) { confirmDeleteWorkplace(currentEditingWorkplace.workplace_id); } } // 작업장 삭제 실행 async function deleteWorkplaceById(workplaceId) { try { const response = await window.apiCall(`/workplaces/${workplaceId}`, 'DELETE'); if (response && response.success) { showToast('작업장이 성공적으로 삭제되었습니다.', 'success'); closeWorkplaceModal(); await loadAllData(); } else { throw new Error(response?.message || '삭제에 실패했습니다.'); } } catch (error) { console.error('작업장 삭제 오류:', error); showToast(error.message || '작업장 삭제 중 오류가 발생했습니다.', 'error'); } } // ==================== 유틸리티 ==================== // 전체 새로고침 async function refreshWorkplaces() { const refreshBtn = document.querySelector('.btn-secondary'); if (refreshBtn) { const originalText = refreshBtn.innerHTML; refreshBtn.innerHTML = '새로고침 중...'; refreshBtn.disabled = true; await loadAllData(); refreshBtn.innerHTML = originalText; refreshBtn.disabled = false; } else { await loadAllData(); } showToast('데이터가 새로고침되었습니다.', 'success'); } // 통계 업데이트 async function updateStatistics() { const total = workplaces.length; const active = workplaces.filter(w => w.is_active === 1 || w.is_active === true).length; const factoryTotal = categories.length; // 상단 통계 카드 업데이트 const factoryCountEl = document.getElementById('factoryCount'); const totalCountEl = document.getElementById('totalCount'); const activeCountEl = document.getElementById('activeCount'); const equipmentCountEl = document.getElementById('equipmentCount'); if (factoryCountEl) factoryCountEl.textContent = factoryTotal; if (totalCountEl) totalCountEl.textContent = total; if (activeCountEl) activeCountEl.textContent = active; // 설비 수 조회 및 업데이트 if (equipmentCountEl) { try { const response = await window.apiCall('/equipments', 'GET'); let equipmentCount = 0; if (response && response.success && Array.isArray(response.data)) { equipmentCount = response.data.length; } else if (Array.isArray(response)) { equipmentCount = response.length; } equipmentCountEl.textContent = equipmentCount; } catch (e) { equipmentCountEl.textContent = '-'; } } // 섹션 통계 업데이트 const sectionTotalEl = document.getElementById('sectionTotalCount'); const sectionActiveEl = document.getElementById('sectionActiveCount'); // 현재 필터링된 작업장 기준 const filtered = currentCategoryId === '' ? workplaces : workplaces.filter(w => w.category_id == currentCategoryId); const filteredActive = filtered.filter(w => w.is_active === 1 || w.is_active === true).length; if (sectionTotalEl) sectionTotalEl.textContent = filtered.length; if (sectionActiveEl) sectionActiveEl.textContent = filteredActive; } // 날짜 포맷팅 function formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }); } // 토스트 메시지 표시 function showToast(message, type = 'info') { // 기존 토스트 제거 const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } // 새 토스트 생성 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; // 스타일 적용 Object.assign(toast.style, { position: 'fixed', top: '20px', right: '20px', padding: '12px 24px', borderRadius: '8px', color: 'white', fontWeight: '500', zIndex: '1000', transform: 'translateX(100%)', transition: 'transform 0.3s ease' }); // 타입별 배경색 const colors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' }; toast.style.backgroundColor = colors[type] || colors.info; document.body.appendChild(toast); // 애니메이션 setTimeout(() => { toast.style.transform = 'translateX(0)'; }, 100); // 자동 제거 setTimeout(() => { toast.style.transform = 'translateX(100%)'; setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }, 3000); } // 전역 함수 및 변수로 노출 (다른 모듈에서 접근 가능하도록) // 모듈 시스템이 이미 정의했으면 건너뜀 if (!window.WorkplaceState) { // getter/setter를 사용하여 항상 최신 값을 반환 Object.defineProperty(window, 'categories', { get: function() { return categories; } }); Object.defineProperty(window, 'workplaces', { get: function() { return workplaces; } }); Object.defineProperty(window, 'currentCategoryId', { get: function() { return currentCategoryId; }, set: function(value) { currentCategoryId = value; } }); } // ==================== 작업장 지도 관리 ==================== // 작업장 지도 관련 전역 변수 let workplaceCanvas = null; let workplaceCtx = null; let workplaceImage = null; let workplaceIsDrawing = false; let workplaceStartX = 0; let workplaceStartY = 0; let workplaceCurrentRect = null; let workplaceEquipmentRegions = []; let existingEquipments = []; // DB에서 로드한 기존 설비 목록 let allEquipments = []; // 시스템 전체 설비 목록 (드롭다운 선택용) // 작업장 지도 모달 열기 async function openWorkplaceMapModal(workplaceId) { const workplace = workplaces.find(w => w.workplace_id === workplaceId); if (!workplace) { showToast('작업장 정보를 찾을 수 없습니다.', 'error'); return; } // 작업장 이름 설정 const modalTitle = document.getElementById('workplaceMapModalTitle'); if (modalTitle) { modalTitle.textContent = `${workplace.workplace_name} - 지도 관리`; } // 현재 작업장 ID 저장 window.currentWorkplaceMapId = workplaceId; // 레이아웃 이미지 미리보기 영역 초기화 const preview = document.getElementById('workplaceLayoutPreview'); if (preview && workplace.layout_image) { const fullImageUrl = workplace.layout_image.startsWith('http') ? workplace.layout_image : `${window.API_BASE_URL || 'http://localhost:20005/api'}${workplace.layout_image}`.replace('/api/', '/'); preview.innerHTML = `작업장 레이아웃`; // 캔버스 초기화 initWorkplaceCanvas(fullImageUrl); } else if (preview) { preview.innerHTML = '

레이아웃 이미지를 업로드해주세요

'; } // 설비 영역 목록 로드 (API 연동) await Promise.all([ loadWorkplaceEquipments(workplaceId), loadAllEquipments() ]); renderWorkplaceEquipmentList(); // 모달 표시 const modal = document.getElementById('workplaceMapModal'); if (modal) { modal.style.display = 'flex'; } } // 작업장의 설비 목록 로드 async function loadWorkplaceEquipments(workplaceId) { try { const response = await window.apiCall(`/equipments/workplace/${workplaceId}`, 'GET'); let equipments = []; if (response && response.success && Array.isArray(response.data)) { equipments = response.data; } else if (Array.isArray(response)) { equipments = response; } // 지도 영역이 있는 설비만 workplaceEquipmentRegions에 추가 workplaceEquipmentRegions = equipments .filter(eq => eq.map_x_percent != null && eq.map_y_percent != null) .map(eq => ({ equipment_id: eq.equipment_id, equipment_name: eq.equipment_name, equipment_code: eq.equipment_code, x_percent: parseFloat(eq.map_x_percent), y_percent: parseFloat(eq.map_y_percent), width_percent: parseFloat(eq.map_width_percent) || 10, height_percent: parseFloat(eq.map_height_percent) || 10 })); // 해당 작업장에 할당된 설비 목록 저장 existingEquipments = equipments; console.log(`✅ 작업장 ${workplaceId}의 설비 ${equipments.length}개 로드 완료 (지도 영역: ${workplaceEquipmentRegions.length}개)`); } catch (error) { console.error('설비 로드 오류:', error); workplaceEquipmentRegions = []; existingEquipments = []; } } // 시스템 전체 설비 목록 로드 (드롭다운 선택용) async function loadAllEquipments() { try { const response = await window.apiCall('/equipments', 'GET'); let equipments = []; if (response && response.success && Array.isArray(response.data)) { equipments = response.data; } else if (Array.isArray(response)) { equipments = response; } allEquipments = equipments; console.log(`✅ 전체 설비 ${allEquipments.length}개 로드 완료`); } catch (error) { console.error('전체 설비 로드 오류:', error); allEquipments = []; } } // 작업장 지도 모달 닫기 function closeWorkplaceMapModal() { const modal = document.getElementById('workplaceMapModal'); if (modal) { modal.style.display = 'none'; } window.currentWorkplaceMapId = null; } // 작업장 레이아웃 이미지 업로드 async function uploadWorkplaceLayout() { const fileInput = document.getElementById('workplaceLayoutFile'); if (!fileInput || !fileInput.files || !fileInput.files[0]) { showToast('파일을 선택해주세요.', 'warning'); return; } if (!window.currentWorkplaceMapId) { showToast('작업장 정보를 찾을 수 없습니다.', 'error'); return; } const formData = new FormData(); formData.append('image', fileInput.files[0]); try { showToast('이미지 업로드 중...', 'info'); const response = await fetch(`${window.API_BASE_URL || 'http://localhost:20005/api'}/workplaces/${window.currentWorkplaceMapId}/layout-image`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: formData }); const result = await response.json(); if (result.success) { showToast('레이아웃 이미지가 업로드되었습니다.', 'success'); // 작업장 목록 새로고침 await loadWorkplaces(); renderWorkplaces(); // 미리보기 업데이트 및 캔버스 초기화 const preview = document.getElementById('workplaceLayoutPreview'); if (preview && result.data.image_path) { const fullImageUrl = result.data.image_path.startsWith('http') ? result.data.image_path : `${window.API_BASE_URL || 'http://localhost:20005/api'}${result.data.image_path}`.replace('/api/', '/'); preview.innerHTML = `작업장 레이아웃`; // 캔버스 초기화 (설비 영역 편집용) initWorkplaceCanvas(fullImageUrl); } // 파일 입력 초기화 fileInput.value = ''; } else { showToast(result.message || '업로드 실패', 'error'); } } catch (error) { console.error('레이아웃 이미지 업로드 오류:', error); showToast('레이아웃 이미지 업로드 중 오류가 발생했습니다.', 'error'); } } // 작업장 캔버스 초기화 function initWorkplaceCanvas(imageUrl) { const img = new Image(); img.onload = function() { workplaceImage = img; workplaceCanvas = document.getElementById('workplaceRegionCanvas'); if (!workplaceCanvas) return; workplaceCtx = workplaceCanvas.getContext('2d'); // 캔버스 크기 설정 (최대 너비를 모달 크기에 맞게) const container = document.getElementById('workplaceCanvasContainer'); const maxWidth = container ? Math.min(container.clientWidth - 20, 900) : 800; const scale = img.width > maxWidth ? maxWidth / img.width : 1; workplaceCanvas.width = img.width * scale; workplaceCanvas.height = img.height * scale; // 이미지 그리기 workplaceCtx.drawImage(img, 0, 0, workplaceCanvas.width, workplaceCanvas.height); // 기존 영역들 표시 drawWorkplaceRegions(); // 이벤트 리스너 등록 (기존 리스너 제거 후 등록) workplaceCanvas.onmousedown = startWorkplaceDraw; workplaceCanvas.onmousemove = drawWorkplace; workplaceCanvas.onmouseup = endWorkplaceDraw; workplaceCanvas.onmouseleave = endWorkplaceDraw; // 터치 이벤트 지원 workplaceCanvas.ontouchstart = handleWorkplaceTouchStart; workplaceCanvas.ontouchmove = handleWorkplaceTouchMove; workplaceCanvas.ontouchend = endWorkplaceDraw; }; img.onerror = function() { console.error('작업장 이미지 로드 실패:', imageUrl); }; img.src = imageUrl; } // 터치 이벤트 핸들러 function handleWorkplaceTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const rect = workplaceCanvas.getBoundingClientRect(); workplaceIsDrawing = true; workplaceStartX = touch.clientX - rect.left; workplaceStartY = touch.clientY - rect.top; } function handleWorkplaceTouchMove(e) { e.preventDefault(); if (!workplaceIsDrawing) return; const touch = e.touches[0]; const rect = workplaceCanvas.getBoundingClientRect(); const currentX = touch.clientX - rect.left; const currentY = touch.clientY - rect.top; // 캔버스 초기화 및 이미지 다시 그리기 workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height); drawWorkplaceRegions(); // 현재 그리는 사각형 workplaceCtx.strokeStyle = '#3b82f6'; workplaceCtx.lineWidth = 3; workplaceCtx.setLineDash([5, 5]); workplaceCtx.strokeRect( workplaceStartX, workplaceStartY, currentX - workplaceStartX, currentY - workplaceStartY ); workplaceCtx.setLineDash([]); workplaceCurrentRect = { x: workplaceStartX, y: workplaceStartY, width: currentX - workplaceStartX, height: currentY - workplaceStartY }; } // 드래그 시작 function startWorkplaceDraw(e) { workplaceIsDrawing = true; const rect = workplaceCanvas.getBoundingClientRect(); workplaceStartX = e.clientX - rect.left; workplaceStartY = e.clientY - rect.top; } // 드래그 중 function drawWorkplace(e) { if (!workplaceIsDrawing) return; const rect = workplaceCanvas.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; // 캔버스 초기화 및 이미지 다시 그리기 workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height); // 기존 영역들 표시 drawWorkplaceRegions(); // 현재 그리는 사각형 (점선으로) workplaceCtx.strokeStyle = '#3b82f6'; workplaceCtx.lineWidth = 3; workplaceCtx.setLineDash([5, 5]); workplaceCtx.strokeRect( workplaceStartX, workplaceStartY, currentX - workplaceStartX, currentY - workplaceStartY ); workplaceCtx.setLineDash([]); // 선택 영역 배경 (반투명) workplaceCtx.fillStyle = 'rgba(59, 130, 246, 0.1)'; workplaceCtx.fillRect( workplaceStartX, workplaceStartY, currentX - workplaceStartX, currentY - workplaceStartY ); workplaceCurrentRect = { x: workplaceStartX, y: workplaceStartY, width: currentX - workplaceStartX, height: currentY - workplaceStartY }; } // 드래그 종료 function endWorkplaceDraw(e) { workplaceIsDrawing = false; } // 기존 영역들 그리기 function drawWorkplaceRegions() { workplaceEquipmentRegions.forEach((region, index) => { // 퍼센트를 픽셀로 변환 const x = (region.x_percent / 100) * workplaceCanvas.width; const y = (region.y_percent / 100) * workplaceCanvas.height; const width = (region.width_percent / 100) * workplaceCanvas.width; const height = (region.height_percent / 100) * workplaceCanvas.height; // 영역 배경 (반투명) workplaceCtx.fillStyle = 'rgba(16, 185, 129, 0.15)'; workplaceCtx.fillRect(x, y, width, height); // 영역 테두리 workplaceCtx.strokeStyle = '#10b981'; workplaceCtx.lineWidth = 2; workplaceCtx.strokeRect(x, y, width, height); // 영역 이름 표시 (배경 포함) const displayName = region.equipment_code ? `[${region.equipment_code}] ${region.equipment_name}` : region.equipment_name; workplaceCtx.font = 'bold 12px sans-serif'; const textMetrics = workplaceCtx.measureText(displayName); const textPadding = 4; // 텍스트 배경 workplaceCtx.fillStyle = 'rgba(255, 255, 255, 0.9)'; workplaceCtx.fillRect(x + 3, y + 3, textMetrics.width + textPadding * 2, 18); // 텍스트 workplaceCtx.fillStyle = '#047857'; workplaceCtx.fillText(displayName, x + 3 + textPadding, y + 16); }); } // 현재 영역 지우기 function clearWorkplaceCurrentRegion() { workplaceCurrentRect = null; if (workplaceCanvas && workplaceImage) { workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height); drawWorkplaceRegions(); } } // 설비 위치 저장 async function saveWorkplaceEquipmentRegion() { const equipmentSelect = document.getElementById('equipmentSelectInput'); const equipmentNameInput = document.getElementById('equipmentNameInput'); const equipmentCodeInput = document.getElementById('equipmentCodeInput'); const selectedEquipmentId = equipmentSelect?.value; const newEquipmentName = equipmentNameInput?.value.trim(); const newEquipmentCode = equipmentCodeInput?.value.trim(); // 기존 설비 선택 또는 새 설비 입력 확인 if (!selectedEquipmentId && (!newEquipmentName || !newEquipmentCode)) { showToast('기존 설비를 선택하거나 새 설비 코드와 이름을 입력해주세요.', 'warning'); return; } if (!workplaceCurrentRect) { showToast('영역을 드래그하여 선택해주세요.', 'warning'); return; } // 퍼센트로 변환 (음수 영역 처리) let xPercent = (Math.min(workplaceCurrentRect.x, workplaceCurrentRect.x + workplaceCurrentRect.width) / workplaceCanvas.width) * 100; let yPercent = (Math.min(workplaceCurrentRect.y, workplaceCurrentRect.y + workplaceCurrentRect.height) / workplaceCanvas.height) * 100; let widthPercent = (Math.abs(workplaceCurrentRect.width) / workplaceCanvas.width) * 100; let heightPercent = (Math.abs(workplaceCurrentRect.height) / workplaceCanvas.height) * 100; try { let equipmentId = selectedEquipmentId; let equipmentName = ''; if (selectedEquipmentId) { // 기존 설비 - 위치 및 작업장 업데이트 const response = await window.apiCall(`/equipments/${selectedEquipmentId}/map-position`, 'PATCH', { workplace_id: window.currentWorkplaceMapId, map_x_percent: xPercent, map_y_percent: yPercent, map_width_percent: widthPercent, map_height_percent: heightPercent }); if (!response || !response.success) { throw new Error(response?.message || '위치 저장 실패'); } const eq = allEquipments.find(e => e.equipment_id == selectedEquipmentId); equipmentName = eq?.equipment_name || '설비'; } else { // 새 설비 생성 const response = await window.apiCall('/equipments', 'POST', { equipment_code: newEquipmentCode, equipment_name: newEquipmentName, workplace_id: window.currentWorkplaceMapId, map_x_percent: xPercent, map_y_percent: yPercent, map_width_percent: widthPercent, map_height_percent: heightPercent, status: 'active' }); if (!response || !response.success) { throw new Error(response?.message || '설비 생성 실패'); } equipmentId = response.data.equipment_id; equipmentName = newEquipmentName; } // 로컬 배열 업데이트 const newRegion = { equipment_id: equipmentId, equipment_name: equipmentName, equipment_code: newEquipmentCode || existingEquipments.find(e => e.equipment_id == selectedEquipmentId)?.equipment_code, x_percent: xPercent, y_percent: yPercent, width_percent: widthPercent, height_percent: heightPercent }; // 기존 영역이 있으면 교체, 없으면 추가 const existingIndex = workplaceEquipmentRegions.findIndex(r => r.equipment_id == equipmentId); if (existingIndex >= 0) { workplaceEquipmentRegions[existingIndex] = newRegion; } else { workplaceEquipmentRegions.push(newRegion); } // 설비 목록 새로고침 (현재 작업장 + 전체) await Promise.all([ loadWorkplaceEquipments(window.currentWorkplaceMapId), loadAllEquipments() ]); // UI 업데이트 renderWorkplaceEquipmentList(); clearWorkplaceCurrentRegion(); if (equipmentNameInput) equipmentNameInput.value = ''; if (equipmentCodeInput) equipmentCodeInput.value = ''; if (equipmentSelect) equipmentSelect.value = ''; showToast(`설비 "${equipmentName}" 위치가 저장되었습니다.`, 'success'); } catch (error) { console.error('설비 위치 저장 오류:', error); showToast(error.message || '설비 위치 저장 중 오류가 발생했습니다.', 'error'); } } // 설비 목록 렌더링 function renderWorkplaceEquipmentList() { const listDiv = document.getElementById('workplaceEquipmentList'); // 등록된 설비 목록 렌더링 if (listDiv) { if (workplaceEquipmentRegions.length === 0) { listDiv.innerHTML = '

아직 정의된 설비가 없습니다

'; } else { let html = ''; workplaceEquipmentRegions.forEach((region, index) => { html += `
${region.equipment_name} [${region.equipment_code || '-'}]
위치: (${region.x_percent.toFixed(1)}%, ${region.y_percent.toFixed(1)}%) | 크기: ${region.width_percent.toFixed(1)}% × ${region.height_percent.toFixed(1)}%
`; }); listDiv.innerHTML = html; } } // 설비 선택 드롭다운 업데이트 (항상 호출되어야 함!) updateEquipmentSelectDropdown(); } // 설비 선택 드롭다운 업데이트 function updateEquipmentSelectDropdown() { const selectEl = document.getElementById('equipmentSelectInput'); if (!selectEl) { console.warn('⚠️ equipmentSelectInput 요소를 찾을 수 없습니다'); return; } console.log(`📋 드롭다운 업데이트: 전체 설비 ${allEquipments.length}개`); // 전체 설비 중에서 아직 어떤 지도에도 배치되지 않은 설비만 표시 // map_x_percent가 null이면 지도에 배치되지 않은 설비 const availableEquipments = allEquipments.filter(eq => eq.map_x_percent == null || eq.map_x_percent === '' ); console.log(`📋 지도에 미배치된 설비: ${availableEquipments.length}개`); // 이 작업장에 이미 배치된 설비는 제외 (현재 작업장에서 방금 배치한 경우) const registeredIds = workplaceEquipmentRegions.map(r => r.equipment_id); const unregisteredEquipments = availableEquipments.filter(eq => !registeredIds.includes(eq.equipment_id)); console.log(`📋 선택 가능한 설비: ${unregisteredEquipments.length}개`); let options = ''; if (unregisteredEquipments.length === 0) { options += ''; } else { unregisteredEquipments.forEach(eq => { const workplaceInfo = eq.workplace_name ? ` (${eq.workplace_name})` : ' (미배정)'; options += ``; }); } selectEl.innerHTML = options; // 배치 가능한 설비 수 표시 const countEl = document.getElementById('availableEquipmentCount'); if (countEl) { countEl.textContent = `배치 가능: ${unregisteredEquipments.length}개`; } } // 설비 영역 삭제 (지도에서만 제거, 설비 자체는 유지) async function removeWorkplaceEquipmentRegion(equipmentId) { if (!confirm('이 설비의 지도 위치 정보를 삭제하시겠습니까?\n(설비 자체는 삭제되지 않습니다)')) { return; } try { // API로 지도 위치 초기화 const response = await window.apiCall(`/equipments/${equipmentId}/map-position`, 'PATCH', { map_x_percent: null, map_y_percent: null, map_width_percent: null, map_height_percent: null }); if (!response || !response.success) { throw new Error(response?.message || '위치 삭제 실패'); } // 로컬 배열에서 제거 workplaceEquipmentRegions = workplaceEquipmentRegions.filter(r => r.equipment_id != equipmentId); // 설비 목록 새로고침 (현재 작업장 + 전체) await Promise.all([ loadWorkplaceEquipments(window.currentWorkplaceMapId), loadAllEquipments() ]); renderWorkplaceEquipmentList(); // 캔버스 다시 그리기 if (workplaceCanvas && workplaceImage) { workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height); drawWorkplaceRegions(); } showToast('설비 위치가 삭제되었습니다.', 'success'); } catch (error) { console.error('설비 위치 삭제 오류:', error); showToast(error.message || '설비 위치 삭제 중 오류가 발생했습니다.', 'error'); } } // 작업장 레이아웃 이미지 미리보기 function previewWorkplaceLayoutImage(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { const preview = document.getElementById('workplaceLayoutPreview'); if (preview) { preview.innerHTML = `미리보기`; } }; reader.readAsDataURL(file); } } window.switchCategory = switchCategory; window.openCategoryModal = openCategoryModal; window.closeCategoryModal = closeCategoryModal; window.saveCategory = saveCategory; window.deleteCategory = deleteCategory; window.openWorkplaceModal = openWorkplaceModal; window.closeWorkplaceModal = closeWorkplaceModal; window.editWorkplace = editWorkplace; window.saveWorkplace = saveWorkplace; window.deleteWorkplace = deleteWorkplace; window.confirmDeleteWorkplace = confirmDeleteWorkplace; window.refreshWorkplaces = refreshWorkplaces; window.showToast = showToast; window.loadCategories = loadCategories; window.updateLayoutPreview = updateLayoutPreview; window.openWorkplaceMapModal = openWorkplaceMapModal; window.closeWorkplaceMapModal = closeWorkplaceMapModal; window.uploadWorkplaceLayout = uploadWorkplaceLayout; window.clearWorkplaceCurrentRegion = clearWorkplaceCurrentRegion; window.saveWorkplaceEquipmentRegion = saveWorkplaceEquipmentRegion; window.removeWorkplaceEquipmentRegion = removeWorkplaceEquipmentRegion; window.previewWorkplaceLayoutImage = previewWorkplaceLayoutImage; window.toggleNewEquipmentFields = toggleNewEquipmentFields; // 새 설비 필드 토글 (기존 설비 선택 시 숨김) function toggleNewEquipmentFields() { const selectEl = document.getElementById('equipmentSelectInput'); const fieldsDiv = document.getElementById('newEquipmentFields'); if (selectEl && fieldsDiv) { if (selectEl.value) { fieldsDiv.style.display = 'none'; } else { fieldsDiv.style.display = 'block'; } } } // ==================== 전체화면 설비 배치 편집기 ==================== // 전체화면 편집기 관련 전역 변수 let fsCanvas = null; let fsCtx = null; let fsImage = null; let fsIsDrawing = false; let fsStartX = 0; let fsStartY = 0; let fsCurrentRect = null; let fsSidebarVisible = true; // 전체화면 편집기 열기 async function openFullscreenEquipmentEditor() { const workplaceId = window.currentWorkplaceMapId; if (!workplaceId) { showToast('작업장을 먼저 선택해주세요.', 'warning'); return; } const workplace = workplaces.find(w => w.workplace_id === workplaceId); if (!workplace) { showToast('작업장 정보를 찾을 수 없습니다.', 'error'); return; } // 레이아웃 이미지 확인 if (!workplace.layout_image) { showToast('작업장 레이아웃 이미지를 먼저 업로드해주세요.', 'warning'); return; } // 전체화면 에디터 타이틀 설정 const titleEl = document.getElementById('fullscreenEditorTitle'); if (titleEl) { titleEl.textContent = `${workplace.workplace_name} - 설비 위치 편집`; } // 전체화면 에디터 표시 const editor = document.getElementById('fullscreenEquipmentEditor'); if (editor) { editor.style.display = 'flex'; document.body.style.overflow = 'hidden'; } // 이미지 URL 생성 const fullImageUrl = workplace.layout_image.startsWith('http') ? workplace.layout_image : `${window.API_BASE_URL || 'http://localhost:20005/api'}${workplace.layout_image}`.replace('/api/', '/'); // 캔버스 초기화 initFullscreenCanvas(fullImageUrl); // 설비 목록 로드 및 드롭다운 업데이트 await Promise.all([ loadWorkplaceEquipments(workplaceId), loadAllEquipments() ]); updateFsEquipmentUI(); } // 전체화면 편집기 닫기 function closeFullscreenEditor() { const editor = document.getElementById('fullscreenEquipmentEditor'); if (editor) { editor.style.display = 'none'; document.body.style.overflow = ''; } // 전역 상태 초기화 fsCanvas = null; fsCtx = null; fsImage = null; fsIsDrawing = false; fsCurrentRect = null; // 작업장 지도 모달의 설비 목록도 업데이트 renderWorkplaceEquipmentList(); // 작업장 카드 목록 새로고침 (썸네일 업데이트) renderWorkplaces(); } // 사이드바 토글 function toggleEditorSidebar() { const sidebar = document.getElementById('fullscreenSidebar'); const canvasArea = document.getElementById('fullscreenCanvasArea'); const toggleIcon = document.getElementById('sidebarToggleIcon'); if (!sidebar || !canvasArea) return; fsSidebarVisible = !fsSidebarVisible; if (fsSidebarVisible) { sidebar.style.display = 'flex'; toggleIcon.textContent = '▶'; } else { sidebar.style.display = 'none'; toggleIcon.textContent = '◀'; } // 캔버스 크기 재조정 if (fsImage) { setTimeout(() => { resizeFullscreenCanvas(); }, 100); } } // 전체화면 캔버스 초기화 function initFullscreenCanvas(imageUrl) { const img = new Image(); img.onload = function() { fsImage = img; fsCanvas = document.getElementById('fullscreenRegionCanvas'); if (!fsCanvas) return; fsCtx = fsCanvas.getContext('2d'); // 캔버스 크기 조정 resizeFullscreenCanvas(); // 이벤트 리스너 등록 fsCanvas.onmousedown = startFsDraw; fsCanvas.onmousemove = drawFsRegion; fsCanvas.onmouseup = endFsDraw; fsCanvas.onmouseleave = endFsDraw; // 터치 이벤트 지원 fsCanvas.ontouchstart = handleFsTouchStart; fsCanvas.ontouchmove = handleFsTouchMove; fsCanvas.ontouchend = endFsDraw; console.log('✅ 전체화면 캔버스 초기화 완료'); }; img.onerror = function() { console.error('전체화면 이미지 로드 실패:', imageUrl); showToast('이미지를 불러올 수 없습니다.', 'error'); }; img.src = imageUrl; } // 캔버스 크기 조정 (화면에 맞게) function resizeFullscreenCanvas() { if (!fsImage || !fsCanvas) return; const wrapper = document.getElementById('fullscreenCanvasWrapper'); if (!wrapper) return; // 사용 가능한 영역 계산 const maxWidth = wrapper.clientWidth - 40; const maxHeight = wrapper.clientHeight - 40; // 이미지 비율 유지하면서 크기 조정 const imgRatio = fsImage.width / fsImage.height; const containerRatio = maxWidth / maxHeight; let scale; if (imgRatio > containerRatio) { // 이미지가 더 넓음 - 너비 기준 scale = maxWidth / fsImage.width; } else { // 이미지가 더 높음 - 높이 기준 scale = maxHeight / fsImage.height; } // 최소/최대 스케일 제한 scale = Math.min(Math.max(scale, 0.1), 2); fsCanvas.width = fsImage.width * scale; fsCanvas.height = fsImage.height * scale; // 이미지 그리기 fsCtx.drawImage(fsImage, 0, 0, fsCanvas.width, fsCanvas.height); // 기존 영역 그리기 drawFsRegions(); // 줌 정보 업데이트 const zoomInfo = document.getElementById('canvasZoomInfo'); if (zoomInfo) { zoomInfo.textContent = `${Math.round(scale * 100)}%`; } } // 드래그 시작 function startFsDraw(e) { fsIsDrawing = true; const rect = fsCanvas.getBoundingClientRect(); fsStartX = e.clientX - rect.left; fsStartY = e.clientY - rect.top; } // 드래그 중 function drawFsRegion(e) { if (!fsIsDrawing) return; const rect = fsCanvas.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; // 캔버스 초기화 및 이미지 다시 그리기 fsCtx.drawImage(fsImage, 0, 0, fsCanvas.width, fsCanvas.height); // 기존 영역들 표시 drawFsRegions(); // 현재 그리는 사각형 (점선) fsCtx.strokeStyle = '#3b82f6'; fsCtx.lineWidth = 3; fsCtx.setLineDash([8, 4]); fsCtx.strokeRect( fsStartX, fsStartY, currentX - fsStartX, currentY - fsStartY ); fsCtx.setLineDash([]); // 선택 영역 배경 fsCtx.fillStyle = 'rgba(59, 130, 246, 0.15)'; fsCtx.fillRect( fsStartX, fsStartY, currentX - fsStartX, currentY - fsStartY ); // 영역 정보 표시 const width = Math.abs(currentX - fsStartX); const height = Math.abs(currentY - fsStartY); fsCtx.font = 'bold 12px sans-serif'; fsCtx.fillStyle = '#3b82f6'; fsCtx.fillText(`${Math.round(width)} × ${Math.round(height)}px`, Math.min(fsStartX, currentX) + 5, Math.min(fsStartY, currentY) - 5); fsCurrentRect = { x: fsStartX, y: fsStartY, width: currentX - fsStartX, height: currentY - fsStartY }; } // 드래그 종료 function endFsDraw(e) { fsIsDrawing = false; } // 터치 이벤트 핸들러 function handleFsTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const rect = fsCanvas.getBoundingClientRect(); fsIsDrawing = true; fsStartX = touch.clientX - rect.left; fsStartY = touch.clientY - rect.top; } function handleFsTouchMove(e) { e.preventDefault(); if (!fsIsDrawing) return; const touch = e.touches[0]; const rect = fsCanvas.getBoundingClientRect(); const currentX = touch.clientX - rect.left; const currentY = touch.clientY - rect.top; // 캔버스 초기화 및 이미지 다시 그리기 fsCtx.drawImage(fsImage, 0, 0, fsCanvas.width, fsCanvas.height); drawFsRegions(); // 현재 그리는 사각형 fsCtx.strokeStyle = '#3b82f6'; fsCtx.lineWidth = 3; fsCtx.setLineDash([8, 4]); fsCtx.strokeRect( fsStartX, fsStartY, currentX - fsStartX, currentY - fsStartY ); fsCtx.setLineDash([]); fsCtx.fillStyle = 'rgba(59, 130, 246, 0.15)'; fsCtx.fillRect( fsStartX, fsStartY, currentX - fsStartX, currentY - fsStartY ); fsCurrentRect = { x: fsStartX, y: fsStartY, width: currentX - fsStartX, height: currentY - fsStartY }; } // 기존 영역들 그리기 function drawFsRegions() { workplaceEquipmentRegions.forEach((region, index) => { // 퍼센트를 픽셀로 변환 const x = (region.x_percent / 100) * fsCanvas.width; const y = (region.y_percent / 100) * fsCanvas.height; const width = (region.width_percent / 100) * fsCanvas.width; const height = (region.height_percent / 100) * fsCanvas.height; // 영역 배경 (반투명) fsCtx.fillStyle = 'rgba(16, 185, 129, 0.2)'; fsCtx.fillRect(x, y, width, height); // 영역 테두리 fsCtx.strokeStyle = '#10b981'; fsCtx.lineWidth = 2; fsCtx.strokeRect(x, y, width, height); // 영역 이름 표시 const displayName = region.equipment_code ? `[${region.equipment_code}] ${region.equipment_name}` : region.equipment_name; fsCtx.font = 'bold 13px sans-serif'; const textMetrics = fsCtx.measureText(displayName); const textPadding = 6; // 텍스트 배경 fsCtx.fillStyle = 'rgba(255, 255, 255, 0.95)'; fsCtx.fillRect(x + 4, y + 4, textMetrics.width + textPadding * 2, 22); // 텍스트 fsCtx.fillStyle = '#047857'; fsCtx.fillText(displayName, x + 4 + textPadding, y + 19); }); } // 현재 영역 지우기 function fsClearCurrentRegion() { fsCurrentRect = null; if (fsCanvas && fsImage) { fsCtx.drawImage(fsImage, 0, 0, fsCanvas.width, fsCanvas.height); drawFsRegions(); } showToast('영역이 초기화되었습니다.', 'info'); } // 설비 영역 저장 (전체화면 편집기용) async function fsSaveEquipmentRegion() { const equipmentSelect = document.getElementById('fsEquipmentSelect'); const equipmentNameInput = document.getElementById('fsEquipmentName'); const equipmentCodeInput = document.getElementById('fsEquipmentCode'); const selectedEquipmentId = equipmentSelect?.value; const newEquipmentName = equipmentNameInput?.value.trim(); const newEquipmentCode = equipmentCodeInput?.value.trim(); // 기존 설비 선택 또는 새 설비 입력 확인 if (!selectedEquipmentId && (!newEquipmentName || !newEquipmentCode)) { showToast('기존 설비를 선택하거나 새 설비 코드와 이름을 입력해주세요.', 'warning'); return; } if (!fsCurrentRect) { showToast('지도에서 영역을 드래그하여 선택해주세요.', 'warning'); return; } // 퍼센트로 변환 (음수 영역 처리) let xPercent = (Math.min(fsCurrentRect.x, fsCurrentRect.x + fsCurrentRect.width) / fsCanvas.width) * 100; let yPercent = (Math.min(fsCurrentRect.y, fsCurrentRect.y + fsCurrentRect.height) / fsCanvas.height) * 100; let widthPercent = (Math.abs(fsCurrentRect.width) / fsCanvas.width) * 100; let heightPercent = (Math.abs(fsCurrentRect.height) / fsCanvas.height) * 100; try { let equipmentId = selectedEquipmentId; let equipmentName = ''; let equipmentCode = ''; if (selectedEquipmentId) { // 기존 설비 - 위치 및 작업장 업데이트 const response = await window.apiCall(`/equipments/${selectedEquipmentId}/map-position`, 'PATCH', { workplace_id: window.currentWorkplaceMapId, map_x_percent: xPercent, map_y_percent: yPercent, map_width_percent: widthPercent, map_height_percent: heightPercent }); if (!response || !response.success) { throw new Error(response?.message || '위치 저장 실패'); } const eq = allEquipments.find(e => e.equipment_id == selectedEquipmentId); equipmentName = eq?.equipment_name || '설비'; equipmentCode = eq?.equipment_code || ''; } else { // 새 설비 생성 const response = await window.apiCall('/equipments', 'POST', { equipment_code: newEquipmentCode, equipment_name: newEquipmentName, workplace_id: window.currentWorkplaceMapId, map_x_percent: xPercent, map_y_percent: yPercent, map_width_percent: widthPercent, map_height_percent: heightPercent, status: 'active' }); if (!response || !response.success) { throw new Error(response?.message || '설비 생성 실패'); } equipmentId = response.data.equipment_id; equipmentName = newEquipmentName; equipmentCode = newEquipmentCode; } // 로컬 배열 업데이트 const newRegion = { equipment_id: equipmentId, equipment_name: equipmentName, equipment_code: equipmentCode, x_percent: xPercent, y_percent: yPercent, width_percent: widthPercent, height_percent: heightPercent }; // 기존 영역이 있으면 교체, 없으면 추가 const existingIndex = workplaceEquipmentRegions.findIndex(r => r.equipment_id == equipmentId); if (existingIndex >= 0) { workplaceEquipmentRegions[existingIndex] = newRegion; } else { workplaceEquipmentRegions.push(newRegion); } // 설비 목록 새로고침 await Promise.all([ loadWorkplaceEquipments(window.currentWorkplaceMapId), loadAllEquipments() ]); // 입력 필드 초기화 (UI 업데이트 전에 먼저) if (equipmentNameInput) equipmentNameInput.value = ''; if (equipmentCodeInput) equipmentCodeInput.value = ''; // 비워야 다음 코드 자동 로드됨 if (equipmentSelect) equipmentSelect.value = ''; // UI 업데이트 및 다음 관리번호 로드 await updateFsEquipmentUI(); fsClearCurrentRegion(); showToast(`설비 "${equipmentName}" 위치가 저장되었습니다.`, 'success'); } catch (error) { console.error('설비 위치 저장 오류:', error); showToast(error.message || '설비 위치 저장 중 오류가 발생했습니다.', 'error'); } } // 새 설비 필드 토글 (전체화면 편집기용) function fsToggleNewEquipmentFields() { const selectEl = document.getElementById('fsEquipmentSelect'); const fieldsDiv = document.getElementById('fsNewEquipmentFields'); if (selectEl && fieldsDiv) { if (selectEl.value) { fieldsDiv.style.opacity = '0.5'; fieldsDiv.querySelector('input')?.setAttribute('disabled', 'true'); document.getElementById('fsEquipmentCode')?.setAttribute('disabled', 'true'); document.getElementById('fsEquipmentName')?.setAttribute('disabled', 'true'); } else { fieldsDiv.style.opacity = '1'; document.getElementById('fsEquipmentCode')?.removeAttribute('disabled'); document.getElementById('fsEquipmentName')?.removeAttribute('disabled'); } } } // 다음 관리번호 로드 (전체화면 편집기용) async function loadNextEquipmentCodeForFs() { try { const response = await window.apiCall('/equipments/next-code', 'GET'); if (response && response.success) { const codeInput = document.getElementById('fsEquipmentCode'); if (codeInput && !codeInput.value) { codeInput.value = response.data.next_code; } } } catch (error) { console.error('다음 관리번호 조회 실패:', error); // 오류 시 무시 (사용자가 직접 입력) } } // 전체화면 편집기 UI 업데이트 async function updateFsEquipmentUI() { // 설비 선택 드롭다운 업데이트 const selectEl = document.getElementById('fsEquipmentSelect'); if (selectEl) { // 지도에 미배치된 설비만 표시 const availableEquipments = allEquipments.filter(eq => eq.map_x_percent == null || eq.map_x_percent === '' ); // 이미 이 작업장에 배치된 설비 제외 const registeredIds = workplaceEquipmentRegions.map(r => r.equipment_id); const unregisteredEquipments = availableEquipments.filter(eq => !registeredIds.includes(eq.equipment_id)); let options = ''; if (unregisteredEquipments.length === 0) { options += ''; } else { unregisteredEquipments.forEach(eq => { options += ``; }); } selectEl.innerHTML = options; // 배치 가능한 설비 수 표시 const countEl = document.getElementById('fsAvailableEquipmentCount'); if (countEl) { countEl.textContent = `${unregisteredEquipments.length}개`; } } // 새 설비 코드 자동 생성 await loadNextEquipmentCodeForFs(); // 등록된 설비 목록 업데이트 const listEl = document.getElementById('fsEquipmentList'); if (listEl) { if (workplaceEquipmentRegions.length === 0) { listEl.innerHTML = '

등록된 설비가 없습니다

'; } else { let html = ''; workplaceEquipmentRegions.forEach((region) => { html += `
${region.equipment_name} [${region.equipment_code || '-'}]
위치: (${region.x_percent.toFixed(1)}%, ${region.y_percent.toFixed(1)}%)
`; }); listEl.innerHTML = html; } // 등록된 설비 수 표시 const registeredCountEl = document.getElementById('fsRegisteredCount'); if (registeredCountEl) { registeredCountEl.textContent = `${workplaceEquipmentRegions.length}개`; } } // 작업장 지도 모달의 설비 수도 업데이트 const modalCountEl = document.getElementById('workplaceEquipmentCount'); if (modalCountEl) { modalCountEl.textContent = `${workplaceEquipmentRegions.length}개`; } } // 설비 영역 삭제 (전체화면 편집기용) async function fsRemoveEquipmentRegion(equipmentId) { if (!confirm('이 설비의 지도 위치를 삭제하시겠습니까?')) { return; } try { // API로 지도 위치 초기화 const response = await window.apiCall(`/equipments/${equipmentId}/map-position`, 'PATCH', { map_x_percent: null, map_y_percent: null, map_width_percent: null, map_height_percent: null }); if (!response || !response.success) { throw new Error(response?.message || '위치 삭제 실패'); } // 로컬 배열에서 제거 workplaceEquipmentRegions = workplaceEquipmentRegions.filter(r => r.equipment_id != equipmentId); // 설비 목록 새로고침 await Promise.all([ loadWorkplaceEquipments(window.currentWorkplaceMapId), loadAllEquipments() ]); // UI 업데이트 updateFsEquipmentUI(); // 캔버스 다시 그리기 if (fsCanvas && fsImage) { fsCtx.drawImage(fsImage, 0, 0, fsCanvas.width, fsCanvas.height); drawFsRegions(); } showToast('설비 위치가 삭제되었습니다.', 'success'); } catch (error) { console.error('설비 위치 삭제 오류:', error); showToast(error.message || '삭제 중 오류가 발생했습니다.', 'error'); } } // 전체화면 편집기 함수들 전역 노출 window.openFullscreenEquipmentEditor = openFullscreenEquipmentEditor; window.closeFullscreenEditor = closeFullscreenEditor; window.toggleEditorSidebar = toggleEditorSidebar; window.fsClearCurrentRegion = fsClearCurrentRegion; window.fsSaveEquipmentRegion = fsSaveEquipmentRegion; window.fsToggleNewEquipmentFields = fsToggleNewEquipmentFields; window.fsRemoveEquipmentRegion = fsRemoveEquipmentRegion;