// zone-detail.js - 구역 상세 페이지 JavaScript
// 전역 상태
let workplaceId = null;
let workplaceData = null;
let zoneItems = [];
let selectedZoneItem = null;
let isAddingItem = false;
let selectionStart = null;
let selectionBox = null;
// XSS 방지를 위한 HTML 이스케이프 함수
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// axios 설정 대기
function waitForAxiosConfig() {
return new Promise((resolve) => {
const check = setInterval(() => {
if (axios.defaults.baseURL) {
clearInterval(check);
resolve();
}
}, 50);
setTimeout(() => {
clearInterval(check);
resolve();
}, 5000);
});
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
await waitForAxiosConfig();
// URL에서 workplace_id 파라미터 가져오기
const urlParams = new URLSearchParams(window.location.search);
workplaceId = urlParams.get('id');
if (!workplaceId) {
alert('잘못된 접근입니다.');
goBack();
return;
}
// 현재 날짜 표시
const now = new Date();
document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short'
});
// 데이터 로드
await loadWorkplaceDetail();
});
// 작업장 상세 정보 로드
async function loadWorkplaceDetail() {
try {
const today = new Date().toISOString().slice(0, 10);
const response = await axios.get(`/patrol/workplaces/${workplaceId}/detail?date=${today}`);
if (response.data.success) {
workplaceData = response.data.data;
renderPage();
} else {
throw new Error(response.data.message || '데이터 로드 실패');
}
} catch (error) {
console.error('작업장 상세 정보 로드 실패:', error);
alert('데이터를 불러오는데 실패했습니다.');
}
}
// 페이지 렌더링
function renderPage() {
if (!workplaceData) return;
const { workplace, summary } = workplaceData;
// 헤더 정보
document.getElementById('zoneName').textContent = workplace.workplace_name;
document.getElementById('zoneCategory').textContent = workplace.category_name || '';
// 요약 카드
renderSummaryCards(summary);
// 각 탭 콘텐츠 렌더링
renderMapTab();
renderIssuesTab();
renderEquipmentTab();
renderVisitsTab();
renderTbmTab();
renderPatrolTab();
// 구역 현황 로드
loadZoneItems();
}
// ==================== 구역 지도 탭 ====================
// 지도 탭 렌더링
function renderMapTab() {
const container = document.getElementById('zoneMapContainer');
const { workplace } = workplaceData;
// 작업장 자체의 지도 이미지 사용
const mapImage = workplace.layout_image;
if (mapImage) {
// 이미지 URL 생성 (API base URL에서 /api 제거)
const staticUrl = window.API_BASE_URL ? window.API_BASE_URL.replace(/\/api$/, '') : '';
const imageUrl = mapImage.startsWith('http') ? mapImage : staticUrl + mapImage;
container.innerHTML = `
`;
} else {
container.innerHTML = `
🗺️
작업장 지도 이미지가 등록되지 않았습니다.
지도 없이도 현황을 등록할 수 있습니다.
`;
// 지도가 없어도 설비 목록은 표시
renderEquipmentList();
}
// 마우스 이벤트 핸들러 설정
setupMapEventHandlers(container);
}
// 설비 목록 렌더링 (지도가 없을 때 텍스트로 표시)
function renderEquipmentList() {
if (!workplaceData || !workplaceData.equipments || workplaceData.equipments.length === 0) return;
const container = document.getElementById('zoneMapContainer');
const { equipments } = workplaceData;
const listHtml = `
등록된 설비 (${equipments.length}개)
${equipments.map(eq => {
let statusIcon = '⚙️';
let statusText = '정상';
let statusColor = '#22c55e';
if (eq.status === 'repair_needed' || eq.status === 'repair_external') {
statusIcon = '🔧';
statusText = eq.status === 'repair_external' ? '외부수리' : '수리필요';
statusColor = '#ef4444';
} else if (eq.status === 'maintenance') {
statusIcon = '⚠️';
statusText = '점검중';
statusColor = '#f59e0b';
} else if (eq.is_temporarily_moved) {
statusIcon = '📤';
statusText = '이동됨';
statusColor = '#9ca3af';
}
return `
${statusIcon}
${escapeHtml(eq.equipment_name)}
${statusText}
`;
}).join('')}
`;
container.insertAdjacentHTML('beforeend', listHtml);
}
// 지도 이미지 로드 완료
function onMapImageLoaded() {
renderEquipmentsOnMap();
renderZoneItemsOnMap();
}
// 지도 이미지 로드 실패
function onMapImageError() {
const container = document.getElementById('zoneMapContainer');
container.innerHTML = `
⚠️
지도 이미지를 불러올 수 없습니다.
지도 없이도 현황을 등록할 수 있습니다.
`;
setupMapEventHandlers(container);
}
// 지도 이벤트 핸들러 설정
function setupMapEventHandlers(container) {
container.addEventListener('mousedown', onMapMouseDown);
container.addEventListener('mousemove', onMapMouseMove);
container.addEventListener('mouseup', onMapMouseUp);
container.addEventListener('mouseleave', onMapMouseLeave);
// 터치 이벤트 (모바일)
container.addEventListener('touchstart', onMapTouchStart, { passive: false });
container.addEventListener('touchmove', onMapTouchMove, { passive: false });
container.addEventListener('touchend', onMapTouchEnd);
}
// 설비를 지도에 렌더링
function renderEquipmentsOnMap() {
const container = document.getElementById('zoneMapContainer');
if (!workplaceData || !workplaceData.equipments) return;
const { equipments } = workplaceData;
const currentWorkplaceId = parseInt(workplaceId);
// 기존 설비 마커 제거
container.querySelectorAll('.equipment-map-marker').forEach(el => el.remove());
equipments.forEach(eq => {
// 이 작업장에 표시할 좌표 결정
let x, y, width, height;
let isHere = true; // 이 작업장에 현재 있는지
let isVisitor = false; // 다른 곳에서 임시로 온 설비인지
// 원래 이 작업장 설비인 경우
if (eq.workplace_id === currentWorkplaceId) {
if (eq.is_temporarily_moved && eq.current_workplace_id !== currentWorkplaceId) {
// 다른 곳으로 이동함 - 원래 위치에 "이동됨" 표시
x = eq.map_x_percent;
y = eq.map_y_percent;
width = eq.map_width_percent || 5;
height = eq.map_height_percent || 5;
isHere = false;
} else {
// 현재 여기 있음
x = eq.current_map_x_percent || eq.map_x_percent;
y = eq.current_map_y_percent || eq.map_y_percent;
width = eq.current_map_width_percent || eq.map_width_percent || 5;
height = eq.current_map_height_percent || eq.map_height_percent || 5;
}
} else if (eq.current_workplace_id === currentWorkplaceId) {
// 다른 곳에서 임시로 온 설비
x = eq.current_map_x_percent;
y = eq.current_map_y_percent;
width = eq.current_map_width_percent || 5;
height = eq.current_map_height_percent || 5;
isVisitor = true;
} else {
return; // 이 작업장과 관계없는 설비
}
// 좌표가 없으면 건너뜀
if (x === null || x === undefined || y === null || y === undefined) return;
// 상태에 따른 색상과 아이콘
let bgColor, borderColor, icon, statusText;
if (!isHere) {
// 다른 곳으로 이동됨
bgColor = 'rgba(156, 163, 175, 0.3)';
borderColor = '#9ca3af';
icon = '📤';
statusText = `→ ${eq.current_workplace_name || '외부'}`;
} else if (eq.status === 'repair_needed' || eq.status === 'repair_external') {
// 수리 필요/요청
bgColor = 'rgba(239, 68, 68, 0.4)';
borderColor = '#ef4444';
icon = '🔧';
statusText = eq.status === 'repair_external' ? '외부수리중' : '수리필요';
} else if (eq.status === 'maintenance') {
// 점검중
bgColor = 'rgba(245, 158, 11, 0.4)';
borderColor = '#f59e0b';
icon = '⚠️';
statusText = '점검중';
} else if (isVisitor) {
// 다른 곳에서 온 임시 설비
bgColor = 'rgba(139, 92, 246, 0.4)';
borderColor = '#8b5cf6';
icon = '📥';
statusText = '임시배치';
} else {
// 정상
bgColor = 'rgba(34, 197, 94, 0.4)';
borderColor = '#22c55e';
icon = '⚙️';
statusText = '정상';
}
// 상태별 마커 클래스 결정
let markerClass = 'equipment-marker';
if (!isHere) {
markerClass += ' inactive';
} else if (eq.status === 'repair_needed' || eq.status === 'repair_external') {
markerClass += ' repair';
} else if (eq.status === 'maintenance') {
markerClass += ' maintenance';
} else if (isVisitor) {
markerClass += ' moved';
} else {
markerClass += ' active';
}
const marker = document.createElement('div');
marker.className = markerClass;
marker.style.cssText = `
left: ${x}%;
top: ${y}%;
width: ${width}%;
height: ${height}%;
`;
// 이동/임시배치 표시 이모지
const badge = !isHere ? ' 📤' : isVisitor ? ' 🚚' : '';
// 마커 라벨 (설비 이름만 표시)
marker.innerHTML = `${escapeHtml(eq.equipment_name)}${badge} `;
marker.title = `${eq.equipment_name}\n상태: ${statusText}`;
// 호버 시 정보 표시
marker.addEventListener('mouseenter', () => {
marker.style.transform = 'scale(1.1)';
marker.style.zIndex = '50';
});
marker.addEventListener('mouseleave', () => {
marker.style.transform = 'scale(1)';
marker.style.zIndex = '5';
});
// 클릭 시 상세 정보 표시
marker.addEventListener('click', (e) => {
e.stopPropagation();
showEquipmentInfo(eq, isHere, isVisitor);
});
container.appendChild(marker);
});
}
// 설비 정보 표시
function showEquipmentInfo(eq, isHere, isVisitor) {
let statusText = '정상';
if (!isHere) {
statusText = `다른 작업장으로 이동됨 (${eq.current_workplace_name || '외부'})`;
} else if (eq.status === 'repair_needed') {
statusText = '수리 필요';
} else if (eq.status === 'repair_external') {
statusText = '외부 수리중';
} else if (eq.status === 'maintenance') {
statusText = '점검중';
} else if (isVisitor) {
statusText = '임시 배치 (원래 위치 아님)';
}
alert(`📌 ${eq.equipment_name}\n\n` +
`유형: ${eq.equipment_type || '-'}\n` +
`관리번호: ${eq.equipment_code || '-'}\n` +
`상태: ${statusText}\n` +
(eq.notes ? `비고: ${eq.notes}` : ''));
}
// 구역 현황 목록 로드
async function loadZoneItems() {
try {
const response = await axios.get(`/patrol/workplaces/${workplaceId}/zone-items`);
if (response.data.success) {
zoneItems = response.data.data || [];
renderZoneItemsList();
renderZoneItemsOnMap();
}
} catch (error) {
console.error('구역 현황 로드 실패:', error);
zoneItems = [];
renderZoneItemsList();
}
}
// 현황 목록 렌더링
function renderZoneItemsList() {
const container = document.getElementById('zoneItemsList');
const typeLabels = {
'working': '작업중',
'temp_storage': '임시적치',
'moved_equipment': '이동설비',
'unreported': '미신고품',
'general': '일반',
'other': '기타'
};
const warningLabels = {
'good': '양호',
'caution': '주의',
'needs_management': '관리필요'
};
const projectTypeLabels = {
'project': '프로젝트',
'non_project': '비프로젝트',
'unknown': '미확인'
};
if (!zoneItems.length) {
container.innerHTML = `
등록된 현황이 없습니다.
위 [현황 등록] 버튼을 눌러 지도에서 범위를 선택하세요.
`;
return;
}
container.innerHTML = `
${zoneItems.map(item => `
${escapeHtml(item.item_name)}
${item.photos && item.photos.length > 0 ? `📷${item.photos.length} ` : ''}
${typeLabels[item.item_type] || item.item_type}
${item.project_type === 'project' && item.project_name ? `• ${escapeHtml(item.project_name)} ` : ''}
${item.project_type === 'unknown' ? `• 미확인 ` : ''}
${warningLabels[item.warning_level] || '양호'}
`).join('')}
`;
}
// 지도 위에 현황 마커 렌더링
function renderZoneItemsOnMap() {
const container = document.getElementById('zoneMapContainer');
// 기존 마커 제거
container.querySelectorAll('.zone-item-marker').forEach(el => el.remove());
// 유형 라벨
const typeLabels = {
'working': '작업중',
'temp_storage': '임시적치',
'moved_equipment': '이동설비',
'unreported': '미신고품'
};
// 마커 추가
zoneItems.forEach(item => {
const marker = document.createElement('div');
const isSelected = selectedZoneItem === item.item_id;
const warningClass = item.warning_level === 'needs_management' ? 'warning-high' :
item.warning_level === 'caution' ? 'warning-mid' : '';
marker.className = `zone-item-marker ${isSelected ? 'selected' : ''} ${warningClass}`;
marker.style.cssText = `
left: ${item.x_percent}%;
top: ${item.y_percent}%;
width: ${item.width_percent || 5}%;
height: ${item.height_percent || 5}%;
--marker-color: ${item.color || '#3b82f6'};
`;
marker.dataset.itemId = item.item_id;
// 클릭 시 바로 수정 모달 열기
marker.onclick = (e) => {
e.stopPropagation();
editZoneItem(item.item_id);
};
// 주의 수준 아이콘
const warningIcon = item.warning_level === 'needs_management' ? '⚠️ ' :
item.warning_level === 'caution' ? '⚡ ' : '';
// 라벨 (이름 + 유형)
const typeName = typeLabels[item.item_type] || item.item_type;
marker.innerHTML = `
${warningIcon}${escapeHtml(item.item_name)}
${typeName}
`;
marker.title = `${item.item_name}\n유형: ${typeName}\n클릭하여 수정`;
container.appendChild(marker);
});
}
// 현황 선택
function selectZoneItem(itemId) {
selectedZoneItem = selectedZoneItem === itemId ? null : itemId;
renderZoneItemsList();
renderZoneItemsOnMap();
}
// 현황 등록 시작/토글
function startAddItem() {
const container = document.getElementById('zoneMapContainer');
const btn = document.getElementById('addItemBtn');
if (isAddingItem) {
// 이미 등록 모드면 취소
cancelAddItem();
return;
}
isAddingItem = true;
selectedZoneItem = null;
container.classList.add('adding-item');
// 버튼 텍스트 변경
btn.innerHTML = '❌ 취소';
btn.classList.add('btn-danger');
btn.classList.remove('btn-primary');
// 하단 안내 바 (오버레이 대신)
let guideBar = document.getElementById('addItemGuideBar');
if (!guideBar) {
guideBar = document.createElement('div');
guideBar.id = 'addItemGuideBar';
guideBar.className = 'add-item-guide-bar';
container.appendChild(guideBar);
}
guideBar.innerHTML = `
👆
클릭 : 위치 지정 | 드래그 : 영역 선택
`;
guideBar.style.display = 'flex';
}
// 현황 등록 취소
function cancelAddItem() {
isAddingItem = false;
const container = document.getElementById('zoneMapContainer');
container.classList.remove('adding-item');
// 안내 바 숨기기
const guideBar = document.getElementById('addItemGuideBar');
if (guideBar) guideBar.style.display = 'none';
// 선택 박스 정리
if (selectionBox) {
selectionBox.remove();
selectionBox = null;
}
selectionStart = null;
// 버튼 원복
const btn = document.getElementById('addItemBtn');
btn.innerHTML = '➕ 현황 등록';
btn.classList.remove('btn-danger');
btn.classList.add('btn-primary');
}
// 마우스 이벤트 핸들러
function onMapMouseDown(e) {
if (!isAddingItem) return;
if (e.target.closest('.zone-item-marker, .equipment-marker')) return; // 기존 마커 클릭 무시
e.preventDefault();
const container = document.getElementById('zoneMapContainer');
const rect = container.getBoundingClientRect();
selectionStart = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
xPercent: ((e.clientX - rect.left) / rect.width) * 100,
yPercent: ((e.clientY - rect.top) / rect.height) * 100,
time: Date.now()
};
// 선택 박스 생성
selectionBox = document.createElement('div');
selectionBox.className = 'selection-box';
selectionBox.style.left = selectionStart.x + 'px';
selectionBox.style.top = selectionStart.y + 'px';
selectionBox.style.width = '0px';
selectionBox.style.height = '0px';
container.appendChild(selectionBox);
// 크기 표시 라벨
const sizeLabel = document.createElement('div');
sizeLabel.className = 'selection-size-label';
sizeLabel.id = 'selectionSizeLabel';
selectionBox.appendChild(sizeLabel);
}
function onMapMouseMove(e) {
if (!isAddingItem || !selectionStart || !selectionBox) return;
const container = document.getElementById('zoneMapContainer');
const rect = container.getBoundingClientRect();
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
const left = Math.min(selectionStart.x, currentX);
const top = Math.min(selectionStart.y, currentY);
const width = Math.abs(currentX - selectionStart.x);
const height = Math.abs(currentY - selectionStart.y);
selectionBox.style.left = left + 'px';
selectionBox.style.top = top + 'px';
selectionBox.style.width = width + 'px';
selectionBox.style.height = height + 'px';
// 크기 표시 업데이트
const widthPercent = ((width / rect.width) * 100).toFixed(1);
const heightPercent = ((height / rect.height) * 100).toFixed(1);
const sizeLabel = document.getElementById('selectionSizeLabel');
if (sizeLabel && width > 30) {
sizeLabel.textContent = `${widthPercent}% × ${heightPercent}%`;
sizeLabel.style.display = 'block';
}
}
function onMapMouseUp(e) {
if (!isAddingItem || !selectionStart) return;
const container = document.getElementById('zoneMapContainer');
const rect = container.getBoundingClientRect();
const endX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const endY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
const width = Math.abs(endX - selectionStart.x);
const height = Math.abs(endY - selectionStart.y);
const elapsed = Date.now() - selectionStart.time;
// 선택 박스 제거
if (selectionBox) {
selectionBox.remove();
selectionBox = null;
}
// 빠른 클릭 (200ms 이하) 또는 작은 영역 (15px 이하) → 고정 크기 마커
if (elapsed < 200 || (width < 15 && height < 15)) {
const xPercent = Math.max(0, selectionStart.xPercent - 2.5); // 중앙 정렬
const yPercent = Math.max(0, selectionStart.yPercent - 2.5);
openZoneItemModal(xPercent, yPercent, 5, 5);
} else {
// 드래그로 선택한 영역
const xPercent = Math.min(selectionStart.xPercent, (endX / rect.width) * 100);
const yPercent = Math.min(selectionStart.yPercent, (endY / rect.height) * 100);
const widthPercent = (width / rect.width) * 100;
const heightPercent = (height / rect.height) * 100;
openZoneItemModal(xPercent, yPercent, widthPercent, heightPercent);
}
selectionStart = null;
// 등록 모드 유지 (연속 등록 가능)
}
function onMapMouseLeave() {
// 지도 영역 벗어나면 현재 선택만 취소 (모드는 유지)
if (selectionBox && selectionStart) {
selectionBox.remove();
selectionBox = null;
selectionStart = null;
}
}
// 터치 이벤트 (모바일)
function onMapTouchStart(e) {
if (!isAddingItem) return;
if (e.target.closest('.zone-item-marker, .equipment-marker')) return;
e.preventDefault();
const touch = e.touches[0];
onMapMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: e.target });
}
function onMapTouchMove(e) {
if (!isAddingItem || !selectionStart) return;
e.preventDefault();
const touch = e.touches[0];
onMapMouseMove({ clientX: touch.clientX, clientY: touch.clientY });
}
function onMapTouchEnd(e) {
if (!isAddingItem || !selectionStart) return;
const touch = e.changedTouches[0];
onMapMouseUp({ clientX: touch.clientX, clientY: touch.clientY });
}
// 프로젝트 목록 캐시
let projectsCache = null;
let selectedPhotos = [];
// 프로젝트 목록 로드
async function loadProjects() {
if (projectsCache) return projectsCache;
try {
const response = await axios.get('/projects?status=in_progress&limit=100');
if (response.data.success) {
projectsCache = response.data.data || [];
return projectsCache;
}
} catch (err) {
console.error('프로젝트 로드 실패:', err);
}
return [];
}
// 프로젝트 타입 변경
function onProjectTypeChange(value) {
const projectGroup = document.getElementById('projectSelectGroup');
if (value === 'project') {
projectGroup.style.display = 'block';
populateProjectSelect();
} else {
projectGroup.style.display = 'none';
}
}
// 프로젝트 셀렉트 채우기
async function populateProjectSelect() {
const select = document.getElementById('zoneItemProject');
const projects = await loadProjects();
select.innerHTML = '프로젝트를 선택하세요 ';
projects.forEach(p => {
select.innerHTML += `${escapeHtml(p.project_name)} `;
});
}
// 커스텀 유형 추가
function addCustomType() {
const customType = prompt('새로운 상태/유형을 입력하세요:');
if (!customType || !customType.trim()) return;
const select = document.getElementById('zoneItemType');
const value = customType.trim().toLowerCase().replace(/\s+/g, '_');
// 중복 체크
if (Array.from(select.options).some(opt => opt.value === value)) {
alert('이미 존재하는 유형입니다.');
return;
}
const option = document.createElement('option');
option.value = value;
option.textContent = customType.trim();
select.appendChild(option);
select.value = value;
}
// 사진 선택
function onPhotoSelected(event) {
const files = Array.from(event.target.files);
files.forEach(file => {
if (!file.type.startsWith('image/')) return;
if (selectedPhotos.length >= 5) {
alert('사진은 최대 5장까지 등록할 수 있습니다.');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
selectedPhotos.push({
file: file,
dataUrl: e.target.result
});
renderPhotoPreview();
};
reader.readAsDataURL(file);
});
event.target.value = ''; // 리셋
}
// 사진 미리보기 렌더링
function renderPhotoPreview() {
const container = document.getElementById('photoPreviewList');
container.innerHTML = selectedPhotos.map((photo, idx) => `
×
`).join('');
}
// 사진 제거
function removePhoto(index) {
selectedPhotos.splice(index, 1);
renderPhotoPreview();
}
// 현황 모달 열기
function openZoneItemModal(x, y, width, height, item = null) {
const modal = document.getElementById('zoneItemModal');
const title = document.getElementById('zoneItemModalTitle');
const deleteBtn = document.getElementById('deleteZoneItemBtn');
// 사진 초기화
selectedPhotos = [];
renderPhotoPreview();
if (item) {
// 수정 모드
title.textContent = '현황 수정';
deleteBtn.style.display = 'inline-block';
document.getElementById('zoneItemId').value = item.item_id;
document.getElementById('zoneItemX').value = item.x_percent;
document.getElementById('zoneItemY').value = item.y_percent;
document.getElementById('zoneItemWidth').value = item.width_percent || 5;
document.getElementById('zoneItemHeight').value = item.height_percent || 5;
document.getElementById('zoneItemName').value = item.item_name || '';
document.getElementById('zoneItemType').value = item.item_type || 'working';
document.getElementById('zoneItemWarning').value = item.warning_level || 'good';
document.getElementById('zoneItemDesc').value = item.description || '';
document.getElementById('zoneItemColor').value = item.color || '#3b82f6';
// 프로젝트 타입
const projectType = item.project_type || 'non_project';
document.querySelector(`input[name="zoneItemProjectType"][value="${projectType}"]`).checked = true;
onProjectTypeChange(projectType);
if (projectType === 'project' && item.project_id) {
setTimeout(() => {
document.getElementById('zoneItemProject').value = item.project_id;
}, 100);
}
// 기존 사진 로드 (있으면)
if (item.photos && item.photos.length > 0) {
item.photos.forEach(photo => {
selectedPhotos.push({
existing: true,
photo_id: photo.photo_id,
dataUrl: photo.photo_url
});
});
renderPhotoPreview();
}
} else {
// 등록 모드
title.textContent = '현황 등록';
deleteBtn.style.display = 'none';
document.getElementById('zoneItemId').value = '';
document.getElementById('zoneItemX').value = x;
document.getElementById('zoneItemY').value = y;
document.getElementById('zoneItemWidth').value = width;
document.getElementById('zoneItemHeight').value = height;
document.getElementById('zoneItemForm').reset();
document.getElementById('zoneItemColor').value = '#3b82f6';
// 기본값 설정
document.querySelector('input[name="zoneItemProjectType"][value="non_project"]').checked = true;
onProjectTypeChange('non_project');
}
modal.style.display = 'flex';
}
// 현황 수정 모달 열기
function editZoneItem(itemId) {
const item = zoneItems.find(i => i.item_id === itemId);
if (!item) return;
openZoneItemModal(item.x_percent, item.y_percent, item.width_percent, item.height_percent, item);
}
// 모달 닫기
function closeZoneItemModal() {
document.getElementById('zoneItemModal').style.display = 'none';
}
// 색상 프리셋 선택
function setItemColor(color) {
document.getElementById('zoneItemColor').value = color;
}
// 현황 저장
async function saveZoneItem() {
const itemId = document.getElementById('zoneItemId').value;
const projectType = document.querySelector('input[name="zoneItemProjectType"]:checked')?.value || 'non_project';
const projectId = projectType === 'project' ? document.getElementById('zoneItemProject').value : null;
const itemData = {
item_name: document.getElementById('zoneItemName').value.trim(),
item_type: document.getElementById('zoneItemType').value,
warning_level: document.getElementById('zoneItemWarning').value,
project_type: projectType,
project_id: projectId || null,
description: document.getElementById('zoneItemDesc').value.trim(),
color: document.getElementById('zoneItemColor').value,
x_percent: parseFloat(document.getElementById('zoneItemX').value),
y_percent: parseFloat(document.getElementById('zoneItemY').value),
width_percent: parseFloat(document.getElementById('zoneItemWidth').value),
height_percent: parseFloat(document.getElementById('zoneItemHeight').value)
};
if (!itemData.item_name) {
alert('명칭을 입력해주세요.');
return;
}
if (projectType === 'project' && !projectId) {
alert('프로젝트를 선택해주세요.');
return;
}
try {
let response;
if (itemId) {
// 수정
response = await axios.put(`/patrol/zone-items/${itemId}`, itemData);
} else {
// 등록
response = await axios.post(`/patrol/workplaces/${workplaceId}/zone-items`, itemData);
}
if (response.data.success) {
const savedItemId = itemId || response.data.data?.item_id;
// 사진 업로드 (새로 추가된 사진만)
const newPhotos = selectedPhotos.filter(p => !p.existing && p.file);
if (newPhotos.length > 0 && savedItemId) {
await uploadZoneItemPhotos(savedItemId, newPhotos);
}
closeZoneItemModal();
await loadZoneItems();
alert(itemId ? '현황이 수정되었습니다.' : '현황이 등록되었습니다.');
} else {
throw new Error(response.data.message || '저장 실패');
}
} catch (error) {
console.error('현황 저장 실패:', error);
alert('저장에 실패했습니다: ' + (error.response?.data?.message || error.message));
}
}
// 사진 업로드
async function uploadZoneItemPhotos(itemId, photos) {
for (const photo of photos) {
try {
const formData = new FormData();
formData.append('photo', photo.file);
formData.append('item_id', itemId);
await axios.post('/patrol/zone-items/photos', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
} catch (err) {
console.error('사진 업로드 실패:', err);
}
}
}
// 현황 삭제
async function deleteZoneItem() {
const itemId = document.getElementById('zoneItemId').value;
if (!itemId) return;
if (!confirm('이 현황을 삭제하시겠습니까?')) return;
try {
const response = await axios.delete(`/patrol/zone-items/${itemId}`);
if (response.data.success) {
closeZoneItemModal();
selectedZoneItem = null;
await loadZoneItems();
alert('현황이 삭제되었습니다.');
} else {
throw new Error(response.data.message || '삭제 실패');
}
} catch (error) {
console.error('현황 삭제 실패:', error);
alert('삭제에 실패했습니다: ' + (error.response?.data?.message || error.message));
}
}
// 요약 카드 렌더링
function renderSummaryCards(summary) {
const container = document.getElementById('summaryCards');
container.innerHTML = `
⚙️
${summary.equipmentCount}
등록 설비
🔧
${summary.pendingRepairs}
수리 요청
🚨
${summary.openIssues}
미해결 신고
🚶
${summary.todayVisitors}
금일 방문자
📋
${summary.todayTbmSessions}
금일 TBM
`;
}
// 안전신고/부적합 탭 렌더링
function renderIssuesTab() {
const container = document.getElementById('issuesContent');
const { workIssues } = workplaceData;
if (!workIssues.all.length) {
container.innerHTML = `
`;
return;
}
let html = '';
// 안전 신고
if (workIssues.safety.length > 0) {
html += `
🛡️ 안전 신고
${workIssues.safety.length}
${workIssues.safety.map(issue => renderIssueCard(issue, 'safety')).join('')}
`;
}
// 부적합 사항
if (workIssues.nonconformity.length > 0) {
html += `
⚠️ 부적합 사항
${workIssues.nonconformity.length}
${workIssues.nonconformity.map(issue => renderIssueCard(issue, 'nonconformity')).join('')}
`;
}
container.innerHTML = html;
}
// 신고 카드 렌더링
function renderIssueCard(issue, type) {
const statusLabels = {
'pending': '대기',
'received': '접수',
'in_progress': '처리중',
'completed': '완료',
'closed': '종료'
};
const severityLabels = {
'low': '경미',
'medium': '보통',
'high': '중요',
'critical': '긴급'
};
return `
📁 ${escapeHtml(issue.category_name || '미분류')}
${issue.severity ? `${severityLabels[issue.severity]} ` : ''}
📅 ${formatDateTime(issue.created_at)}
👤 ${escapeHtml(issue.reporter_name || '익명')}
${issue.description ? `
${escapeHtml(issue.description)}
` : ''}
`;
}
// 설비/수리 탭 렌더링
function renderEquipmentTab() {
const container = document.getElementById('equipmentContent');
const { equipments, repairRequests } = workplaceData;
let html = '';
// 수리 요청
if (repairRequests.length > 0) {
html += `
🔧 수리 요청
${repairRequests.length}
${repairRequests.map(req => renderRepairCard(req)).join('')}
`;
}
// 설비 현황
if (equipments.length > 0) {
html += `
⚙️ 설비 현황
${equipments.length}
${equipments.map(eq => renderEquipmentCard(eq)).join('')}
`;
}
if (!html) {
html = `
`;
}
container.innerHTML = html;
}
// 수리 요청 카드 렌더링
function renderRepairCard(req) {
const priorityLabels = {
'emergency': '긴급',
'high': '높음',
'normal': '보통',
'low': '낮음'
};
return `
📋 ${escapeHtml(req.repair_category)}
${req.description ? `
${escapeHtml(req.description)}
` : ''}
📅 ${formatDate(req.request_date)}
`;
}
// 설비 카드 렌더링
function renderEquipmentCard(eq) {
const statusLabels = {
'active': '정상',
'inactive': '비활성',
'repair_needed': '수리필요',
'under_repair': '수리중',
'disposed': '폐기'
};
return `
⚙️
${escapeHtml(eq.equipment_name)}
${escapeHtml(eq.equipment_type || '-')}
${statusLabels[eq.status] || eq.status}
`;
}
// 출입현황 탭 렌더링
function renderVisitsTab() {
const container = document.getElementById('visitsContent');
const { visitRecords } = workplaceData;
if (!visitRecords.length) {
container.innerHTML = `
`;
return;
}
container.innerHTML = `
🚶 금일 방문자
${visitRecords.length}
${visitRecords.map(visit => renderVisitCard(visit)).join('')}
`;
}
// 방문자 카드 렌더링
function renderVisitCard(visit) {
return `
${escapeHtml(visit.purpose_name || visit.visit_purpose || '')}
🕐 ${escapeHtml(visit.visit_time_from || '')} ~ ${escapeHtml(visit.visit_time_to || '')}
${visit.companion_count > 0 ? `👥 동행 ${visit.companion_count}명 ` : ''}
${visit.vehicle_number ? `🚗 ${escapeHtml(visit.vehicle_number)} ` : ''}
`;
}
// TBM 탭 렌더링
function renderTbmTab() {
const container = document.getElementById('tbmContent');
const { tbmSessions } = workplaceData;
if (!tbmSessions.length) {
container.innerHTML = `
`;
return;
}
container.innerHTML = `
📋 금일 TBM
${tbmSessions.length}
${tbmSessions.map(tbm => renderTbmCard(tbm)).join('')}
`;
}
// TBM 카드 렌더링
function renderTbmCard(tbm) {
const statusLabels = {
'draft': '작성중',
'in_progress': '진행중',
'completed': '완료'
};
return `
📍 ${escapeHtml(tbm.work_location || '-')}
👷 ${escapeHtml(tbm.leader_name || tbm.leader_worker_name || '-')}
👥 ${tbm.team_size || (tbm.team ? tbm.team.length : 0)}명
${tbm.work_content ? `
작업내용
${escapeHtml(tbm.work_content)}
` : ''}
${tbm.safety_measures ? `
안전조치
${escapeHtml(tbm.safety_measures)}
` : ''}
${tbm.team && tbm.team.length > 0 ? `
참석자 (${tbm.team.length}명)
${tbm.team.map(m => `
${escapeHtml(m.worker_name)}
`).join('')}
` : ''}
`;
}
// 순회점검 탭 렌더링
function renderPatrolTab() {
const container = document.getElementById('patrolContent');
const { recentPatrol } = workplaceData;
if (!recentPatrol || !recentPatrol.length) {
container.innerHTML = `
`;
return;
}
container.innerHTML = `
🔍 최근 순회점검
${recentPatrol.length}
${recentPatrol.map(patrol => renderPatrolCard(patrol)).join('')}
`;
}
// 순회점검 카드 렌더링
function renderPatrolCard(patrol) {
const timeLabels = { 'morning': '오전', 'afternoon': '오후' };
return `
👤 ${escapeHtml(patrol.inspector_name || '-')}
📊 ${patrol.status === 'completed' ? '완료' : '진행중'}
${patrol.checked_count || 0}
점검 항목
${patrol.issue_count || 0}
주의/불량
${patrol.notes ? `
📝 ${escapeHtml(patrol.notes)}
` : ''}
`;
}
// 탭 전환
function switchTab(tabName) {
// 탭 버튼 활성화
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// 탭 콘텐츠 표시
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `tab-${tabName}`);
});
}
// 뒤로 가기
function goBack() {
// 이전 페이지로 이동, 없으면 일일순회점검 페이지로
if (document.referrer && document.referrer.includes(window.location.host)) {
window.history.back();
} else {
window.location.href = '/pages/inspection/daily-patrol.html';
}
}
// 유틸리티 함수
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', weekday: 'short' });
}
function formatDateTime(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}