Files
TK-FB-Project/web-ui/js/workplace-management.js
Hyungi Ahn e1227a69fe feat: 설비 관리 시스템 구축
## 주요 기능
- 설비 등록/수정/삭제 기능
- 작업장별 설비 연결
- 작업장 지도에서 설비 위치 정의
- 필터링 및 검색 기능

## 백엔드
- 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>
2026-01-28 09:22:57 +09:00

1165 lines
38 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
// 전역 변수
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;