- 인증 없는 임시 엔드포인트 삭제 (index.js, healthRoutes.js, publicPaths) - skipAuth 우회 라우트 삭제 (workAnalysis.js) - 하드코딩 유저 백도어 삭제 (routes/auth.js) - 안전체크 CRUD에 admin 권한 추가 (tbmRoutes.js) - deprecated shim 3개 삭제 + 8개 소비 파일 import 정리 (auth.js 직접 참조) - 미사용 pageAccessController, db.js, common/security.js 삭제 - escapeHtml() 5곳 로컬 중복 제거 → api-base.js 전역 사용 - userPageAccess_v2_v2 캐시 키 버그 수정 (app-init.js) - system3 .bak 파일 삭제, PROGRESS.md 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
439 lines
15 KiB
JavaScript
439 lines
15 KiB
JavaScript
// mobile-dashboard.js - 모바일 대시보드 v2
|
|
// 공장별 카테고리 탭 → 작업장 리스트 → 작업장별 상태 요약
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
if (window.innerWidth > 768) return;
|
|
|
|
var today = new Date().toISOString().slice(0, 10);
|
|
|
|
// ==================== 캐시 변수 ====================
|
|
var categories = [];
|
|
var allWorkplaces = [];
|
|
var tbmByWorkplace = {};
|
|
var visitorsByWorkplace = {};
|
|
var movedByWorkplace = {};
|
|
var issuesByWorkplace = {};
|
|
var workplacesByCategory = {};
|
|
|
|
// ==================== 유틸리티 ====================
|
|
|
|
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
|
|
|
function waitForApi(timeout) {
|
|
timeout = timeout || 5000;
|
|
return new Promise(function(resolve, reject) {
|
|
if (window.apiCall) return resolve();
|
|
var elapsed = 0;
|
|
var interval = setInterval(function() {
|
|
elapsed += 50;
|
|
if (window.apiCall) { clearInterval(interval); resolve(); }
|
|
else if (elapsed >= timeout) { clearInterval(interval); reject(new Error('apiCall timeout')); }
|
|
}, 50);
|
|
});
|
|
}
|
|
|
|
// ==================== 데이터 그룹핑 ====================
|
|
|
|
function groupTbmByWorkplace(sessions) {
|
|
tbmByWorkplace = {};
|
|
if (!Array.isArray(sessions)) return;
|
|
sessions.forEach(function(s) {
|
|
var wpId = s.workplace_id;
|
|
if (!wpId) return;
|
|
if (!tbmByWorkplace[wpId]) {
|
|
tbmByWorkplace[wpId] = { taskCount: 0, totalWorkers: 0, sessions: [] };
|
|
}
|
|
tbmByWorkplace[wpId].taskCount++;
|
|
// team_member_count + leader 1
|
|
tbmByWorkplace[wpId].totalWorkers += (parseInt(s.team_member_count) || 0) + 1;
|
|
tbmByWorkplace[wpId].sessions.push(s);
|
|
});
|
|
}
|
|
|
|
function groupVisitorsByWorkplace(requests) {
|
|
visitorsByWorkplace = {};
|
|
if (!Array.isArray(requests)) return;
|
|
requests.forEach(function(r) {
|
|
// 오늘 날짜 + 승인된 건만
|
|
if (r.visit_date !== today) return;
|
|
if (r.status !== 'approved') return;
|
|
var wpId = r.workplace_id;
|
|
if (!wpId) return;
|
|
if (!visitorsByWorkplace[wpId]) {
|
|
visitorsByWorkplace[wpId] = { visitCount: 0, totalVisitors: 0, requests: [] };
|
|
}
|
|
visitorsByWorkplace[wpId].visitCount++;
|
|
visitorsByWorkplace[wpId].totalVisitors += parseInt(r.visitor_count) || 0;
|
|
visitorsByWorkplace[wpId].requests.push(r);
|
|
});
|
|
}
|
|
|
|
function groupMovedByWorkplace(items) {
|
|
movedByWorkplace = {};
|
|
if (!Array.isArray(items)) return;
|
|
items.forEach(function(eq) {
|
|
var wpId = eq.current_workplace_id;
|
|
if (!wpId) return;
|
|
if (!movedByWorkplace[wpId]) {
|
|
movedByWorkplace[wpId] = { movedCount: 0, items: [] };
|
|
}
|
|
movedByWorkplace[wpId].movedCount++;
|
|
movedByWorkplace[wpId].items.push(eq);
|
|
});
|
|
}
|
|
|
|
function groupIssuesByWorkplace(issues) {
|
|
issuesByWorkplace = {};
|
|
if (!Array.isArray(issues)) return;
|
|
var activeStatuses = ['reported', 'received', 'in_progress'];
|
|
issues.forEach(function(issue) {
|
|
var wpId = issue.workplace_id;
|
|
if (!wpId) return;
|
|
if (activeStatuses.indexOf(issue.status) === -1) return;
|
|
if (!issuesByWorkplace[wpId]) {
|
|
issuesByWorkplace[wpId] = { activeCount: 0, items: [] };
|
|
}
|
|
issuesByWorkplace[wpId].activeCount++;
|
|
issuesByWorkplace[wpId].items.push(issue);
|
|
});
|
|
}
|
|
|
|
function groupWorkplacesByCategory(workplaces) {
|
|
workplacesByCategory = {};
|
|
if (!Array.isArray(workplaces)) return;
|
|
workplaces.forEach(function(wp) {
|
|
var catId = wp.category_id;
|
|
if (!catId) return;
|
|
if (!workplacesByCategory[catId]) {
|
|
workplacesByCategory[catId] = [];
|
|
}
|
|
workplacesByCategory[catId].push(wp);
|
|
});
|
|
}
|
|
|
|
// ==================== 렌더링 ====================
|
|
|
|
function renderCategoryTabs() {
|
|
var container = document.getElementById('mCategoryTabs');
|
|
if (!container || !categories.length) return;
|
|
|
|
var html = '';
|
|
categories.forEach(function(cat, idx) {
|
|
html += '<button class="md-cat-tab' + (idx === 0 ? ' active' : '') +
|
|
'" data-id="' + cat.category_id + '">' +
|
|
escapeHtml(cat.category_name) + '</button>';
|
|
});
|
|
|
|
// 전체 탭
|
|
html += '<button class="md-cat-tab" data-id="all">전체</button>';
|
|
|
|
container.innerHTML = html;
|
|
|
|
// 이벤트 바인딩
|
|
var tabs = container.querySelectorAll('.md-cat-tab');
|
|
tabs.forEach(function(tab) {
|
|
tab.addEventListener('click', function() {
|
|
tabs.forEach(function(t) { t.classList.remove('active'); });
|
|
tab.classList.add('active');
|
|
var catId = tab.getAttribute('data-id');
|
|
selectCategory(catId);
|
|
});
|
|
});
|
|
|
|
// 첫 번째 카테고리 자동 선택
|
|
if (categories.length > 0) {
|
|
selectCategory(String(categories[0].category_id));
|
|
}
|
|
}
|
|
|
|
function selectCategory(categoryId) {
|
|
var workplaces;
|
|
if (categoryId === 'all') {
|
|
workplaces = allWorkplaces.filter(function(wp) { return wp.is_active !== false; });
|
|
} else {
|
|
workplaces = (workplacesByCategory[categoryId] || []).filter(function(wp) {
|
|
return wp.is_active !== false;
|
|
});
|
|
}
|
|
renderWorkplaceList(workplaces);
|
|
}
|
|
|
|
function renderWorkplaceList(workplaces) {
|
|
var container = document.getElementById('mWorkplaceList');
|
|
if (!container) return;
|
|
|
|
if (!workplaces || workplaces.length === 0) {
|
|
container.innerHTML = '<div class="md-wp-empty-all">등록된 작업장이 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
workplaces.forEach(function(wp) {
|
|
var wpId = wp.workplace_id;
|
|
var tbm = tbmByWorkplace[wpId];
|
|
var visitors = visitorsByWorkplace[wpId];
|
|
var moved = movedByWorkplace[wpId];
|
|
var issues = issuesByWorkplace[wpId];
|
|
|
|
var hasAny = tbm || visitors || moved || issues;
|
|
|
|
html += '<div class="md-wp-card" data-wp-id="' + wpId + '">';
|
|
|
|
// 헤더 (클릭 영역)
|
|
html += '<div class="md-wp-header">';
|
|
html += '<h3 class="md-wp-name">' + escapeHtml(wp.workplace_name);
|
|
if (hasAny) {
|
|
html += '<span class="md-wp-toggle">▼</span>';
|
|
}
|
|
html += '</h3>';
|
|
|
|
if (!hasAny) {
|
|
html += '<p class="md-wp-no-activity">오늘 활동이 없습니다</p>';
|
|
} else {
|
|
html += '<div class="md-wp-stats">';
|
|
|
|
// TBM 작업
|
|
if (tbm) {
|
|
html += '<div class="md-wp-stat-row">' +
|
|
'<span class="md-wp-stat-icon">🛠</span>' +
|
|
'<span class="md-wp-stat-text">작업 ' + tbm.taskCount + '건 · ' + tbm.totalWorkers + '명</span>' +
|
|
'</div>';
|
|
}
|
|
|
|
// 방문
|
|
if (visitors) {
|
|
html += '<div class="md-wp-stat-row">' +
|
|
'<span class="md-wp-stat-icon">🚪</span>' +
|
|
'<span class="md-wp-stat-text">방문 ' + visitors.visitCount + '건 · ' + visitors.totalVisitors + '명</span>' +
|
|
'</div>';
|
|
}
|
|
|
|
// 신고 (미완료만)
|
|
if (issues && issues.activeCount > 0) {
|
|
html += '<div class="md-wp-stat-row md-wp-stat--warning">' +
|
|
'<span class="md-wp-stat-icon">⚠</span>' +
|
|
'<span class="md-wp-stat-text">신고 ' + issues.activeCount + '건</span>' +
|
|
'</div>';
|
|
}
|
|
|
|
// 이동설비
|
|
if (moved && moved.movedCount > 0) {
|
|
html += '<div class="md-wp-stat-row">' +
|
|
'<span class="md-wp-stat-icon">↔</span>' +
|
|
'<span class="md-wp-stat-text">이동설비 ' + moved.movedCount + '건</span>' +
|
|
'</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>'; // .md-wp-header
|
|
|
|
// 상세 영역 (활동 있는 카드만)
|
|
if (hasAny) {
|
|
html += '<div class="md-wp-detail">' + renderCardDetail(wpId) + '</div>';
|
|
}
|
|
|
|
html += '</div>'; // .md-wp-card
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
|
|
// 클릭 이벤트 바인딩
|
|
var cards = container.querySelectorAll('.md-wp-card[data-wp-id]');
|
|
cards.forEach(function(card) {
|
|
var wpId = card.getAttribute('data-wp-id');
|
|
var hasActivity = tbmByWorkplace[wpId] || visitorsByWorkplace[wpId] ||
|
|
movedByWorkplace[wpId] || issuesByWorkplace[wpId];
|
|
if (!hasActivity) return;
|
|
card.querySelector('.md-wp-header').addEventListener('click', function() {
|
|
toggleCard(wpId);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ==================== 카드 확장/접기 ====================
|
|
|
|
function toggleCard(wpId) {
|
|
var allCards = document.querySelectorAll('.md-wp-card.expanded');
|
|
var targetCard = document.querySelector('.md-wp-card[data-wp-id="' + wpId + '"]');
|
|
if (!targetCard) return;
|
|
|
|
var isExpanded = targetCard.classList.contains('expanded');
|
|
|
|
// 다른 카드 모두 접기 (아코디언)
|
|
allCards.forEach(function(card) {
|
|
card.classList.remove('expanded');
|
|
});
|
|
|
|
// 토글
|
|
if (!isExpanded) {
|
|
targetCard.classList.add('expanded');
|
|
}
|
|
}
|
|
|
|
function renderCardDetail(wpId) {
|
|
var html = '';
|
|
var tbm = tbmByWorkplace[wpId];
|
|
var visitors = visitorsByWorkplace[wpId];
|
|
var issues = issuesByWorkplace[wpId];
|
|
var moved = movedByWorkplace[wpId];
|
|
|
|
// TBM 작업
|
|
if (tbm && tbm.sessions.length > 0) {
|
|
html += '<div class="md-wp-detail-section">';
|
|
html += '<div class="md-wp-detail-title">▶ 작업</div>';
|
|
tbm.sessions.forEach(function(s) {
|
|
var taskName = s.task_name || '작업명 미지정';
|
|
var leaderName = s.leader_name || '미지정';
|
|
var memberCount = (parseInt(s.team_member_count) || 0) + 1;
|
|
html += '<div class="md-wp-detail-item">';
|
|
html += '<div class="md-wp-detail-main">' + escapeHtml(taskName) + '</div>';
|
|
html += '<div class="md-wp-detail-sub">' + escapeHtml(leaderName) + ' · ' + memberCount + '명</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 방문
|
|
if (visitors && visitors.requests.length > 0) {
|
|
html += '<div class="md-wp-detail-section">';
|
|
html += '<div class="md-wp-detail-title">▶ 방문</div>';
|
|
visitors.requests.forEach(function(r) {
|
|
var company = r.visitor_company || '업체 미지정';
|
|
var count = parseInt(r.visitor_count) || 0;
|
|
var purpose = r.purpose_name || '';
|
|
html += '<div class="md-wp-detail-item">';
|
|
html += '<div class="md-wp-detail-main">' + escapeHtml(company) + ' · ' + count + '명';
|
|
if (purpose) html += ' · ' + escapeHtml(purpose);
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 신고
|
|
if (issues && issues.items.length > 0) {
|
|
var statusMap = { reported: '신고', received: '접수', in_progress: '처리중' };
|
|
html += '<div class="md-wp-detail-section">';
|
|
html += '<div class="md-wp-detail-title">▶ 신고</div>';
|
|
issues.items.forEach(function(issue) {
|
|
var category = issue.issue_category_name || '미분류';
|
|
var desc = issue.additional_description || '';
|
|
if (desc.length > 30) desc = desc.substring(0, 30) + '...';
|
|
var statusText = statusMap[issue.status] || issue.status;
|
|
var statusClass = 'md-wp-issue-status--' + (issue.status || 'reported');
|
|
var reporter = issue.reporter_name || '';
|
|
var icon = issue.status === 'in_progress' ? '🔴' : '⚠';
|
|
html += '<div class="md-wp-detail-item">';
|
|
html += '<div class="md-wp-detail-main">' + icon + ' ' + escapeHtml(category);
|
|
if (desc) html += ' · ' + escapeHtml(desc);
|
|
html += '</div>';
|
|
html += '<div class="md-wp-detail-sub"><span class="md-wp-issue-status ' + statusClass + '">' + statusText + '</span>';
|
|
if (reporter) html += ' → ' + escapeHtml(reporter);
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 이동설비
|
|
if (moved && moved.items.length > 0) {
|
|
html += '<div class="md-wp-detail-section">';
|
|
html += '<div class="md-wp-detail-title">▶ 이동설비</div>';
|
|
moved.items.forEach(function(eq) {
|
|
var eqName = eq.equipment_name || '설비명 미지정';
|
|
var fromWp = eq.original_workplace_name || '?';
|
|
var toWp = eq.current_workplace_name || '?';
|
|
html += '<div class="md-wp-detail-item">';
|
|
html += '<div class="md-wp-detail-main">' + escapeHtml(eqName) + '</div>';
|
|
html += '<div class="md-wp-detail-sub">' + escapeHtml(fromWp) + ' → ' + escapeHtml(toWp) + '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ==================== 초기화 ====================
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
try {
|
|
await waitForApi();
|
|
} catch (e) {
|
|
console.error('mobile-dashboard: apiCall not available');
|
|
return;
|
|
}
|
|
|
|
var view = document.getElementById('mobileDashboardView');
|
|
if (!view) return;
|
|
view.style.display = 'block';
|
|
|
|
// 날짜 표시
|
|
var now = new Date();
|
|
var days = ['일', '월', '화', '수', '목', '금', '토'];
|
|
var dateEl = document.getElementById('mDateValue');
|
|
if (dateEl) {
|
|
dateEl.textContent = now.getFullYear() + '.' +
|
|
String(now.getMonth() + 1).padStart(2, '0') + '.' +
|
|
String(now.getDate()).padStart(2, '0') + ' (' + days[now.getDay()] + ')';
|
|
}
|
|
|
|
// 로딩 표시
|
|
var listContainer = document.getElementById('mWorkplaceList');
|
|
if (listContainer) {
|
|
listContainer.innerHTML =
|
|
'<div class="md-skeleton"></div>' +
|
|
'<div class="md-skeleton" style="margin-top:8px;"></div>' +
|
|
'<div class="md-skeleton" style="margin-top:8px;"></div>';
|
|
}
|
|
|
|
// 데이터 병렬 로딩
|
|
var results = await Promise.allSettled([
|
|
window.apiCall('/workplaces/categories'),
|
|
window.apiCall('/tbm/sessions/date/' + today),
|
|
window.apiCall('/workplace-visits/requests?visit_date=' + today + '&status=approved'),
|
|
window.apiCall('/equipments/moved/list'),
|
|
window.apiCall('/work-issues?start_date=' + today + '&end_date=' + today),
|
|
window.apiCall('/workplaces')
|
|
]);
|
|
|
|
// 카테고리
|
|
if (results[0].status === 'fulfilled' && results[0].value && results[0].value.success) {
|
|
categories = results[0].value.data || [];
|
|
}
|
|
|
|
// TBM
|
|
if (results[1].status === 'fulfilled' && results[1].value && results[1].value.success) {
|
|
groupTbmByWorkplace(results[1].value.data || []);
|
|
}
|
|
|
|
// 방문
|
|
if (results[2].status === 'fulfilled' && results[2].value && results[2].value.success) {
|
|
groupVisitorsByWorkplace(results[2].value.data || []);
|
|
}
|
|
|
|
// 이동설비
|
|
if (results[3].status === 'fulfilled' && results[3].value && results[3].value.success) {
|
|
groupMovedByWorkplace(results[3].value.data || []);
|
|
}
|
|
|
|
// 신고
|
|
if (results[4].status === 'fulfilled' && results[4].value && results[4].value.success) {
|
|
groupIssuesByWorkplace(results[4].value.data || []);
|
|
}
|
|
|
|
// 작업장 전체 (카테고리별 그룹핑)
|
|
if (results[5].status === 'fulfilled' && results[5].value && results[5].value.success) {
|
|
allWorkplaces = results[5].value.data || [];
|
|
groupWorkplacesByCategory(allWorkplaces);
|
|
}
|
|
|
|
// 렌더링
|
|
renderCategoryTabs();
|
|
});
|
|
})();
|