- 설비 마커 클릭 시 슬라이드 패널로 상세 정보 표시 - 설비 사진 업로드/삭제 기능 - 설비 임시 이동 기능 (3단계 지도 기반 선택) - Step 1: 공장 선택 - Step 2: 레이아웃 지도에서 작업장 선택 - Step 3: 상세 지도에서 위치 선택 - 설비 외부 반출/반입 기능 - 설비 수리 신청 기능 (기존 신고 시스템 연동) - DB 마이그레이션 추가 (사진, 임시이동, 외부반출 테이블) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1569 lines
52 KiB
JavaScript
1569 lines
52 KiB
JavaScript
// 작업장 현황 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 = '<p class="empty-message">현재 진행 중인 작업이 없습니다.</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
workers.forEach(worker => {
|
||
html += `
|
||
<div class="current-task-item">
|
||
<div class="task-info">
|
||
<p class="task-name">${worker.task_name}</p>
|
||
<p class="task-detail">
|
||
${worker.work_location ? `📍 ${worker.work_location}` : ''}
|
||
${worker.project_name ? ` • 📁 ${worker.project_name}` : ''}
|
||
</p>
|
||
</div>
|
||
<div class="task-badge">
|
||
<span>👷</span>
|
||
<span>${worker.member_count}명</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
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 += `
|
||
<div class="equipment-item">
|
||
<span class="equipment-icon">⚙️</span>
|
||
<div class="equipment-info">
|
||
<p class="equipment-name">${eq.equipment_name}</p>
|
||
<p class="equipment-status ${statusClass}">${statusText}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
if (equipments.length > 4) {
|
||
html += `<div class="equipment-item" style="justify-content: center; color: #6b7280; font-size: 0.85rem;">+${equipments.length - 4}개 더...</div>`;
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
} else {
|
||
container.innerHTML = '<p class="empty-message">등록된 설비가 없습니다.</p>';
|
||
}
|
||
} catch (error) {
|
||
console.error('설비 현황 로드 오류:', error);
|
||
container.innerHTML = '<p class="empty-message">설비 정보를 불러올 수 없습니다.</p>';
|
||
}
|
||
}
|
||
|
||
// 작업자 탭 렌더링
|
||
function renderWorkersTab(workers) {
|
||
const container = document.getElementById('internalWorkersList');
|
||
|
||
if (workers.length === 0) {
|
||
container.innerHTML = '<p class="empty-message">금일 작업 예정 인원이 없습니다.</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
workers.forEach(worker => {
|
||
html += `
|
||
<div class="worker-item">
|
||
<div class="worker-item-header">
|
||
<p class="worker-item-title">${worker.task_name}</p>
|
||
<span class="worker-item-badge">${worker.member_count}명</span>
|
||
</div>
|
||
${worker.work_location ? `<p class="worker-item-detail">📍 ${worker.work_location}</p>` : ''}
|
||
${worker.project_name ? `<p class="worker-item-detail">📁 ${worker.project_name}</p>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 방문자 탭 렌더링
|
||
function renderVisitorsTab(visitors) {
|
||
const container = document.getElementById('externalVisitorsList');
|
||
|
||
if (visitors.length === 0) {
|
||
container.innerHTML = '<p class="empty-message">금일 방문 예정 인원이 없습니다.</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
visitors.forEach(visitor => {
|
||
const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨';
|
||
|
||
html += `
|
||
<div class="visitor-item">
|
||
<div class="visitor-item-header">
|
||
<p class="visitor-item-title">${visitor.visitor_company}</p>
|
||
<span class="visitor-item-badge">${visitor.visitor_count}명 • ${statusText}</span>
|
||
</div>
|
||
<p class="visitor-item-detail">⏰ ${visitor.visit_time}</p>
|
||
<p class="visitor-item-detail">📋 ${visitor.purpose_name}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
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 = `
|
||
<div id="mapImageWrapper" class="map-image-wrapper">
|
||
<img src="${imageUrl}" alt="${workplace.workplace_name} 상세 지도" />
|
||
<div id="equipmentMarkersLayer" class="equipment-markers-layer"></div>
|
||
</div>
|
||
<div id="mapErrorPlaceholder" class="detail-map-placeholder" style="display: none;">
|
||
<span class="placeholder-icon">🗺️</span>
|
||
<p>지도 이미지를 불러올 수 없습니다.</p>
|
||
</div>
|
||
`;
|
||
|
||
// 설비 마커 로드
|
||
await loadEquipmentMarkers(workplace.workplace_id);
|
||
};
|
||
|
||
img.onerror = () => {
|
||
container.innerHTML = `
|
||
<div class="detail-map-placeholder">
|
||
<span class="placeholder-icon">🗺️</span>
|
||
<p>지도 이미지를 불러올 수 없습니다.</p>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
img.src = imageUrl;
|
||
|
||
// 범례 표시
|
||
legendContainer.innerHTML = `
|
||
<div class="map-legend-item">
|
||
<div class="legend-color" style="background: rgba(34, 197, 94, 0.5); border-color: #22c55e;"></div>
|
||
<span>정상 가동</span>
|
||
</div>
|
||
<div class="map-legend-item">
|
||
<div class="legend-color" style="background: rgba(245, 158, 11, 0.5); border-color: #f59e0b;"></div>
|
||
<span>점검 중</span>
|
||
</div>
|
||
<div class="map-legend-item">
|
||
<div class="legend-color" style="background: rgba(239, 68, 68, 0.5); border-color: #ef4444;"></div>
|
||
<span>수리 필요</span>
|
||
</div>
|
||
<div class="map-legend-item">
|
||
<div class="legend-color" style="background: rgba(156, 163, 175, 0.5); border-color: #9ca3af;"></div>
|
||
<span>비활성</span>
|
||
</div>
|
||
`;
|
||
} else {
|
||
container.innerHTML = `
|
||
<div class="detail-map-placeholder">
|
||
<span class="placeholder-icon">🗺️</span>
|
||
<p>상세 지도가 등록되지 않았습니다.</p>
|
||
<p style="font-size: 0.8rem; margin-top: 8px;">작업장 관리에서 레이아웃 이미지를 등록해주세요.</p>
|
||
</div>
|
||
`;
|
||
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 += `
|
||
<div class="equipment-marker ${statusClass}"
|
||
style="left: ${eq.map_x_percent}%; top: ${eq.map_y_percent}%;
|
||
width: ${width}%; height: ${height}%;"
|
||
title="${displayName}"
|
||
onclick="openEquipmentPanel(${JSON.stringify(eq).replace(/"/g, '"')})">
|
||
<span class="marker-label">${displayName}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
|
||
if (markersHtml) {
|
||
markersLayer.innerHTML = markersHtml;
|
||
} else {
|
||
markersLayer.innerHTML = '<div class="no-markers-message">위치가 등록된 설비가 없습니다.</div>';
|
||
}
|
||
} else {
|
||
markersLayer.innerHTML = '<div class="no-markers-message">등록된 설비가 없습니다.</div>';
|
||
}
|
||
} catch (error) {
|
||
console.error('설비 마커 로드 오류:', error);
|
||
markersLayer.innerHTML = '<div class="no-markers-message">설비 정보를 불러올 수 없습니다.</div>';
|
||
}
|
||
}
|
||
|
||
// 설비 툴팁 표시
|
||
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 = `
|
||
<div class="tooltip-header">
|
||
<strong>${equipment.equipment_name}</strong>
|
||
<span class="tooltip-status ${statusClass}">${statusText}</span>
|
||
</div>
|
||
<div class="tooltip-body">
|
||
<p><span class="label">코드:</span> ${equipment.equipment_code}</p>
|
||
${equipment.equipment_type ? `<p><span class="label">유형:</span> ${equipment.equipment_type}</p>` : ''}
|
||
${equipment.model_name ? `<p><span class="label">모델:</span> ${equipment.model_name}</p>` : ''}
|
||
${equipment.manufacturer ? `<p><span class="label">제조사:</span> ${equipment.manufacturer}</p>` : ''}
|
||
</div>
|
||
<div class="tooltip-actions">
|
||
<button class="tooltip-detail-btn" onclick="this.closest('.equipment-tooltip').remove(); openEquipmentPanel(${JSON.stringify(equipment).replace(/"/g, '"')})">상세보기</button>
|
||
</div>
|
||
<button class="tooltip-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
// 툴팁 위치 설정
|
||
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 = `
|
||
<div class="panel-info-item">
|
||
<span class="label">모델명</span>
|
||
<span class="value">${eq.model_name || '-'}</span>
|
||
</div>
|
||
<div class="panel-info-item">
|
||
<span class="label">규격</span>
|
||
<span class="value">${eq.specifications || '-'}</span>
|
||
</div>
|
||
<div class="panel-info-item">
|
||
<span class="label">제조사</span>
|
||
<span class="value">${eq.manufacturer || '-'}</span>
|
||
</div>
|
||
<div class="panel-info-item">
|
||
<span class="label">구입처</span>
|
||
<span class="value">${eq.supplier || '-'}</span>
|
||
</div>
|
||
<div class="panel-info-item">
|
||
<span class="label">설비유형</span>
|
||
<span class="value">${eq.equipment_type || '-'}</span>
|
||
</div>
|
||
<div class="panel-info-item">
|
||
<span class="label">구입일</span>
|
||
<span class="value">${eq.installation_date ? formatPanelDate(eq.installation_date) : '-'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 사진 로드
|
||
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 =
|
||
'<div class="panel-empty">사진을 불러올 수 없습니다</div>';
|
||
}
|
||
}
|
||
|
||
function renderPanelPhotos(photos) {
|
||
const grid = document.getElementById('panelPhotoGrid');
|
||
|
||
if (!photos || photos.length === 0) {
|
||
grid.innerHTML = '<div class="panel-empty">등록된 사진이 없습니다</div>';
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = photos.map(photo => `
|
||
<div class="panel-photo-item" onclick="viewPanelPhoto('${window.API_BASE_URL}${photo.photo_path}')">
|
||
<img src="${window.API_BASE_URL}${photo.photo_path}" alt="">
|
||
<button class="delete-btn" onclick="event.stopPropagation(); deletePanelPhoto(${photo.photo_id})">×</button>
|
||
</div>
|
||
`).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 = `<img src="${url}" style="max-width:90%;max-height:90%;object-fit:contain;">`;
|
||
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 =
|
||
`<img src="${e.target.result}">`;
|
||
};
|
||
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 = '<div class="panel-empty">수리 이력이 없습니다</div>';
|
||
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 `
|
||
<div class="panel-history-item">
|
||
<span class="panel-history-date">${formatPanelDate(h.created_at)}</span>
|
||
<div class="panel-history-content">
|
||
<div class="panel-history-title">${h.item_name || '수리 요청'}</div>
|
||
<div class="panel-history-detail">${truncateText(h.description, 30)}</div>
|
||
</div>
|
||
<span class="panel-history-badge ${statusClass}">${statusLabel}</span>
|
||
</div>
|
||
`;
|
||
}).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 = '<div class="panel-empty">외부반출 이력이 없습니다</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = logs.slice(0, 5).map(log => {
|
||
const isReturned = !!log.actual_return_date;
|
||
const statusClass = isReturned ? 'returned' : 'exported';
|
||
const statusLabel = isReturned ? '반입완료' : '반출중';
|
||
|
||
return `
|
||
<div class="panel-history-item">
|
||
<span class="panel-history-date">${formatPanelDate(log.export_date)}</span>
|
||
<div class="panel-history-content">
|
||
<div class="panel-history-title">${log.destination || '외부'}</div>
|
||
<div class="panel-history-detail">${truncateText(log.reason, 30)}</div>
|
||
</div>
|
||
${!isReturned
|
||
? `<button class="panel-history-action" onclick="openPanelReturnModal(${log.log_id})">반입</button>`
|
||
: `<span class="panel-history-badge ${statusClass}">${statusLabel}</span>`
|
||
}
|
||
</div>
|
||
`;
|
||
}).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) => `
|
||
<div class="move-factory-card" data-category-id="${f.category_id}">
|
||
<div class="factory-icon">${icons[i % icons.length]}</div>
|
||
<div class="factory-name">${f.category_name}</div>
|
||
</div>
|
||
`).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 = `<div style="padding: 20px; text-align: center; color: #666;">레이아웃 지도가 없습니다.</div>`;
|
||
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 `
|
||
<div class="layout-region move-wp-region" data-wp-id="${region.workplace_id}"
|
||
style="left: ${left}%; top: ${top}%; width: ${width}%; height: ${height}%;">
|
||
<span class="region-label">${region.workplace_name}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `
|
||
<img src="${baseUrl}${layoutImage}" alt="공장 레이아웃">
|
||
${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 = `
|
||
<div style="padding: 40px; text-align: center; color: #666;">
|
||
<div style="font-size: 3rem; margin-bottom: 12px;">📍</div>
|
||
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 8px;">${workplace.workplace_name}</div>
|
||
<div>이 작업장에는 상세 지도가 없습니다.</div>
|
||
<div style="margin-top: 8px; color: #999; font-size: 0.9rem;">위치 좌표 없이 작업장만 기록됩니다.</div>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<div class="${markerClass}"
|
||
style="left: ${eq.map_x_percent}%; top: ${eq.map_y_percent}%;
|
||
width: ${width}%; height: ${height}%;"
|
||
title="${eq.equipment_name}">
|
||
<span class="eq-label">${eq.equipment_code || eq.equipment_name}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `
|
||
<div class="move-map-wrapper" id="moveMapWrapper">
|
||
<img src="${imageUrl}" id="moveDetailMapImg">
|
||
<div class="move-markers-layer">
|
||
${markersHtml}
|
||
<div id="moveTargetMarker" class="move-eq-marker target" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 지도 클릭 이벤트
|
||
document.getElementById('moveMapWrapper').addEventListener('click', onMoveDetailMapClick);
|
||
};
|
||
|
||
img.onerror = () => {
|
||
container.innerHTML = `<div style="padding: 40px; text-align: center; color: #666;">지도를 불러올 수 없습니다.</div>`;
|
||
};
|
||
|
||
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 = `<span class="eq-label">${currentPanelEquipment.equipment_code || currentPanelEquipment.equipment_name}</span>`;
|
||
|
||
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 = '<option value="">선택하세요</option>';
|
||
panelRepairCategories.forEach(item => {
|
||
select.innerHTML += `<option value="${item.item_id}">${item.item_name}</option>`;
|
||
});
|
||
|
||
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;
|