## 주요 기능 - 설비 등록/수정/삭제 기능 - 작업장별 설비 연결 - 작업장 지도에서 설비 위치 정의 - 필터링 및 검색 기능 ## 백엔드 - equipments 테이블 생성 (마이그레이션) - 설비 API (모델, 컨트롤러, 라우트) 구현 - workplaces 테이블에 layout_image 컬럼 추가 ## 프론트엔드 - 설비 관리 페이지 (equipments.html) - 설비 관리 JavaScript (equipment-management.js) - 작업장 지도 모달 개선 ## 버그 수정 - 카테고리/작업장 이미지 보존 로직 개선 (null 처리) - 작업장 레이아웃 이미지 업로드 경로 수정 (public/uploads → uploads) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1165 lines
38 KiB
JavaScript
1165 lines
38 KiB
JavaScript
// 작업장 관리 페이지 JavaScript
|
||
|
||
// 전역 변수
|
||
let categories = [];
|
||
let workplaces = [];
|
||
let currentCategoryId = '';
|
||
let currentEditingCategory = null;
|
||
let currentEditingWorkplace = null;
|
||
|
||
// 페이지 초기화
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('🏗️ 작업장 관리 페이지 초기화 시작');
|
||
|
||
loadAllData();
|
||
});
|
||
|
||
// 모든 데이터 로드
|
||
async function loadAllData() {
|
||
try {
|
||
await Promise.all([
|
||
loadCategories(),
|
||
loadWorkplaces()
|
||
]);
|
||
|
||
renderCategoryTabs();
|
||
renderWorkplaces();
|
||
updateStatistics();
|
||
} catch (error) {
|
||
console.error('데이터 로딩 오류:', error);
|
||
showToast('데이터를 불러오는데 실패했습니다.', 'error');
|
||
}
|
||
}
|
||
|
||
// ==================== 카테고리(공장) 관련 ====================
|
||
|
||
// 카테고리 목록 로드
|
||
async function loadCategories() {
|
||
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="tab-btn ${currentCategoryId === '' ? 'active' : ''}"
|
||
data-category=""
|
||
onclick="switchCategory('')">
|
||
<span class="tab-icon">🏗️</span>
|
||
전체 (${workplaces.length})
|
||
</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="tab-btn ${isActive ? 'active' : ''}"
|
||
data-category="${category.category_id}"
|
||
onclick="switchCategory(${category.category_id})">
|
||
<span class="tab-icon">🏭</span>
|
||
${category.category_name} (${count})
|
||
</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:20005/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:20005/api'}${workplace.layout_image}`.replace('/api/', '/');
|
||
|
||
thumbnailDiv.innerHTML = `
|
||
<div style="text-align: center; padding: 8px; background: #f0f9ff; border-radius: 6px; border: 1px solid #bae6fd;">
|
||
<div style="font-size: 12px; color: #0369a1; margin-bottom: 6px; font-weight: 500;">📍 작업장 레이아웃</div>
|
||
<img src="${fullImageUrl}" alt="작업장 레이아웃" style="max-width: 100%; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 작업장에 이미지가 없으면 카테고리 지도의 영역 표시
|
||
try {
|
||
// 해당 작업장의 지도 영역 정보 가져오기
|
||
const response = await window.apiCall(`/workplaces/map-regions/workplace/${workplace.workplace_id}`, 'GET');
|
||
|
||
if (!response || (!response.success && !response.region_id)) {
|
||
return; // 영역이 정의되지 않은 경우 아무것도 표시하지 않음
|
||
}
|
||
|
||
const region = response.success ? response.data : response;
|
||
|
||
// 카테고리 정보에서 레이아웃 이미지 가져오기
|
||
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 = `
|
||
<div style="text-align: center; padding: 8px; background: #f9fafb; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px; font-weight: 500;">📍 지도 위치</div>
|
||
<canvas id="${canvasId}" style="max-width: 100%; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"></canvas>
|
||
</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;
|
||
|
||
// 썸네일 크기 설정 (최대 너비 300px)
|
||
const maxThumbWidth = 300;
|
||
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}의 지도 영역 없음`);
|
||
}
|
||
}
|
||
|
||
// 카테고리 모달 열기
|
||
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="empty-state">
|
||
<div class="empty-icon">🏗️</div>
|
||
<h3>등록된 작업장이 없습니다.</h3>
|
||
<p>"작업장 추가" 버튼을 눌러 작업장을 등록해보세요.</p>
|
||
<button class="btn btn-primary" onclick="openWorkplaceModal()">
|
||
➕ 첫 작업장 추가하기
|
||
</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="code-card workplace-card ${isActive ? '' : 'inactive'}" onclick="editWorkplace(${workplace.workplace_id})">
|
||
<div class="code-header">
|
||
<div class="code-icon" style="background: #dbeafe;">${purposeIcon}</div>
|
||
<div class="code-info">
|
||
<h3 class="code-name">${workplace.workplace_name}</h3>
|
||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px;">
|
||
${workplace.category_id ? `<span class="code-label">🏭 ${categoryName}</span>` : ''}
|
||
${workplace.workplace_purpose ? `<span class="code-label" style="background: #f3e8ff; color: #7c3aed;">${workplace.workplace_purpose}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="code-actions">
|
||
<button class="btn-small btn-primary" onclick="event.stopPropagation(); openWorkplaceMapModal(${workplace.workplace_id})" title="지도 관리" style="font-size: 14px;">
|
||
🗺️
|
||
</button>
|
||
<button class="btn-small btn-edit" onclick="event.stopPropagation(); editWorkplace(${workplace.workplace_id})" title="수정">
|
||
✏️
|
||
</button>
|
||
<button class="btn-small btn-delete" onclick="event.stopPropagation(); confirmDeleteWorkplace(${workplace.workplace_id})" title="삭제">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
${workplace.description ? `<p class="code-description">${workplace.description}</p>` : ''}
|
||
<div id="workplace-map-${workplace.workplace_id}" style="margin: 12px 0;"></div>
|
||
<div class="code-meta">
|
||
<span class="code-date">등록: ${formatDate(workplace.created_at)}</span>
|
||
${workplace.updated_at !== workplace.created_at ? `<span class="code-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');
|
||
}
|
||
|
||
// 통계 업데이트
|
||
function updateStatistics() {
|
||
const total = workplaces.length;
|
||
const active = workplaces.filter(w => w.is_active === 1 || w.is_active === true).length;
|
||
|
||
document.getElementById('totalCount').textContent = total;
|
||
document.getElementById('activeCount').textContent = active;
|
||
}
|
||
|
||
// 날짜 포맷팅
|
||
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);
|
||
}
|
||
|
||
// 전역 함수 및 변수로 노출 (다른 모듈에서 접근 가능하도록)
|
||
// 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 = [];
|
||
|
||
// 작업장 지도 모달 열기
|
||
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 = `<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>';
|
||
}
|
||
|
||
// 설비 영역 목록 로드 (TODO: API 연동)
|
||
workplaceEquipmentRegions = [];
|
||
renderWorkplaceEquipmentList();
|
||
|
||
// 모달 표시
|
||
const modal = document.getElementById('workplaceMapModal');
|
||
if (modal) {
|
||
modal.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// 작업장 지도 모달 닫기
|
||
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 {
|
||
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 = `<img src="${fullImageUrl}" alt="작업장 레이아웃" style="max-width: 100%; border-radius: 4px;">`;
|
||
}
|
||
|
||
// 파일 입력 초기화
|
||
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');
|
||
|
||
// 캔버스 크기 설정 (최대 800px 너비)
|
||
const maxWidth = 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;
|
||
};
|
||
img.src = imageUrl;
|
||
}
|
||
|
||
// 드래그 시작
|
||
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.strokeRect(
|
||
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) => {
|
||
workplaceCtx.strokeStyle = '#10b981';
|
||
workplaceCtx.lineWidth = 2;
|
||
workplaceCtx.strokeRect(region.x, region.y, region.width, region.height);
|
||
|
||
// 영역 이름 표시
|
||
workplaceCtx.fillStyle = '#10b981';
|
||
workplaceCtx.font = '14px sans-serif';
|
||
workplaceCtx.fillText(region.equipment_name, region.x + 5, region.y + 20);
|
||
});
|
||
}
|
||
|
||
// 현재 영역 지우기
|
||
function clearWorkplaceCurrentRegion() {
|
||
workplaceCurrentRect = null;
|
||
if (workplaceCanvas && workplaceImage) {
|
||
workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height);
|
||
drawWorkplaceRegions();
|
||
}
|
||
}
|
||
|
||
// 설비 위치 저장
|
||
function saveWorkplaceEquipmentRegion() {
|
||
const equipmentName = document.getElementById('equipmentNameInput');
|
||
|
||
if (!equipmentName || !equipmentName.value.trim()) {
|
||
showToast('설비 이름을 입력해주세요.', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!workplaceCurrentRect) {
|
||
showToast('영역을 드래그하여 선택해주세요.', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 퍼센트로 변환
|
||
const xPercent = (workplaceCurrentRect.x / workplaceCanvas.width) * 100;
|
||
const yPercent = (workplaceCurrentRect.y / workplaceCanvas.height) * 100;
|
||
const widthPercent = (workplaceCurrentRect.width / workplaceCanvas.width) * 100;
|
||
const heightPercent = (workplaceCurrentRect.height / workplaceCanvas.height) * 100;
|
||
|
||
const newRegion = {
|
||
equipment_name: equipmentName.value.trim(),
|
||
x: workplaceCurrentRect.x,
|
||
y: workplaceCurrentRect.y,
|
||
width: workplaceCurrentRect.width,
|
||
height: workplaceCurrentRect.height,
|
||
x_percent: xPercent,
|
||
y_percent: yPercent,
|
||
width_percent: widthPercent,
|
||
height_percent: heightPercent
|
||
};
|
||
|
||
workplaceEquipmentRegions.push(newRegion);
|
||
|
||
// UI 업데이트
|
||
renderWorkplaceEquipmentList();
|
||
clearWorkplaceCurrentRegion();
|
||
equipmentName.value = '';
|
||
|
||
showToast(`설비 "${newRegion.equipment_name}" 위치가 저장되었습니다.`, 'success');
|
||
}
|
||
|
||
// 설비 목록 렌더링
|
||
function renderWorkplaceEquipmentList() {
|
||
const listDiv = document.getElementById('workplaceEquipmentList');
|
||
if (!listDiv) return;
|
||
|
||
if (workplaceEquipmentRegions.length === 0) {
|
||
listDiv.innerHTML = '<p style="color: #94a3b8; text-align: center;">아직 정의된 설비가 없습니다</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
workplaceEquipmentRegions.forEach((region, index) => {
|
||
html += `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; border-radius: 6px; margin-bottom: 8px;">
|
||
<div>
|
||
<strong style="color: #1e293b;">${region.equipment_name}</strong>
|
||
<span style="color: #64748b; font-size: 12px; margin-left: 8px;">
|
||
(${region.x_percent.toFixed(1)}%, ${region.y_percent.toFixed(1)}%)
|
||
</span>
|
||
</div>
|
||
<button class="btn-small btn-delete" onclick="removeWorkplaceEquipmentRegion(${index})" title="삭제">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
listDiv.innerHTML = html;
|
||
}
|
||
|
||
// 설비 영역 삭제
|
||
function removeWorkplaceEquipmentRegion(index) {
|
||
workplaceEquipmentRegions.splice(index, 1);
|
||
renderWorkplaceEquipmentList();
|
||
|
||
// 캔버스 다시 그리기
|
||
if (workplaceCanvas && workplaceImage) {
|
||
workplaceCtx.drawImage(workplaceImage, 0, 0, workplaceCanvas.width, workplaceCanvas.height);
|
||
drawWorkplaceRegions();
|
||
}
|
||
|
||
showToast('설비 위치가 삭제되었습니다.', 'success');
|
||
}
|
||
|
||
// 작업장 레이아웃 이미지 미리보기
|
||
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;
|