feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,18 @@ let selectedWorkplace = null;
|
||||
let itemTypes = []; // 물품 유형
|
||||
let workplaceItems = []; // 현재 작업장 물품
|
||||
let isItemEditMode = false;
|
||||
let workplaceDetail = null; // 작업장 상세 정보
|
||||
|
||||
// XSS 방지를 위한 HTML 이스케이프 함수
|
||||
function escapeHtml(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 이미지 URL 헬퍼 함수 (정적 파일용 - /api 경로 제외)
|
||||
function getImageUrl(path) {
|
||||
@@ -62,6 +74,77 @@ async function initializePage() {
|
||||
loadItemTypes(),
|
||||
loadTodayStatus()
|
||||
]);
|
||||
|
||||
// 저장된 세션 상태 복원 (구역 상세에서 돌아온 경우)
|
||||
await restoreSessionState();
|
||||
}
|
||||
|
||||
// 세션 상태 저장 (페이지 이동 전)
|
||||
function saveSessionState() {
|
||||
if (currentSession) {
|
||||
const state = {
|
||||
session: currentSession,
|
||||
categoryId: currentSession.category_id,
|
||||
checkRecords: checkRecords,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
sessionStorage.setItem('patrolSessionState', JSON.stringify(state));
|
||||
}
|
||||
}
|
||||
|
||||
// 세션 상태 복원
|
||||
async function restoreSessionState() {
|
||||
const savedState = sessionStorage.getItem('patrolSessionState');
|
||||
if (!savedState) return;
|
||||
|
||||
try {
|
||||
const state = JSON.parse(savedState);
|
||||
// 5분 이내의 상태만 복원
|
||||
if (Date.now() - state.timestamp > 5 * 60 * 1000) {
|
||||
sessionStorage.removeItem('patrolSessionState');
|
||||
return;
|
||||
}
|
||||
|
||||
// categories가 비어있으면 복원 불가
|
||||
if (!categories || categories.length === 0) {
|
||||
console.log('카테고리 목록이 없어 세션 복원 불가');
|
||||
sessionStorage.removeItem('patrolSessionState');
|
||||
return;
|
||||
}
|
||||
|
||||
// 해당 카테고리가 존재하는지 확인
|
||||
const category = categories.find(c => c.category_id == state.categoryId);
|
||||
if (!category) {
|
||||
console.log('저장된 카테고리를 찾을 수 없음:', state.categoryId);
|
||||
sessionStorage.removeItem('patrolSessionState');
|
||||
return;
|
||||
}
|
||||
|
||||
// 세션 복원
|
||||
currentSession = state.session;
|
||||
checkRecords = state.checkRecords || {};
|
||||
|
||||
// 작업장 목록 로드
|
||||
await loadWorkplaces(state.categoryId);
|
||||
|
||||
// 체크리스트 항목 로드
|
||||
await loadChecklistItems(state.categoryId);
|
||||
|
||||
// UI 표시
|
||||
document.getElementById('startPatrolBtn').style.display = 'none';
|
||||
document.getElementById('factorySelectionArea').style.display = 'none';
|
||||
document.getElementById('patrolArea').style.display = 'block';
|
||||
renderSessionInfo();
|
||||
renderWorkplaceMap();
|
||||
|
||||
console.log('세션 상태 복원 완료:', state.categoryId);
|
||||
|
||||
// 복원 후 저장 상태 삭제
|
||||
sessionStorage.removeItem('patrolSessionState');
|
||||
} catch (error) {
|
||||
console.error('세션 상태 복원 실패:', error);
|
||||
sessionStorage.removeItem('patrolSessionState');
|
||||
}
|
||||
}
|
||||
|
||||
// 공장(대분류) 목록 로드
|
||||
@@ -206,10 +289,36 @@ function renderTodayStatus(statusList) {
|
||||
// 작업장 목록 로드
|
||||
async function loadWorkplaces(categoryId) {
|
||||
try {
|
||||
// 작업장 목록 로드
|
||||
const response = await axios.get(`/workplaces?category_id=${categoryId}`);
|
||||
if (response.data.success) {
|
||||
workplaces = response.data.data;
|
||||
}
|
||||
|
||||
// 지도 영역(좌표) 로드
|
||||
try {
|
||||
const regionsResponse = await axios.get(`/workplaces/categories/${categoryId}/map-regions`);
|
||||
if (regionsResponse.data.success && regionsResponse.data.data) {
|
||||
// 작업장에 좌표 정보 병합
|
||||
const regions = regionsResponse.data.data;
|
||||
workplaces = workplaces.map(wp => {
|
||||
const region = regions.find(r => r.workplace_id === wp.workplace_id);
|
||||
if (region) {
|
||||
// x_start, y_start를 x_percent, y_percent로 매핑
|
||||
return {
|
||||
...wp,
|
||||
x_percent: region.x_start,
|
||||
y_percent: region.y_start,
|
||||
x_end: region.x_end,
|
||||
y_end: region.y_end
|
||||
};
|
||||
}
|
||||
return wp;
|
||||
});
|
||||
}
|
||||
} catch (regError) {
|
||||
console.log('지도 영역 로드 스킵:', regError.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업장 목록 로드 실패:', error);
|
||||
}
|
||||
@@ -271,30 +380,59 @@ function renderWorkplaceMap() {
|
||||
mapContainer.style.display = 'block';
|
||||
|
||||
// 좌표가 있는 작업장만 마커 추가
|
||||
const hasMarkers = workplaces.some(wp => wp.x_percent && wp.y_percent);
|
||||
const hasMarkers = workplaces.some(wp => wp.x_percent !== undefined && wp.y_percent !== undefined);
|
||||
|
||||
workplaces.forEach(wp => {
|
||||
if (wp.x_percent && wp.y_percent) {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'workplace-marker';
|
||||
marker.style.left = `${parseFloat(wp.x_percent) || 0}%`;
|
||||
marker.style.top = `${parseFloat(wp.y_percent) || 0}%`;
|
||||
marker.textContent = wp.workplace_name; // textContent는 자동 이스케이프
|
||||
marker.dataset.workplaceId = wp.workplace_id;
|
||||
marker.onclick = () => selectWorkplace(wp.workplace_id);
|
||||
|
||||
// 점검 상태에 따른 스타일
|
||||
const records = checkRecords[wp.workplace_id];
|
||||
if (records && records.some(r => r.is_checked)) {
|
||||
marker.classList.add(records.every(r => r.is_checked) ? 'completed' : 'in-progress');
|
||||
// 마커 위치 정보를 먼저 계산
|
||||
const markerData = workplaces
|
||||
.filter(wp => wp.x_percent !== undefined && wp.y_percent !== undefined)
|
||||
.map(wp => {
|
||||
let centerX = parseFloat(wp.x_percent) || 0;
|
||||
let centerY = parseFloat(wp.y_percent) || 0;
|
||||
if (wp.x_end && wp.y_end) {
|
||||
centerX = (parseFloat(wp.x_percent) + parseFloat(wp.x_end)) / 2;
|
||||
centerY = (parseFloat(wp.y_percent) + parseFloat(wp.y_end)) / 2;
|
||||
}
|
||||
return { wp, centerX, centerY };
|
||||
});
|
||||
|
||||
mapContainer.appendChild(marker);
|
||||
// y좌표 기준 정렬 (아래에 있을수록 나중에 추가 = 위에 표시)
|
||||
markerData.sort((a, b) => a.centerY - b.centerY);
|
||||
|
||||
markerData.forEach((data, index) => {
|
||||
const { wp, centerX, centerY } = data;
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'workplace-marker';
|
||||
|
||||
// 밀집도 체크 - 근처에 다른 마커가 있으면 compact 클래스 추가
|
||||
const nearbyMarkers = markerData.filter(other =>
|
||||
other !== data &&
|
||||
Math.abs(other.centerX - centerX) < 12 &&
|
||||
Math.abs(other.centerY - centerY) < 12
|
||||
);
|
||||
if (nearbyMarkers.length > 0) {
|
||||
marker.classList.add('compact');
|
||||
}
|
||||
|
||||
marker.style.left = `${centerX}%`;
|
||||
marker.style.top = `${centerY}%`;
|
||||
// y좌표가 클수록 (아래쪽일수록) z-index가 높아서 위에 표시
|
||||
marker.style.zIndex = Math.floor(centerY) + 10;
|
||||
marker.textContent = wp.workplace_name;
|
||||
marker.dataset.workplaceId = wp.workplace_id;
|
||||
marker.onclick = () => goToZoneDetail(wp.workplace_id);
|
||||
|
||||
// 점검 상태에 따른 스타일
|
||||
const records = checkRecords[wp.workplace_id];
|
||||
if (records && records.some(r => r.is_checked)) {
|
||||
marker.classList.add(records.every(r => r.is_checked) ? 'completed' : 'in-progress');
|
||||
}
|
||||
|
||||
mapContainer.appendChild(marker);
|
||||
});
|
||||
|
||||
// 좌표가 없는 작업장이 있으면 카드 목록도 표시
|
||||
if (!hasMarkers || workplaces.some(wp => !wp.x_percent || !wp.y_percent)) {
|
||||
const hasWorkplacesWithoutCoords = workplaces.some(wp => wp.x_percent === undefined || wp.y_percent === undefined);
|
||||
if (!hasMarkers || hasWorkplacesWithoutCoords) {
|
||||
listContainer.style.display = 'grid';
|
||||
renderWorkplaceCards(listContainer);
|
||||
} else {
|
||||
@@ -308,6 +446,13 @@ function renderWorkplaceMap() {
|
||||
}
|
||||
}
|
||||
|
||||
// 구역 상세 페이지로 이동
|
||||
function goToZoneDetail(workplaceId) {
|
||||
// 현재 세션 상태 저장
|
||||
saveSessionState();
|
||||
window.location.href = `/pages/inspection/zone-detail.html?id=${workplaceId}`;
|
||||
}
|
||||
|
||||
// 작업장 카드 렌더링
|
||||
function renderWorkplaceCards(container) {
|
||||
container.innerHTML = workplaces.map(wp => {
|
||||
@@ -319,7 +464,7 @@ function renderWorkplaceCards(container) {
|
||||
return `
|
||||
<div class="workplace-card ${isCompleted ? 'completed' : ''} ${isInProgress && !isCompleted ? 'in-progress' : ''} ${selectedWorkplace?.workplace_id === wp.workplace_id ? 'selected' : ''}"
|
||||
data-workplace-id="${workplaceId}"
|
||||
onclick="selectWorkplace(${workplaceId})">
|
||||
onclick="goToZoneDetail(${workplaceId})">
|
||||
<div class="workplace-card-name">${escapeHtml(wp.workplace_name)}</div>
|
||||
<div class="workplace-card-status">
|
||||
${isCompleted ? '점검완료' : (isInProgress ? '점검중' : '미점검')}
|
||||
@@ -360,6 +505,9 @@ async function selectWorkplace(workplaceId) {
|
||||
// 물품 현황 로드 및 표시
|
||||
await loadWorkplaceItems(workplaceId);
|
||||
|
||||
// 작업장 상세 정보 로드 (신고, TBM, 출입 등)
|
||||
await loadWorkplaceDetail(workplaceId);
|
||||
|
||||
// 액션 버튼 표시
|
||||
document.getElementById('checklistActions').style.display = 'flex';
|
||||
}
|
||||
@@ -763,5 +911,339 @@ function formatDate(dateStr) {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeItemModal();
|
||||
closeWorkplaceDetailPanel();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 작업장 상세 정보 패널 ====================
|
||||
|
||||
// 작업장 상세 정보 로드
|
||||
async function loadWorkplaceDetail(workplaceId) {
|
||||
try {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const response = await axios.get(`/patrol/workplaces/${workplaceId}/detail?date=${today}`);
|
||||
if (response.data.success) {
|
||||
workplaceDetail = response.data.data;
|
||||
renderWorkplaceDetailPanel();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업장 상세 정보 로드 실패:', error);
|
||||
// 에러 발생 시에도 기본 패널 표시
|
||||
workplaceDetail = null;
|
||||
renderWorkplaceDetailPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// 상세 정보 패널 렌더링
|
||||
function renderWorkplaceDetailPanel() {
|
||||
let panel = document.getElementById('workplaceDetailPanel');
|
||||
if (!panel) {
|
||||
panel = document.createElement('div');
|
||||
panel.id = 'workplaceDetailPanel';
|
||||
panel.className = 'workplace-detail-panel';
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
if (!workplaceDetail || !selectedWorkplace) {
|
||||
panel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const { workplace, equipments, repairRequests, workIssues, visitRecords, tbmSessions, recentPatrol, summary } = workplaceDetail;
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="detail-panel-header">
|
||||
<div class="detail-panel-title">
|
||||
<h3>${escapeHtml(workplace.workplace_name)}</h3>
|
||||
<span class="detail-panel-subtitle">${escapeHtml(workplace.category_name || '')}</span>
|
||||
</div>
|
||||
<button class="detail-panel-close" onclick="closeWorkplaceDetailPanel()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel-summary">
|
||||
<div class="summary-item">
|
||||
<span class="summary-value">${summary.equipmentCount}</span>
|
||||
<span class="summary-label">설비</span>
|
||||
</div>
|
||||
<div class="summary-item ${summary.pendingRepairs > 0 ? 'warning' : ''}">
|
||||
<span class="summary-value">${summary.pendingRepairs}</span>
|
||||
<span class="summary-label">수리요청</span>
|
||||
</div>
|
||||
<div class="summary-item ${summary.openIssues > 0 ? 'danger' : ''}">
|
||||
<span class="summary-value">${summary.openIssues}</span>
|
||||
<span class="summary-label">미해결 신고</span>
|
||||
</div>
|
||||
<div class="summary-item info">
|
||||
<span class="summary-value">${summary.todayVisitors}</span>
|
||||
<span class="summary-label">금일 방문자</span>
|
||||
</div>
|
||||
<div class="summary-item info">
|
||||
<span class="summary-value">${summary.todayTbmSessions}</span>
|
||||
<span class="summary-label">금일 TBM</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel-tabs">
|
||||
<button class="detail-tab active" data-tab="issues" onclick="switchDetailTab('issues')">
|
||||
🚨 신고/부적합 <span class="tab-badge ${workIssues.all.length > 0 ? 'show' : ''}">${workIssues.all.length}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="equipment" onclick="switchDetailTab('equipment')">
|
||||
⚙️ 설비 <span class="tab-badge ${summary.needsAttention > 0 ? 'show warning' : ''}">${summary.needsAttention}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="visits" onclick="switchDetailTab('visits')">
|
||||
🚶 출입 <span class="tab-badge ${visitRecords.length > 0 ? 'show' : ''}">${visitRecords.length}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="tbm" onclick="switchDetailTab('tbm')">
|
||||
📋 TBM <span class="tab-badge ${tbmSessions.length > 0 ? 'show' : ''}">${tbmSessions.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel-content">
|
||||
<!-- 신고/부적합 탭 -->
|
||||
<div class="detail-tab-content active" id="tab-issues">
|
||||
${renderIssuesTab(workIssues)}
|
||||
</div>
|
||||
|
||||
<!-- 설비 탭 -->
|
||||
<div class="detail-tab-content" id="tab-equipment">
|
||||
${renderEquipmentTab(equipments, repairRequests)}
|
||||
</div>
|
||||
|
||||
<!-- 출입 탭 -->
|
||||
<div class="detail-tab-content" id="tab-visits">
|
||||
${renderVisitsTab(visitRecords)}
|
||||
</div>
|
||||
|
||||
<!-- TBM 탭 -->
|
||||
<div class="detail-tab-content" id="tab-tbm">
|
||||
${renderTbmTab(tbmSessions)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
panel.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 신고/부적합 탭 렌더링
|
||||
function renderIssuesTab(workIssues) {
|
||||
if (!workIssues.all.length) {
|
||||
return `<div class="detail-empty">최근 30일간 신고 내역이 없습니다.</div>`;
|
||||
}
|
||||
|
||||
const safetyIssues = workIssues.safety;
|
||||
const nonconformityIssues = workIssues.nonconformity;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (safetyIssues.length > 0) {
|
||||
html += `
|
||||
<div class="issue-section">
|
||||
<h4 class="issue-section-title">🛡️ 안전 신고 (${safetyIssues.length})</h4>
|
||||
${safetyIssues.map(issue => renderIssueItem(issue)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (nonconformityIssues.length > 0) {
|
||||
html += `
|
||||
<div class="issue-section">
|
||||
<h4 class="issue-section-title">⚠️ 부적합 사항 (${nonconformityIssues.length})</h4>
|
||||
${nonconformityIssues.map(issue => renderIssueItem(issue)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html || `<div class="detail-empty">신고 내역이 없습니다.</div>`;
|
||||
}
|
||||
|
||||
// 신고 항목 렌더링
|
||||
function renderIssueItem(issue) {
|
||||
const statusColors = {
|
||||
'pending': 'pending',
|
||||
'received': 'info',
|
||||
'in_progress': 'warning',
|
||||
'completed': 'success',
|
||||
'closed': 'muted'
|
||||
};
|
||||
const statusLabels = {
|
||||
'pending': '대기',
|
||||
'received': '접수',
|
||||
'in_progress': '처리중',
|
||||
'completed': '완료',
|
||||
'closed': '종료'
|
||||
};
|
||||
const severityLabels = {
|
||||
'low': '경미',
|
||||
'medium': '보통',
|
||||
'high': '중요',
|
||||
'critical': '긴급'
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="issue-item ${statusColors[issue.status] || ''}">
|
||||
<div class="issue-item-header">
|
||||
<span class="issue-title">${escapeHtml(issue.title)}</span>
|
||||
<span class="issue-status ${statusColors[issue.status] || ''}">${statusLabels[issue.status] || issue.status}</span>
|
||||
</div>
|
||||
<div class="issue-item-meta">
|
||||
<span class="issue-category">${escapeHtml(issue.category_name || '')}</span>
|
||||
<span class="issue-severity ${issue.severity}">${severityLabels[issue.severity] || ''}</span>
|
||||
<span class="issue-date">${formatDateTime(issue.created_at)}</span>
|
||||
</div>
|
||||
${issue.description ? `<div class="issue-desc">${escapeHtml(issue.description).slice(0, 100)}${issue.description.length > 100 ? '...' : ''}</div>` : ''}
|
||||
<div class="issue-reporter">신고자: ${escapeHtml(issue.reporter_name || '익명')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 설비 탭 렌더링
|
||||
function renderEquipmentTab(equipments, repairRequests) {
|
||||
let html = '';
|
||||
|
||||
// 수리 요청 먼저 표시
|
||||
if (repairRequests.length > 0) {
|
||||
html += `
|
||||
<div class="equipment-section">
|
||||
<h4 class="equipment-section-title">🔧 수리 요청 (${repairRequests.length})</h4>
|
||||
${repairRequests.map(req => `
|
||||
<div class="repair-item ${req.priority}">
|
||||
<div class="repair-item-header">
|
||||
<span class="repair-equipment">${escapeHtml(req.equipment_name)} (${escapeHtml(req.equipment_code)})</span>
|
||||
<span class="repair-priority ${req.priority}">${getPriorityLabel(req.priority)}</span>
|
||||
</div>
|
||||
<div class="repair-category">${escapeHtml(req.repair_category)}</div>
|
||||
<div class="repair-desc">${escapeHtml(req.description || '')}</div>
|
||||
<div class="repair-date">${formatDate(req.request_date)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 설비 목록
|
||||
if (equipments.length > 0) {
|
||||
html += `
|
||||
<div class="equipment-section">
|
||||
<h4 class="equipment-section-title">📦 설비 현황 (${equipments.length})</h4>
|
||||
<div class="equipment-list">
|
||||
${equipments.map(eq => `
|
||||
<div class="equipment-item ${eq.needs_attention ? 'attention' : ''}">
|
||||
<span class="equipment-name">${escapeHtml(eq.equipment_name)}</span>
|
||||
<span class="equipment-code">${escapeHtml(eq.equipment_code)}</span>
|
||||
<span class="equipment-status ${eq.status}">${getEquipmentStatusLabel(eq.status)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html += `<div class="detail-empty">등록된 설비가 없습니다.</div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 출입 탭 렌더링
|
||||
function renderVisitsTab(visitRecords) {
|
||||
if (!visitRecords.length) {
|
||||
return `<div class="detail-empty">금일 승인된 방문자가 없습니다.</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="visits-list">
|
||||
${visitRecords.map(visit => `
|
||||
<div class="visit-item">
|
||||
<div class="visit-item-header">
|
||||
<span class="visit-name">${escapeHtml(visit.visitor_name)}</span>
|
||||
<span class="visit-company">${escapeHtml(visit.visitor_company || '')}</span>
|
||||
</div>
|
||||
<div class="visit-purpose">${escapeHtml(visit.purpose_name || visit.visit_purpose || '')}</div>
|
||||
<div class="visit-time">
|
||||
🕐 ${escapeHtml(visit.visit_time_from || '')} ~ ${escapeHtml(visit.visit_time_to || '')}
|
||||
</div>
|
||||
${visit.companion_count > 0 ? `<div class="visit-companion">동행 ${visit.companion_count}명</div>` : ''}
|
||||
${visit.vehicle_number ? `<div class="visit-vehicle">🚗 ${escapeHtml(visit.vehicle_number)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// TBM 탭 렌더링
|
||||
function renderTbmTab(tbmSessions) {
|
||||
if (!tbmSessions.length) {
|
||||
return `<div class="detail-empty">금일 TBM 세션이 없습니다.</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="tbm-list">
|
||||
${tbmSessions.map(tbm => `
|
||||
<div class="tbm-item">
|
||||
<div class="tbm-item-header">
|
||||
<span class="tbm-task">${escapeHtml(tbm.task_name || tbm.work_type_name || '작업')}</span>
|
||||
<span class="tbm-status ${tbm.status}">${getTbmStatusLabel(tbm.status)}</span>
|
||||
</div>
|
||||
<div class="tbm-location">📍 ${escapeHtml(tbm.work_location || '')}</div>
|
||||
<div class="tbm-leader">👷 ${escapeHtml(tbm.leader_name || tbm.leader_worker_name || '')}</div>
|
||||
${tbm.work_content ? `<div class="tbm-content">작업내용: ${escapeHtml(tbm.work_content).slice(0, 80)}...</div>` : ''}
|
||||
${tbm.team && tbm.team.length > 0 ? `
|
||||
<div class="tbm-team">
|
||||
<span class="tbm-team-label">팀원 (${tbm.team.length}명):</span>
|
||||
<span class="tbm-team-names">${tbm.team.map(m => escapeHtml(m.worker_name)).join(', ')}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${tbm.safety_measures ? `<div class="tbm-safety">⚠️ ${escapeHtml(tbm.safety_measures).slice(0, 60)}...</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 탭 전환
|
||||
function switchDetailTab(tabName) {
|
||||
// 탭 버튼 활성화
|
||||
document.querySelectorAll('.detail-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
});
|
||||
// 탭 콘텐츠 표시
|
||||
document.querySelectorAll('.detail-tab-content').forEach(content => {
|
||||
content.classList.toggle('active', content.id === `tab-${tabName}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 상세 패널 닫기
|
||||
function closeWorkplaceDetailPanel() {
|
||||
const panel = document.getElementById('workplaceDetailPanel');
|
||||
if (panel) {
|
||||
panel.style.display = 'none';
|
||||
}
|
||||
workplaceDetail = null;
|
||||
}
|
||||
|
||||
// 헬퍼 함수들
|
||||
function getPriorityLabel(priority) {
|
||||
const labels = { 'emergency': '긴급', 'high': '높음', 'normal': '보통', 'low': '낮음' };
|
||||
return labels[priority] || priority;
|
||||
}
|
||||
|
||||
function getEquipmentStatusLabel(status) {
|
||||
const labels = {
|
||||
'active': '정상',
|
||||
'inactive': '비활성',
|
||||
'repair_needed': '수리필요',
|
||||
'under_repair': '수리중',
|
||||
'disposed': '폐기'
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
function getTbmStatusLabel(status) {
|
||||
const labels = { 'draft': '작성중', 'in_progress': '진행중', 'completed': '완료' };
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user