// 작업장 관리 페이지 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 = `
🏗️
전체
${workplaces.length}
`;
// 각 카테고리 탭 추가
categories.forEach(category => {
const count = workplaces.filter(w => w.category_id === category.category_id).length;
const isActive = currentCategoryId === category.category_id;
tabsHtml += `
🏭
${category.category_name}
${count}
`;
});
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 += `
${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 += `${cat.category_name} `;
});
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 += `[${eq.equipment_code || '-'}] ${eq.equipment_name}${workplaceInfo} `;
});
}
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 += `[${eq.equipment_code || '-'}] ${eq.equipment_name} `;
});
}
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;