// 작업장 현황 JavaScript
let selectedCategory = null;
let workplaceData = [];
let mapRegions = []; // 작업장 영역 데이터
let canvas = null;
let ctx = null;
let canvasImage = null;
// 금일 TBM 작업자 데이터
let todayWorkers = [];
// 금일 출입 신청 데이터
let todayVisitors = [];
// ==================== 초기화 ====================
document.addEventListener('DOMContentLoaded', async () => {
await loadCategories();
// 이벤트 리스너
document.getElementById('categorySelect').addEventListener('change', onCategoryChange);
document.getElementById('refreshMapBtn').addEventListener('click', refreshMapData);
// 기본값으로 제1공장 선택
await selectFirstCategory();
});
// ==================== 카테고리 (공장) 로드 ====================
async function loadCategories() {
try {
const response = await window.apiCall('/workplaces/categories', 'GET');
if (response && response.success) {
const categories = response.data || [];
const select = document.getElementById('categorySelect');
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.category_id;
option.textContent = cat.category_name;
option.dataset.layoutImage = cat.layout_image;
select.appendChild(option);
});
}
} catch (error) {
console.error('카테고리 로드 오류:', error);
}
}
/**
* 첫 번째 카테고리 자동 선택
*/
async function selectFirstCategory() {
const select = document.getElementById('categorySelect');
if (select.options.length > 1) {
// 첫 번째 옵션 선택 (인덱스 0은 "공장을 선택하세요")
select.selectedIndex = 1;
// 변경 이벤트 트리거
await onCategoryChange({ target: select });
}
}
// ==================== 공장 선택 ====================
async function onCategoryChange(e) {
const categoryId = e.target.value;
if (!categoryId) {
document.getElementById('workplaceMapContainer').style.display = 'none';
document.getElementById('mapPlaceholder').style.display = 'flex';
return;
}
const selectedOption = e.target.options[e.target.selectedIndex];
const layoutImage = selectedOption.dataset.layoutImage;
selectedCategory = {
category_id: categoryId,
category_name: selectedOption.textContent,
layout_image: layoutImage
};
// 지도 로드
await loadWorkplaceMap();
// 금일 작업 데이터 로드
await loadTodayData();
// 지도 렌더링
renderMap();
}
// ==================== 작업장 지도 로드 ====================
async function loadWorkplaceMap() {
try {
// 작업장 데이터 로드
const response = await window.apiCall(`/workplaces?category_id=${selectedCategory.category_id}`, 'GET');
if (response && response.success) {
workplaceData = response.data || [];
}
// 작업장 영역 데이터 로드 (map-regions API)
const regionsResponse = await window.apiCall(`/workplaces/categories/${selectedCategory.category_id}/map-regions`, 'GET');
if (regionsResponse && regionsResponse.success) {
mapRegions = regionsResponse.data || [];
console.log('[지도] 로드된 영역:', mapRegions);
}
// 이미지 로드
await loadMapImage();
// 지도 컨테이너 표시
document.getElementById('mapPlaceholder').style.display = 'none';
document.getElementById('workplaceMapContainer').style.display = 'block';
} catch (error) {
console.error('작업장 데이터 로드 오류:', error);
}
}
async function loadMapImage() {
return new Promise((resolve, reject) => {
const img = new Image();
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
const fullImageUrl = selectedCategory.layout_image.startsWith('http')
? selectedCategory.layout_image
: `${baseUrl}${selectedCategory.layout_image}`;
img.onload = () => {
canvasImage = img;
// 캔버스 초기화
canvas = document.getElementById('workplaceMapCanvas');
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext('2d');
// 클릭 이벤트
canvas.addEventListener('click', onMapClick);
resolve();
};
img.onerror = () => {
console.error('이미지 로드 실패:', fullImageUrl);
reject();
};
img.src = fullImageUrl;
});
}
// ==================== 금일 데이터 로드 ====================
async function loadTodayData() {
// 로컬 시간대 기준으로 오늘 날짜 구하기 (UTC가 아닌 한국 시간 기준)
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const today = `${year}-${month}-${day}`;
console.log('[대시보드] 조회 날짜 (로컬):', today);
// TBM 작업자 데이터 로드
await loadTodayWorkers(today);
// 출입 신청 데이터 로드
await loadTodayVisitors(today);
}
async function loadTodayWorkers(date) {
try {
const response = await window.apiCall(`/tbm/sessions/date/${date}`, 'GET');
if (response && response.success) {
const sessions = response.data || [];
todayWorkers = [];
// 각 세션의 작업 정보 추가
sessions.forEach(session => {
if (session.workplace_id) {
const memberCount = session.team_member_count || 0;
const leaderCount = session.leader_id ? 1 : 0;
const totalCount = memberCount + leaderCount;
todayWorkers.push({
workplace_id: session.workplace_id,
task_name: session.task_name || '작업',
work_location: session.work_location || '',
member_count: totalCount,
project_name: session.project_name || ''
});
console.log(`[TBM] 작업 추가: ${session.work_location || session.workplace_id} - ${session.task_name} (${totalCount}명)`);
}
});
console.log('로드된 작업자:', todayWorkers);
}
} catch (error) {
console.error('TBM 작업자 데이터 로드 오류:', error);
}
}
async function loadTodayVisitors(date) {
try {
// 날짜 형식 확인 (YYYY-MM-DD)
const formattedDate = date.split('T')[0];
const response = await window.apiCall(`/workplace-visits/requests`, 'GET');
if (response && response.success) {
const requests = response.data || [];
// 금일 날짜와 승인된 요청 필터링
todayVisitors = requests.filter(req => {
// UTC 변환 없이 로컬 날짜로 비교
const visitDateObj = new Date(req.visit_date);
const visitYear = visitDateObj.getFullYear();
const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0');
const visitDay = String(visitDateObj.getDate()).padStart(2, '0');
const visitDate = `${visitYear}-${visitMonth}-${visitDay}`;
return visitDate === formattedDate &&
(req.status === 'approved' || req.status === 'training_completed');
}).map(req => ({
workplace_id: req.workplace_id,
visitor_company: req.visitor_company,
visitor_count: req.visitor_count,
visit_time: req.visit_time,
purpose_name: req.purpose_name,
status: req.status
}));
console.log('로드된 방문자:', todayVisitors);
}
} catch (error) {
console.error('출입 신청 데이터 로드 오류:', error);
}
}
// ==================== 지도 렌더링 ====================
function renderMap() {
if (!canvas || !ctx || !canvasImage) return;
// 이미지 그리기
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvasImage, 0, 0);
// 모든 작업장 영역 표시
mapRegions.forEach(region => {
// 해당 작업장의 작업자/방문자 인원 계산
const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id);
const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id);
const totalWorkerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
const totalVisitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
// 영역 그리기
drawWorkplaceRegion(region, totalWorkerCount, totalVisitorCount);
});
}
function drawWorkplaceRegion(region, workerCount, visitorCount) {
// 사각형 좌표 변환
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;
const centerX = x1 + width / 2;
const centerY = y1 + height / 2;
// 색상 결정
let fillColor, strokeColor;
const hasActivity = workerCount > 0 || visitorCount > 0;
if (workerCount > 0 && visitorCount > 0) {
// 둘 다 있음 - 초록색
fillColor = 'rgba(34, 197, 94, 0.3)';
strokeColor = 'rgb(34, 197, 94)';
} else if (workerCount > 0) {
// 내부 작업자만 - 파란색
fillColor = 'rgba(59, 130, 246, 0.3)';
strokeColor = 'rgb(59, 130, 246)';
} else if (visitorCount > 0) {
// 외부 방문자만 - 보라색
fillColor = 'rgba(168, 85, 247, 0.3)';
strokeColor = 'rgb(168, 85, 247)';
} else {
// 인원 없음 - 회색 테두리만
fillColor = 'rgba(0, 0, 0, 0)'; // 투명
strokeColor = 'rgb(156, 163, 175)'; // 회색
}
// 사각형 그리기
ctx.save();
ctx.fillStyle = fillColor;
ctx.fillRect(x1, y1, width, height);
ctx.strokeStyle = strokeColor;
ctx.lineWidth = hasActivity ? 3 : 2;
ctx.strokeRect(x1, y1, width, height);
ctx.restore();
// 인원수 표시 (인원이 있을 때만)
if (hasActivity) {
ctx.save();
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 배경 원
ctx.beginPath();
ctx.arc(centerX, centerY, 20, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 2;
ctx.stroke();
// 텍스트
const totalCount = workerCount + visitorCount;
ctx.fillStyle = strokeColor;
ctx.fillText(totalCount.toString(), centerX, centerY);
ctx.restore();
} else {
// 인원이 없을 때는 작업장 이름만 표시
ctx.save();
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgb(107, 114, 128)';
ctx.fillText(region.workplace_name, centerX, centerY);
ctx.restore();
}
}
// ==================== 지도 클릭 ====================
function onMapClick(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width * canvas.width;
const y = (e.clientY - rect.top) / rect.height * canvas.height;
// 클릭한 위치의 작업장 영역 찾기
for (const region of mapRegions) {
if (isPointInRegion(x, y, region)) {
// 작업장 정보를 찾아서 모달 표시
const workplace = workplaceData.find(w => w.workplace_id === region.workplace_id);
if (workplace) {
showWorkplaceDetail({ ...workplace, ...region });
} else {
// 작업장 정보가 없으면 region 데이터만 사용
showWorkplaceDetail(region);
}
break;
}
}
}
function isPointInRegion(x, y, 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;
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
}
// ==================== 작업장 상세 정보 모달 ====================
// 현재 선택된 작업장 정보 (모달용)
let currentModalWorkplace = null;
function showWorkplaceDetail(workplace) {
currentModalWorkplace = workplace;
const workers = todayWorkers.filter(w => w.workplace_id === workplace.workplace_id);
const visitors = todayVisitors.filter(v => v.workplace_id === workplace.workplace_id);
// 모달 제목
document.getElementById('modalWorkplaceName').textContent = workplace.workplace_name;
document.getElementById('modalWorkplaceDesc').textContent = `${selectedCategory.category_name} • ${workplace.description || ''}`;
// 요약 카드 업데이트
const totalWorkers = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
const totalVisitors = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
document.getElementById('summaryWorkerCount').textContent = totalWorkers;
document.getElementById('summaryVisitorCount').textContent = totalVisitors;
document.getElementById('summaryTaskCount').textContent = workers.length;
// 배지 업데이트
document.getElementById('workerCountBadge').textContent = totalWorkers;
document.getElementById('visitorCountBadge').textContent = totalVisitors;
// 현황 개요 탭 - 현재 작업 목록
renderCurrentTasks(workers);
// 현황 개요 탭 - 설비 현황
loadEquipmentStatus(workplace.workplace_id);
// 작업자 탭
renderWorkersTab(workers);
// 방문자 탭
renderVisitorsTab(visitors);
// 상세 지도 초기화
initDetailMap(workplace);
// 첫 번째 탭 활성화
switchWorkplaceTab('overview');
// 모달 표시
document.getElementById('workplaceDetailModal').style.display = 'flex';
}
// 현재 작업 목록 렌더링
function renderCurrentTasks(workers) {
const container = document.getElementById('currentTasksList');
if (workers.length === 0) {
container.innerHTML = '
현재 진행 중인 작업이 없습니다.
';
return;
}
let html = '';
workers.forEach(worker => {
html += `
${worker.task_name}
${worker.work_location ? `📍 ${worker.work_location}` : ''}
${worker.project_name ? ` • 📁 ${worker.project_name}` : ''}
👷
${worker.member_count}명
`;
});
container.innerHTML = html;
}
// 설비 현황 로드
async function loadEquipmentStatus(workplaceId) {
const container = document.getElementById('equipmentSummary');
try {
const response = await window.apiCall(`/equipments?workplace_id=${workplaceId}`, 'GET');
if (response && response.success && response.data && response.data.length > 0) {
const equipments = response.data;
let html = '';
// 최대 4개만 표시
equipments.slice(0, 4).forEach(eq => {
const statusClass = eq.status === 'active' ? 'normal' :
eq.status === 'maintenance' ? 'warning' : 'error';
const statusText = eq.status === 'active' ? '정상' :
eq.status === 'maintenance' ? '점검중' : '수리필요';
html += `
⚙️
${eq.equipment_name}
${statusText}
`;
});
if (equipments.length > 4) {
html += `+${equipments.length - 4}개 더...
`;
}
container.innerHTML = html;
} else {
container.innerHTML = '등록된 설비가 없습니다.
';
}
} catch (error) {
console.error('설비 현황 로드 오류:', error);
container.innerHTML = '설비 정보를 불러올 수 없습니다.
';
}
}
// 작업자 탭 렌더링
function renderWorkersTab(workers) {
const container = document.getElementById('internalWorkersList');
if (workers.length === 0) {
container.innerHTML = '금일 작업 예정 인원이 없습니다.
';
return;
}
let html = '';
workers.forEach(worker => {
html += `
${worker.work_location ? `
📍 ${worker.work_location}
` : ''}
${worker.project_name ? `
📁 ${worker.project_name}
` : ''}
`;
});
container.innerHTML = html;
}
// 방문자 탭 렌더링
function renderVisitorsTab(visitors) {
const container = document.getElementById('externalVisitorsList');
if (visitors.length === 0) {
container.innerHTML = '금일 방문 예정 인원이 없습니다.
';
return;
}
let html = '';
visitors.forEach(visitor => {
const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨';
html += `
⏰ ${visitor.visit_time}
📋 ${visitor.purpose_name}
`;
});
container.innerHTML = html;
}
// 상세 지도 초기화
async function initDetailMap(workplace) {
const container = document.getElementById('detailMapContainer');
const legendContainer = document.getElementById('detailMapLegend');
// 작업장에 레이아웃 이미지가 있는지 확인
if (workplace.layout_image) {
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
const imageUrl = workplace.layout_image.startsWith('http')
? workplace.layout_image
: `${baseUrl}${workplace.layout_image}`;
// 이미지를 먼저 로드하여 비율 계산
const img = new Image();
img.onload = async () => {
// 이미지 래퍼를 생성하여 이미지와 마커가 같은 영역을 공유하도록 함
container.innerHTML = `
`;
// 설비 마커 로드
await loadEquipmentMarkers(workplace.workplace_id);
};
img.onerror = () => {
container.innerHTML = `
`;
};
img.src = imageUrl;
// 범례 표시
legendContainer.innerHTML = `
`;
} else {
container.innerHTML = `
🗺️
상세 지도가 등록되지 않았습니다.
작업장 관리에서 레이아웃 이미지를 등록해주세요.
`;
legendContainer.innerHTML = '';
}
}
// 설비 마커 로드 및 표시
async function loadEquipmentMarkers(workplaceId) {
const markersLayer = document.getElementById('equipmentMarkersLayer');
if (!markersLayer) return;
try {
const response = await window.apiCall(`/equipments?workplace_id=${workplaceId}`, 'GET');
if (response && response.success && response.data && response.data.length > 0) {
const equipments = response.data;
let markersHtml = '';
equipments.forEach(eq => {
// 위치 정보가 있는 설비만 마커 표시
if (eq.map_x_percent != null && eq.map_y_percent != null) {
const statusClass = eq.status === 'active' ? 'active' :
eq.status === 'maintenance' ? 'maintenance' :
eq.status === 'repair_needed' ? 'repair' : 'inactive';
// 마커 크기 (기본값 또는 설정된 값)
const width = eq.map_width_percent || 8;
const height = eq.map_height_percent || 6;
// 표시 이름: [코드] 이름
const displayName = `[${eq.equipment_code}] ${eq.equipment_name}`;
markersHtml += `
${displayName}
`;
}
});
if (markersHtml) {
markersLayer.innerHTML = markersHtml;
} else {
markersLayer.innerHTML = '위치가 등록된 설비가 없습니다.
';
}
} else {
markersLayer.innerHTML = '등록된 설비가 없습니다.
';
}
} catch (error) {
console.error('설비 마커 로드 오류:', error);
markersLayer.innerHTML = '설비 정보를 불러올 수 없습니다.
';
}
}
// 설비 툴팁 표시
function showEquipmentTooltip(event, equipment) {
event.stopPropagation();
// 기존 툴팁 제거
const existingTooltip = document.querySelector('.equipment-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
const statusText = equipment.status === 'active' ? '정상 가동' :
equipment.status === 'maintenance' ? '점검 중' :
equipment.status === 'repair_needed' ? '수리 필요' : '비활성';
const statusClass = equipment.status === 'active' ? 'active' :
equipment.status === 'maintenance' ? 'maintenance' :
equipment.status === 'repair_needed' ? 'repair' : 'inactive';
const tooltip = document.createElement('div');
tooltip.className = 'equipment-tooltip';
tooltip.innerHTML = `
코드: ${equipment.equipment_code}
${equipment.equipment_type ? `
유형: ${equipment.equipment_type}
` : ''}
${equipment.model_name ? `
모델: ${equipment.model_name}
` : ''}
${equipment.manufacturer ? `
제조사: ${equipment.manufacturer}
` : ''}
`;
// 툴팁 위치 설정
const container = document.getElementById('detailMapContainer');
const rect = container.getBoundingClientRect();
tooltip.style.left = `${event.clientX - rect.left + 10}px`;
tooltip.style.top = `${event.clientY - rect.top + 10}px`;
container.appendChild(tooltip);
// 외부 클릭 시 툴팁 닫기
setTimeout(() => {
document.addEventListener('click', function closeTooltip(e) {
if (!tooltip.contains(e.target)) {
tooltip.remove();
document.removeEventListener('click', closeTooltip);
}
});
}, 100);
}
// 탭 전환
function switchWorkplaceTab(tabName) {
// 모든 탭 비활성화
document.querySelectorAll('.workplace-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.workplace-tab-content').forEach(content => {
content.classList.remove('active');
});
// 선택한 탭 활성화
document.querySelector(`.workplace-tab[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`tab-${tabName}`).classList.add('active');
}
// 순회점검 페이지로 이동
function openPatrolPage() {
closeWorkplaceModal();
window.location.href = `/pages/inspection/daily-patrol.html?category=${selectedCategory.category_id}`;
}
function closeWorkplaceModal() {
document.getElementById('workplaceDetailModal').style.display = 'none';
currentModalWorkplace = null;
}
// ==================== 새로고침 ====================
async function refreshMapData() {
if (!selectedCategory) return;
await loadTodayData();
renderMap();
}
// 전역 함수로 노출
window.closeWorkplaceModal = closeWorkplaceModal;
window.switchWorkplaceTab = switchWorkplaceTab;
window.openPatrolPage = openPatrolPage;
window.showEquipmentTooltip = showEquipmentTooltip;
// ==========================================
// 설비 상세 슬라이드 패널
// ==========================================
let currentPanelEquipment = null;
let panelFactories = [];
let panelWorkplaces = [];
let panelMovePosition = null;
let panelRepairCategories = [];
let panelRepairPhotoBases = [];
const STATUS_LABELS = {
active: '정상 가동',
maintenance: '점검 중',
repair_needed: '수리 필요',
inactive: '비활성',
external: '외부 반출',
repair_external: '수리 외주'
};
// 패널 열기
async function openEquipmentPanel(equipment) {
currentPanelEquipment = equipment;
// 패널 헤더 설정
document.getElementById('panelEquipmentTitle').textContent =
`[${equipment.equipment_code}] ${equipment.equipment_name}`;
const statusEl = document.getElementById('panelEquipmentStatus');
statusEl.textContent = STATUS_LABELS[equipment.status] || equipment.status;
statusEl.className = `slide-panel-status ${equipment.status}`;
// 기본 정보 렌더링
renderPanelInfo(equipment);
// 패널 열기
document.getElementById('equipmentSlidePanel').classList.add('open');
// 데이터 로드
await Promise.all([
loadPanelPhotos(),
loadPanelRepairHistory(),
loadPanelExternalLogs(),
loadPanelFactories(),
loadPanelRepairCategories()
]);
}
// 패널 닫기
function closeEquipmentPanel() {
document.getElementById('equipmentSlidePanel').classList.remove('open');
currentPanelEquipment = null;
}
// 기본 정보 렌더링
function renderPanelInfo(eq) {
document.getElementById('panelEquipmentInfo').innerHTML = `
모델명
${eq.model_name || '-'}
규격
${eq.specifications || '-'}
제조사
${eq.manufacturer || '-'}
구입처
${eq.supplier || '-'}
설비유형
${eq.equipment_type || '-'}
구입일
${eq.installation_date ? formatPanelDate(eq.installation_date) : '-'}
`;
}
// 사진 로드
async function loadPanelPhotos() {
if (!currentPanelEquipment) return;
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/photos`, 'GET');
if (response && response.success) {
renderPanelPhotos(response.data);
}
} catch (error) {
console.error('사진 로드 실패:', error);
document.getElementById('panelPhotoGrid').innerHTML =
'사진을 불러올 수 없습니다
';
}
}
function renderPanelPhotos(photos) {
const grid = document.getElementById('panelPhotoGrid');
if (!photos || photos.length === 0) {
grid.innerHTML = '등록된 사진이 없습니다
';
return;
}
grid.innerHTML = photos.map(photo => `
`).join('');
}
// 사진 확대 보기
function viewPanelPhoto(url) {
// 간단한 이미지 뷰어
const viewer = document.createElement('div');
viewer.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);display:flex;align-items:center;justify-content:center;z-index:3000;cursor:pointer;';
viewer.innerHTML = `
`;
viewer.onclick = () => viewer.remove();
document.body.appendChild(viewer);
}
// 사진 업로드 모달
function openPanelPhotoUpload() {
document.getElementById('panelPhotoInput').value = '';
document.getElementById('panelPhotoDesc').value = '';
document.getElementById('panelPhotoPreview').innerHTML = '';
document.getElementById('panelPhotoModal').style.display = 'flex';
}
function closePanelPhotoModal() {
document.getElementById('panelPhotoModal').style.display = 'none';
}
function previewPanelPhoto(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = e => {
document.getElementById('panelPhotoPreview').innerHTML =
`
`;
};
reader.readAsDataURL(file);
}
}
async function uploadPanelPhoto() {
const fileInput = document.getElementById('panelPhotoInput');
const description = document.getElementById('panelPhotoDesc').value;
if (!fileInput.files[0]) {
alert('사진을 선택하세요.');
return;
}
const reader = new FileReader();
reader.onload = async e => {
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/photos`, 'POST', {
photo_base64: e.target.result,
description: description
});
if (response && response.success) {
closePanelPhotoModal();
loadPanelPhotos();
}
} catch (error) {
console.error('업로드 실패:', error);
alert('사진 업로드에 실패했습니다.');
}
};
reader.readAsDataURL(fileInput.files[0]);
}
async function deletePanelPhoto(photoId) {
if (!confirm('이 사진을 삭제하시겠습니까?')) return;
try {
await window.apiCall(`/equipments/photos/${photoId}`, 'DELETE');
loadPanelPhotos();
} catch (error) {
console.error('삭제 실패:', error);
alert('삭제에 실패했습니다.');
}
}
// 수리 이력 로드
async function loadPanelRepairHistory() {
if (!currentPanelEquipment) return;
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/repair-history`, 'GET');
if (response && response.success) {
renderPanelRepairHistory(response.data);
}
} catch (error) {
console.error('수리 이력 로드 실패:', error);
}
}
function renderPanelRepairHistory(history) {
const container = document.getElementById('panelRepairHistory');
if (!history || history.length === 0) {
container.innerHTML = '수리 이력이 없습니다
';
return;
}
container.innerHTML = history.slice(0, 5).map(h => {
const statusLabels = {
reported: '신고됨', received: '접수', in_progress: '처리중', completed: '완료', closed: '종료'
};
const statusLabel = statusLabels[h.status] || h.status;
const statusClass = (h.status === 'completed' || h.status === 'closed') ? 'completed' : 'pending';
return `
${formatPanelDate(h.created_at)}
${h.item_name || '수리 요청'}
${truncateText(h.description, 30)}
${statusLabel}
`;
}).join('');
}
// 외부반출 이력 로드
async function loadPanelExternalLogs() {
if (!currentPanelEquipment) return;
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/external-logs`, 'GET');
if (response && response.success) {
renderPanelExternalLogs(response.data);
}
} catch (error) {
console.error('외부반출 이력 로드 실패:', error);
}
}
function renderPanelExternalLogs(logs) {
const container = document.getElementById('panelExternalHistory');
if (!logs || logs.length === 0) {
container.innerHTML = '외부반출 이력이 없습니다
';
return;
}
container.innerHTML = logs.slice(0, 5).map(log => {
const isReturned = !!log.actual_return_date;
const statusClass = isReturned ? 'returned' : 'exported';
const statusLabel = isReturned ? '반입완료' : '반출중';
return `
${formatPanelDate(log.export_date)}
${log.destination || '외부'}
${truncateText(log.reason, 30)}
${!isReturned
? `
`
: `
${statusLabel}`
}
`;
}).join('');
}
// 공장/작업장 로드 (이동용)
async function loadPanelFactories() {
try {
const response = await window.apiCall('/workplaces/categories', 'GET');
if (response && response.success) {
panelFactories = response.data;
}
} catch (error) {
console.error('공장 목록 로드 실패:', error);
}
}
// 수리 카테고리 로드
async function loadPanelRepairCategories() {
try {
const response = await window.apiCall('/equipments/repair-categories', 'GET');
if (response && response.success) {
panelRepairCategories = response.data;
}
} catch (error) {
console.error('수리 항목 로드 실패:', error);
}
}
// ==================== 임시이동 ====================
let selectedMoveFactory = null;
let selectedMoveWorkplace = null;
function openPanelMoveModal() {
// 초기화
selectedMoveFactory = null;
selectedMoveWorkplace = null;
panelMovePosition = null;
document.getElementById('panelMoveReason').value = '';
document.getElementById('panelMoveConfirmBtn').disabled = true;
// Step 1 표시
document.getElementById('moveStep1').style.display = 'block';
document.getElementById('moveStep2').style.display = 'none';
document.getElementById('moveStep3').style.display = 'none';
// 공장 카드 렌더링
renderMoveFactoryGrid();
document.getElementById('panelMoveModal').style.display = 'flex';
}
function closePanelMoveModal() {
document.getElementById('panelMoveModal').style.display = 'none';
}
function renderMoveFactoryGrid() {
const grid = document.getElementById('moveFactoryGrid');
const icons = ['🏭', '🏢', '🏗️', '🏛️', '⚙️'];
grid.innerHTML = panelFactories.map((f, i) => `
${icons[i % icons.length]}
${f.category_name}
`).join('');
// 이벤트 리스너 추가
grid.querySelectorAll('.move-factory-card').forEach(card => {
card.onclick = () => {
const catId = card.dataset.categoryId;
const factory = panelFactories.find(f => f.category_id == catId);
if (factory) {
selectMoveFactory(factory);
}
};
});
}
let panelMapRegions = []; // 이동 모달용 지도 영역
async function selectMoveFactory(factory) {
selectedMoveFactory = {
category_id: factory.category_id,
category_name: factory.category_name,
layout_image: factory.layout_image
};
// 작업장 목록 + 지도 영역 로드
try {
const [wpResponse, regionsResponse] = await Promise.all([
window.apiCall(`/workplaces?category_id=${factory.category_id}`, 'GET'),
window.apiCall(`/workplaces/categories/${factory.category_id}/map-regions`, 'GET')
]);
if (wpResponse && wpResponse.success) {
panelWorkplaces = wpResponse.data;
}
if (regionsResponse && regionsResponse.success) {
panelMapRegions = regionsResponse.data;
console.log('[이동모달] 로드된 영역:', panelMapRegions);
} else {
console.log('[이동모달] 영역 로드 실패:', regionsResponse);
}
} catch (error) {
console.error('작업장 로드 실패:', error);
return;
}
// Step 2로 이동
document.getElementById('moveStep1').style.display = 'none';
document.getElementById('moveStep2').style.display = 'block';
document.getElementById('moveStep2Title').textContent = `${factory.category_name} - 작업장 선택`;
// 레이아웃 지도 렌더링
renderMoveLayoutMap(factory.layout_image);
}
function renderMoveLayoutMap(layoutImage) {
const container = document.getElementById('moveLayoutMapContainer');
const baseUrl = (window.API_BASE_URL || '').replace('/api', '');
console.log('[이동모달] 렌더링 - layoutImage:', layoutImage);
console.log('[이동모달] 렌더링 - panelMapRegions:', panelMapRegions);
if (!layoutImage) {
container.innerHTML = `레이아웃 지도가 없습니다.
`;
return;
}
// 지도 영역 데이터로 클릭 가능한 영역 생성 (x_start, y_start, x_end, y_end 사용)
let regionsHtml = panelMapRegions.map(region => {
const left = region.x_start;
const top = region.y_start;
const width = region.x_end - region.x_start;
const height = region.y_end - region.y_start;
return `
${region.workplace_name}
`;
}).join('');
container.innerHTML = `
${regionsHtml}
`;
// 영역 클릭 이벤트
container.querySelectorAll('.move-wp-region').forEach(region => {
region.onclick = () => {
const wpId = parseInt(region.dataset.wpId);
const wp = panelWorkplaces.find(w => w.workplace_id === wpId);
if (wp) selectMoveWorkplace(wp);
};
});
}
function selectMoveWorkplace(workplace) {
if (!workplace) return;
console.log('[이동모달] 선택된 작업장:', workplace);
console.log('[이동모달] layout_image:', workplace.layout_image);
selectedMoveWorkplace = workplace;
// 상세 지도가 없으면 위치 없이 바로 확인 가능하게
if (!workplace.layout_image) {
panelMovePosition = null; // 위치 좌표 없음
document.getElementById('moveStep2').style.display = 'none';
document.getElementById('moveStep3').style.display = 'block';
document.getElementById('moveStep3Title').textContent = `${workplace.workplace_name}`;
document.getElementById('moveDetailMapContainer').innerHTML = `
📍
${workplace.workplace_name}
이 작업장에는 상세 지도가 없습니다.
위치 좌표 없이 작업장만 기록됩니다.
`;
document.getElementById('panelMoveConfirmBtn').disabled = false;
return;
}
// Step 3으로 이동
document.getElementById('moveStep2').style.display = 'none';
document.getElementById('moveStep3').style.display = 'block';
document.getElementById('moveStep3Title').textContent = `${workplace.workplace_name} - 위치 선택`;
// 상세 지도 렌더링
renderMoveDetailMap();
}
async function renderMoveDetailMap() {
const container = document.getElementById('moveDetailMapContainer');
const baseUrl = (window.API_BASE_URL || '').replace('/api', '');
const imageUrl = `${baseUrl}${selectedMoveWorkplace.layout_image}`;
// 이동 대상 작업장의 기존 설비 로드
let existingEquipments = [];
try {
const response = await window.apiCall(`/equipments?workplace_id=${selectedMoveWorkplace.workplace_id}`, 'GET');
if (response && response.success) {
existingEquipments = response.data || [];
}
} catch (error) {
console.error('설비 로드 실패:', error);
}
// 이미지 로드 후 마커 배치
const img = new Image();
img.onload = () => {
// 기존 설비 마커 HTML 생성
const markersHtml = existingEquipments.map(eq => {
const isCurrentEquipment = eq.equipment_id === currentPanelEquipment.equipment_id;
const markerClass = isCurrentEquipment ? 'move-eq-marker current-moving' : 'move-eq-marker existing';
const width = eq.map_width_percent || 6;
const height = eq.map_height_percent || 4;
return `
${eq.equipment_code || eq.equipment_name}
`;
}).join('');
container.innerHTML = `
`;
// 지도 클릭 이벤트
document.getElementById('moveMapWrapper').addEventListener('click', onMoveDetailMapClick);
};
img.onerror = () => {
container.innerHTML = `지도를 불러올 수 없습니다.
`;
};
img.src = imageUrl;
}
function onMoveDetailMapClick(event) {
const wrapper = document.getElementById('moveMapWrapper');
const rect = wrapper.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
// 현재 설비의 크기 사용 (없으면 기본값)
const width = currentPanelEquipment.map_width_percent || 6;
const height = currentPanelEquipment.map_height_percent || 4;
panelMovePosition = { x, y, width, height };
// 타겟 마커 표시
const targetMarker = document.getElementById('moveTargetMarker');
targetMarker.style.left = x + '%';
targetMarker.style.top = y + '%';
targetMarker.style.width = width + '%';
targetMarker.style.height = height + '%';
targetMarker.style.display = 'flex';
targetMarker.innerHTML = `${currentPanelEquipment.equipment_code || currentPanelEquipment.equipment_name}`;
document.getElementById('panelMoveConfirmBtn').disabled = false;
}
function moveBackToStep1() {
document.getElementById('moveStep2').style.display = 'none';
document.getElementById('moveStep1').style.display = 'block';
selectedMoveFactory = null;
}
function moveBackToStep2() {
document.getElementById('moveStep3').style.display = 'none';
document.getElementById('moveStep2').style.display = 'block';
selectedMoveWorkplace = null;
panelMovePosition = null;
document.getElementById('panelMoveConfirmBtn').disabled = true;
}
async function confirmPanelMove() {
if (!selectedMoveWorkplace) {
alert('작업장을 선택하세요.');
return;
}
// 상세 지도가 있는데 위치를 선택 안 한 경우
if (selectedMoveWorkplace.layout_image && !panelMovePosition) {
alert('지도에서 위치를 클릭하세요.');
return;
}
const reason = document.getElementById('panelMoveReason').value;
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/move`, 'POST', {
target_workplace_id: selectedMoveWorkplace.workplace_id,
target_x_percent: panelMovePosition ? panelMovePosition.x.toFixed(2) : null,
target_y_percent: panelMovePosition ? panelMovePosition.y.toFixed(2) : null,
from_workplace_id: currentPanelEquipment.workplace_id,
from_x_percent: currentPanelEquipment.map_x_percent,
from_y_percent: currentPanelEquipment.map_y_percent,
reason: reason
});
if (response && response.success) {
closePanelMoveModal();
closeEquipmentPanel();
alert('설비가 임시 이동되었습니다.');
// 설비 마커 새로고침
if (currentModalWorkplace && currentModalWorkplace.workplace_id) {
loadEquipmentMarkers(currentModalWorkplace.workplace_id);
}
}
} catch (error) {
console.error('이동 실패:', error);
alert('이동에 실패했습니다.');
}
}
// ==================== 수리신청 ====================
function openPanelRepairModal() {
const select = document.getElementById('panelRepairItem');
select.innerHTML = '';
panelRepairCategories.forEach(item => {
select.innerHTML += ``;
});
document.getElementById('panelRepairDesc').value = '';
document.getElementById('panelRepairPhotoInput').value = '';
panelRepairPhotoBases = [];
document.getElementById('panelRepairModal').style.display = 'flex';
}
function closePanelRepairModal() {
document.getElementById('panelRepairModal').style.display = 'none';
}
async function submitPanelRepair() {
const itemId = document.getElementById('panelRepairItem').value;
const description = document.getElementById('panelRepairDesc').value;
if (!description) {
alert('수리 내용을 입력하세요.');
return;
}
// 사진 처리
const fileInput = document.getElementById('panelRepairPhotoInput');
const photos = [];
if (fileInput.files.length > 0) {
for (const file of fileInput.files) {
const base64 = await readFileAsBase64(file);
photos.push(base64);
}
}
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/repair-request`, 'POST', {
item_id: itemId || null,
description: description,
photo_base64_list: photos,
workplace_id: currentPanelEquipment.workplace_id
});
if (response && response.success) {
closePanelRepairModal();
loadPanelRepairHistory();
alert('수리 신청이 접수되었습니다.');
}
} catch (error) {
console.error('수리 신청 실패:', error);
alert('수리 신청에 실패했습니다.');
}
}
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// ==================== 외부반출 ====================
function openPanelExportModal() {
document.getElementById('panelIsRepairExport').checked = false;
document.getElementById('panelExportDate').value = new Date().toISOString().slice(0, 10);
document.getElementById('panelExpectedReturn').value = '';
document.getElementById('panelExportDest').value = '';
document.getElementById('panelExportReason').value = '';
document.getElementById('panelExportModal').style.display = 'flex';
}
function closePanelExportModal() {
document.getElementById('panelExportModal').style.display = 'none';
}
async function submitPanelExport() {
const exportDate = document.getElementById('panelExportDate').value;
if (!exportDate) {
alert('반출일을 입력하세요.');
return;
}
try {
const response = await window.apiCall(`/equipments/${currentPanelEquipment.equipment_id}/export`, 'POST', {
export_date: exportDate,
expected_return_date: document.getElementById('panelExpectedReturn').value || null,
destination: document.getElementById('panelExportDest').value,
reason: document.getElementById('panelExportReason').value,
is_repair: document.getElementById('panelIsRepairExport').checked
});
if (response && response.success) {
closePanelExportModal();
loadPanelExternalLogs();
alert('외부 반출이 등록되었습니다.');
}
} catch (error) {
console.error('반출 실패:', error);
alert('반출 등록에 실패했습니다.');
}
}
// ==================== 반입 ====================
function openPanelReturnModal(logId) {
document.getElementById('panelReturnLogId').value = logId;
document.getElementById('panelReturnDate').value = new Date().toISOString().slice(0, 10);
document.getElementById('panelReturnStatus').value = 'active';
document.getElementById('panelReturnModal').style.display = 'flex';
}
function closePanelReturnModal() {
document.getElementById('panelReturnModal').style.display = 'none';
}
async function submitPanelReturn() {
const logId = document.getElementById('panelReturnLogId').value;
const returnDate = document.getElementById('panelReturnDate').value;
if (!returnDate) {
alert('반입일을 입력하세요.');
return;
}
try {
const response = await window.apiCall(`/equipments/external-logs/${logId}/return`, 'POST', {
return_date: returnDate,
new_status: document.getElementById('panelReturnStatus').value
});
if (response && response.success) {
closePanelReturnModal();
loadPanelExternalLogs();
alert('반입 처리가 완료되었습니다.');
}
} catch (error) {
console.error('반입 실패:', error);
alert('반입 처리에 실패했습니다.');
}
}
// ==================== 유틸리티 ====================
function formatPanelDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getMonth() + 1}/${date.getDate()}`;
}
function truncateText(text, maxLen) {
if (!text) return '-';
return text.length > maxLen ? text.slice(0, maxLen) + '...' : text;
}
// 전역 함수 노출
window.openEquipmentPanel = openEquipmentPanel;
window.closeEquipmentPanel = closeEquipmentPanel;
window.openPanelPhotoUpload = openPanelPhotoUpload;
window.closePanelPhotoModal = closePanelPhotoModal;
window.previewPanelPhoto = previewPanelPhoto;
window.uploadPanelPhoto = uploadPanelPhoto;
window.deletePanelPhoto = deletePanelPhoto;
window.viewPanelPhoto = viewPanelPhoto;
window.openPanelMoveModal = openPanelMoveModal;
window.closePanelMoveModal = closePanelMoveModal;
window.selectMoveFactory = selectMoveFactory;
window.selectMoveWorkplace = selectMoveWorkplace;
window.onMoveDetailMapClick = onMoveDetailMapClick;
window.moveBackToStep1 = moveBackToStep1;
window.moveBackToStep2 = moveBackToStep2;
window.confirmPanelMove = confirmPanelMove;
window.openPanelRepairModal = openPanelRepairModal;
window.closePanelRepairModal = closePanelRepairModal;
window.submitPanelRepair = submitPanelRepair;
window.openPanelExportModal = openPanelExportModal;
window.closePanelExportModal = closePanelExportModal;
window.submitPanelExport = submitPanelExport;
window.openPanelReturnModal = openPanelReturnModal;
window.closePanelReturnModal = closePanelReturnModal;
window.submitPanelReturn = submitPanelReturn;