Files
tk-factory-services/system1-factory/web/js/workplace-management.js
Hyungi Ahn 733bb0cb35 feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합
- tkuser 서비스 신규 추가 (API + Web)
  - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리
  - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동
  - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리
  - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산)
- 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유)
- System 2: 작업 이슈 리포트 기능 강화
- System 3: tkuser API 연동, 페이지 권한 체계 적용
- docker-compose에 tkuser-api, tkuser-web 서비스 추가
- ARCHITECTURE.md, DEPLOYMENT.md 문서 작성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 13:45:52 +09:00

2255 lines
75 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 작업장 관리 페이지 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 = `
<button class="wp-tab-btn ${currentCategoryId === '' ? 'active' : ''}"
data-category=""
onclick="switchCategory('')">
<span class="wp-tab-icon">🏗️</span>
전체
<span class="wp-tab-count">${workplaces.length}</span>
</button>
`;
// 각 카테고리 탭 추가
categories.forEach(category => {
const count = workplaces.filter(w => w.category_id === category.category_id).length;
const isActive = currentCategoryId === category.category_id;
tabsHtml += `
<button class="wp-tab-btn ${isActive ? 'active' : ''}"
data-category="${category.category_id}"
onclick="switchCategory(${category.category_id})">
<span class="wp-tab-icon">🏭</span>
${category.category_name}
<span class="wp-tab-count">${count}</span>
</button>
`;
});
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:30005/api'}${category.layout_image}`.replace('/api/', '/');
// Canvas 컨테이너 생성
previewDiv.innerHTML = `
<div style="text-align: center;">
<canvas id="previewCanvas" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); cursor: default;"></canvas>
<p style="color: #64748b; margin-top: 12px; font-size: 14px;">
클릭하여 작업장 영역을 수정하려면 "지도 설정" 버튼을 누르세요
</p>
</div>
`;
// 이미지와 영역 로드
await loadImageWithRegions(fullImageUrl, category.category_id);
} else {
previewDiv.innerHTML = `
<div style="padding: 40px;">
<span style="font-size: 48px;">🗺️</span>
<p style="color: #94a3b8; margin-top: 16px;">
이 공장의 레이아웃 이미지가 아직 등록되지 않았습니다
</p>
<p style="color: #cbd5e1; font-size: 14px; margin-top: 8px;">
"지도 설정" 버튼을 눌러 레이아웃 이미지를 업로드하고 작업장 위치를 지정하세요
</p>
</div>
`;
}
}
// 이미지와 영역을 캔버스에 로드
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:30005/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 = `
<div style="text-align: center; padding: 10px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-radius: 8px; border: 1px solid #bae6fd; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: #0369a1; font-weight: 600;">📍 작업장 지도</span>
${equipmentCount > 0 ? `<span style="font-size: 11px; background: #10b981; color: white; padding: 2px 8px; border-radius: 10px;">설비 ${equipmentCount}개</span>` : ''}
</div>
<canvas id="${canvasId}" style="max-width: 100%; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);"></canvas>
<div style="font-size: 11px; color: #64748b; margin-top: 8px;">클릭하여 지도 관리</div>
</div>
`;
// 캔버스에 이미지 및 설비 영역 그리기
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 = `
<div style="text-align: center; padding: 16px; background: #f9fafb; border-radius: 8px; border: 2px dashed #cbd5e1; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
<div style="font-size: 24px; margin-bottom: 8px;">🗺️</div>
<div style="font-size: 12px; color: #64748b;">클릭하여 지도 설정</div>
</div>
`;
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:30005/api'}${category.layout_image}`.replace('/api/', '/');
// 캔버스 생성
const canvasId = `thumbnail-canvas-${workplace.workplace_id}`;
thumbnailDiv.innerHTML = `
<div style="text-align: center; padding: 10px; background: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px; font-weight: 500;">📍 공장 지도 내 위치</div>
<canvas id="${canvasId}" style="max-width: 100%; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1);"></canvas>
<div style="font-size: 11px; color: #94a3b8; margin-top: 6px;">클릭하여 상세 지도 설정</div>
</div>
`;
// 이미지 로드
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 = `
<div class="wp-empty-state">
<div class="wp-empty-icon">🏗️</div>
<h3 class="wp-empty-title">등록된 작업장이 없습니다</h3>
<p class="wp-empty-description">"작업장 추가" 버튼을 눌러 작업장을 등록해보세요</p>
<button class="wp-btn wp-btn-primary" onclick="openWorkplaceModal()">
<span class="wp-btn-icon"></span>
첫 작업장 추가하기
</button>
</div>
`;
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 += `
<div class="wp-card ${isActive ? '' : 'inactive'}" onclick="editWorkplace(${workplace.workplace_id})">
<div class="wp-card-header">
<div class="wp-card-icon">${purposeIcon}</div>
<div class="wp-card-info">
<h3 class="wp-card-title">${workplace.workplace_name}</h3>
<div class="wp-card-tags">
${workplace.category_id ? `<span class="wp-card-tag factory">🏭 ${categoryName}</span>` : ''}
${workplace.workplace_purpose ? `<span class="wp-card-tag purpose">${workplace.workplace_purpose}</span>` : ''}
</div>
</div>
<div class="wp-card-actions">
<button class="wp-card-btn map" onclick="event.stopPropagation(); openWorkplaceMapModal(${workplace.workplace_id})" title="지도 관리">
🗺️
</button>
<button class="wp-card-btn edit" onclick="event.stopPropagation(); editWorkplace(${workplace.workplace_id})" title="수정">
✏️
</button>
<button class="wp-card-btn delete" onclick="event.stopPropagation(); confirmDeleteWorkplace(${workplace.workplace_id})" title="삭제">
🗑️
</button>
</div>
</div>
${workplace.description ? `<p class="wp-card-description">${workplace.description}</p>` : ''}
<div class="wp-card-map" id="workplace-map-${workplace.workplace_id}"></div>
<div class="wp-card-meta">
<span class="wp-card-date">등록: ${formatDate(workplace.created_at)}</span>
${workplace.updated_at !== workplace.created_at ? `<span class="wp-card-date">수정: ${formatDate(workplace.updated_at)}</span>` : ''}
</div>
</div>
`;
});
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 = '<option value="">미분류</option>';
categories.forEach(cat => {
categoryOptions += `<option value="${cat.category_id}">${cat.category_name}</option>`;
});
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 = '<span class="btn-icon">⏳</span>새로고침 중...';
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:30005/api'}${workplace.layout_image}`.replace('/api/', '/');
preview.innerHTML = `<img src="${fullImageUrl}" alt="작업장 레이아웃" style="max-width: 100%; border-radius: 4px;">`;
// 캔버스 초기화
initWorkplaceCanvas(fullImageUrl);
} else if (preview) {
preview.innerHTML = '<p style="color: #64748b; text-align: center; padding: 20px;">레이아웃 이미지를 업로드해주세요</p>';
}
// 설비 영역 목록 로드 (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:30005/api'}/workplaces/${window.currentWorkplaceMapId}/layout-image`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('sso_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:30005/api'}${result.data.image_path}`.replace('/api/', '/');
preview.innerHTML = `<img src="${fullImageUrl}" alt="작업장 레이아웃" style="max-width: 100%; border-radius: 4px;">`;
// 캔버스 초기화 (설비 영역 편집용)
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 = '<p style="color: #94a3b8; text-align: center;">아직 정의된 설비가 없습니다</p>';
} else {
let html = '';
workplaceEquipmentRegions.forEach((region, index) => {
html += `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background: white; border-radius: 8px; margin-bottom: 8px; border: 1px solid #e5e7eb;">
<div>
<strong style="color: #1e293b;">${region.equipment_name}</strong>
<span style="color: #64748b; font-size: 12px; margin-left: 8px;">[${region.equipment_code || '-'}]</span>
<div style="color: #94a3b8; font-size: 11px; margin-top: 2px;">
위치: (${region.x_percent.toFixed(1)}%, ${region.y_percent.toFixed(1)}%) | 크기: ${region.width_percent.toFixed(1)}% × ${region.height_percent.toFixed(1)}%
</div>
</div>
<button class="btn-small btn-delete" onclick="removeWorkplaceEquipmentRegion(${region.equipment_id})" title="위치 삭제">
🗑️
</button>
</div>
`;
});
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 = '<option value="">-- 기존 설비 선택 --</option>';
if (unregisteredEquipments.length === 0) {
options += '<option value="" disabled>배치 가능한 설비가 없습니다</option>';
} else {
unregisteredEquipments.forEach(eq => {
const workplaceInfo = eq.workplace_name ? ` (${eq.workplace_name})` : ' (미배정)';
options += `<option value="${eq.equipment_id}">[${eq.equipment_code || '-'}] ${eq.equipment_name}${workplaceInfo}</option>`;
});
}
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 = `<img src="${e.target.result}" alt="미리보기" style="max-width: 100%; border-radius: 4px;">`;
}
};
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:30005/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 = '<option value="">-- 기존 설비 선택 --</option>';
if (unregisteredEquipments.length === 0) {
options += '<option value="" disabled>배치 가능한 설비가 없습니다</option>';
} else {
unregisteredEquipments.forEach(eq => {
options += `<option value="${eq.equipment_id}">[${eq.equipment_code || '-'}] ${eq.equipment_name}</option>`;
});
}
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 = '<p class="empty-message">등록된 설비가 없습니다</p>';
} else {
let html = '';
workplaceEquipmentRegions.forEach((region) => {
html += `
<div class="equipment-list-item">
<div class="equipment-info">
<strong>${region.equipment_name}</strong>
<span class="equipment-code">[${region.equipment_code || '-'}]</span>
<div class="equipment-position">
위치: (${region.x_percent.toFixed(1)}%, ${region.y_percent.toFixed(1)}%)
</div>
</div>
<button class="btn-delete-sm" onclick="fsRemoveEquipmentRegion(${region.equipment_id})" title="삭제">
🗑️
</button>
</div>
`;
});
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;