- 인증 없는 임시 엔드포인트 삭제 (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>
682 lines
24 KiB
JavaScript
682 lines
24 KiB
JavaScript
// /js/app-init.js
|
|
// 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드
|
|
// 모든 페이지에서 이 하나의 스크립트만 로드하면 됨
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ===== 캐시 설정 =====
|
|
const CACHE_DURATION = 10 * 60 * 1000; // 10분
|
|
const COMPONENT_CACHE_PREFIX = 'component_v3_';
|
|
|
|
// ===== 인증 함수 (api-base.js의 전역 헬퍼 활용) =====
|
|
function isLoggedIn() {
|
|
var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
|
return token && token !== 'undefined' && token !== 'null';
|
|
}
|
|
|
|
function getUser() {
|
|
return window.getSSOUser ? window.getSSOUser() : (function() {
|
|
var u = localStorage.getItem('sso_user');
|
|
return u ? JSON.parse(u) : null;
|
|
})();
|
|
}
|
|
|
|
function clearAuthData() {
|
|
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
|
localStorage.removeItem('sso_token');
|
|
localStorage.removeItem('sso_user');
|
|
localStorage.removeItem('userPageAccess_v2');
|
|
}
|
|
|
|
// ===== 페이지 권한 캐시 =====
|
|
let pageAccessPromise = null;
|
|
|
|
async function getPageAccess(currentUser) {
|
|
if (!currentUser || !currentUser.user_id) return null;
|
|
|
|
// 캐시 확인
|
|
const cached = localStorage.getItem('userPageAccess_v2');
|
|
if (cached) {
|
|
try {
|
|
const cacheData = JSON.parse(cached);
|
|
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
|
|
return cacheData.pages;
|
|
}
|
|
} catch (e) {
|
|
localStorage.removeItem('userPageAccess_v2');
|
|
}
|
|
}
|
|
|
|
// 이미 로딩 중이면 기존 Promise 반환
|
|
if (pageAccessPromise) return pageAccessPromise;
|
|
|
|
// 새로운 API 호출
|
|
pageAccessPromise = (async () => {
|
|
try {
|
|
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + (window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))
|
|
}
|
|
});
|
|
|
|
if (!response.ok) return null;
|
|
|
|
const data = await response.json();
|
|
const pages = data.data.pageAccess || [];
|
|
|
|
localStorage.setItem('userPageAccess_v2', JSON.stringify({
|
|
pages: pages,
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
return pages;
|
|
} catch (error) {
|
|
console.error('페이지 권한 조회 오류:', error);
|
|
return null;
|
|
} finally {
|
|
pageAccessPromise = null;
|
|
}
|
|
})();
|
|
|
|
return pageAccessPromise;
|
|
}
|
|
|
|
async function getAccessiblePageKeys(currentUser) {
|
|
const pages = await getPageAccess(currentUser);
|
|
if (!pages) return [];
|
|
return pages.filter(p => p.can_access === 1).map(p => p.page_key);
|
|
}
|
|
|
|
// ===== 현재 페이지 키 추출 =====
|
|
// 하위 페이지 → 부모 페이지 키 매핑 (동일 권한 공유)
|
|
var PAGE_KEY_ALIASES = {
|
|
'work.tbm-create': 'work.tbm',
|
|
'work.tbm-mobile': 'work.tbm',
|
|
'work.report-create-mobile': 'work.report-create'
|
|
};
|
|
|
|
function getCurrentPageKey() {
|
|
const path = window.location.pathname;
|
|
if (!path.startsWith('/pages/')) return null;
|
|
const pagePath = path.substring(7).replace('.html', '');
|
|
const rawKey = pagePath.replace(/\//g, '.');
|
|
return PAGE_KEY_ALIASES[rawKey] || rawKey;
|
|
}
|
|
|
|
// ===== 컴포넌트 로더 =====
|
|
async function loadComponent(name, selector, processor) {
|
|
const container = document.querySelector(selector);
|
|
if (!container) return;
|
|
|
|
const paths = {
|
|
'navbar': '/components/navbar.html',
|
|
'sidebar-nav': '/components/sidebar-nav.html'
|
|
};
|
|
|
|
const componentPath = paths[name];
|
|
if (!componentPath) return;
|
|
|
|
try {
|
|
const cacheKey = COMPONENT_CACHE_PREFIX + name;
|
|
let html = sessionStorage.getItem(cacheKey);
|
|
|
|
if (!html) {
|
|
const response = await fetch(componentPath);
|
|
if (!response.ok) throw new Error('컴포넌트 로드 실패');
|
|
html = await response.text();
|
|
try { sessionStorage.setItem(cacheKey, html); } catch (e) {}
|
|
}
|
|
|
|
if (processor) {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
await processor(doc);
|
|
container.innerHTML = doc.body.innerHTML;
|
|
} else {
|
|
container.innerHTML = html;
|
|
}
|
|
} catch (error) {
|
|
console.error(`컴포넌트 로드 오류 (${name}):`, error);
|
|
}
|
|
}
|
|
|
|
// ===== 네비바 처리 =====
|
|
const ROLE_NAMES = {
|
|
'system admin': '시스템 관리자',
|
|
'admin': '관리자',
|
|
'leader': '그룹장',
|
|
'user': '작업자',
|
|
'support': '지원팀',
|
|
'default': '사용자'
|
|
};
|
|
|
|
async function processNavbar(doc, currentUser, accessiblePageKeys) {
|
|
const userRole = (currentUser.role || '').toLowerCase();
|
|
const isAdmin = userRole === 'admin' || userRole === 'system admin';
|
|
|
|
if (isAdmin) {
|
|
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
|
} else {
|
|
doc.querySelectorAll('[data-page-key]').forEach(item => {
|
|
const pageKey = item.getAttribute('data-page-key');
|
|
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
|
|
if (!accessiblePageKeys.includes(pageKey)) item.remove();
|
|
});
|
|
doc.querySelectorAll('.admin-only').forEach(el => el.remove());
|
|
}
|
|
|
|
// 사용자 정보 표시
|
|
const displayName = currentUser.name || currentUser.username;
|
|
const roleName = ROLE_NAMES[userRole] || ROLE_NAMES.default;
|
|
|
|
const setElementText = (id, text) => {
|
|
const el = doc.getElementById(id);
|
|
if (el) el.textContent = text;
|
|
};
|
|
|
|
setElementText('userName', displayName);
|
|
setElementText('userRole', roleName);
|
|
setElementText('userInitial', displayName.charAt(0));
|
|
}
|
|
|
|
// ===== 사이드바 처리 =====
|
|
async function processSidebar(doc, currentUser, accessiblePageKeys) {
|
|
const userRole = (currentUser.role || '').toLowerCase();
|
|
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
|
// role 또는 access_level로 관리자 확인
|
|
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
|
accessLevel === 'admin' || accessLevel === 'system';
|
|
|
|
if (isAdmin) {
|
|
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
|
} else {
|
|
doc.querySelectorAll('[data-page-key]').forEach(item => {
|
|
const pageKey = item.getAttribute('data-page-key');
|
|
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
|
|
if (!accessiblePageKeys.includes(pageKey)) item.style.display = 'none';
|
|
});
|
|
doc.querySelectorAll('.nav-category.admin-only').forEach(el => el.remove());
|
|
}
|
|
|
|
// 현재 페이지 하이라이트
|
|
const currentPath = window.location.pathname;
|
|
doc.querySelectorAll('.nav-item').forEach(item => {
|
|
const href = item.getAttribute('href');
|
|
if (href && currentPath.includes(href.replace(/^\//, ''))) {
|
|
item.classList.add('active');
|
|
const category = item.closest('.nav-category');
|
|
if (category) category.classList.add('expanded');
|
|
}
|
|
});
|
|
|
|
// 크로스 시스템 링크 URL 설정
|
|
var hostname = window.location.hostname;
|
|
var protocol = window.location.protocol;
|
|
var systemUrls = {};
|
|
if (hostname.includes('technicalkorea.net')) {
|
|
systemUrls.report = protocol + '//tkreport.technicalkorea.net';
|
|
systemUrls.nc = protocol + '//tkqc.technicalkorea.net';
|
|
} else {
|
|
systemUrls.report = protocol + '//' + hostname + ':30180';
|
|
systemUrls.nc = protocol + '//' + hostname + ':30280';
|
|
}
|
|
doc.querySelectorAll('.cross-system-link').forEach(function(link) {
|
|
var system = link.getAttribute('data-system');
|
|
var path = link.getAttribute('data-path');
|
|
if (systemUrls[system]) {
|
|
link.setAttribute('href', systemUrls[system] + path);
|
|
link.setAttribute('target', '_blank');
|
|
}
|
|
});
|
|
|
|
// 저장된 상태 복원 (기본값: 접힌 상태)
|
|
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
|
const sidebar = doc.querySelector('.sidebar-nav');
|
|
if (isCollapsed && sidebar) {
|
|
sidebar.classList.add('collapsed');
|
|
document.body.classList.add('sidebar-collapsed');
|
|
}
|
|
|
|
const expandedCategories = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]');
|
|
expandedCategories.forEach(category => {
|
|
const el = doc.querySelector(`[data-category="${category}"]`);
|
|
if (el) el.classList.add('expanded');
|
|
});
|
|
}
|
|
|
|
// ===== 사이드바 이벤트 설정 =====
|
|
function setupSidebarEvents() {
|
|
const sidebar = document.getElementById('sidebarNav');
|
|
const toggle = document.getElementById('sidebarToggle');
|
|
if (!sidebar || !toggle) return;
|
|
|
|
toggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('collapsed');
|
|
document.body.classList.toggle('sidebar-collapsed');
|
|
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
|
|
});
|
|
|
|
sidebar.querySelectorAll('.nav-category-header').forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const category = header.closest('.nav-category');
|
|
category.classList.toggle('expanded');
|
|
|
|
const expanded = [];
|
|
sidebar.querySelectorAll('.nav-category.expanded').forEach(cat => {
|
|
const name = cat.getAttribute('data-category');
|
|
if (name) expanded.push(name);
|
|
});
|
|
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== 네비바 이벤트 설정 =====
|
|
function setupNavbarEvents() {
|
|
const logoutButton = document.getElementById('logoutBtn');
|
|
if (logoutButton) {
|
|
logoutButton.addEventListener('click', () => {
|
|
if (confirm('로그아웃 하시겠습니까?')) {
|
|
clearAuthData();
|
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 알림 버튼 이벤트
|
|
const notificationBtn = document.getElementById('notificationBtn');
|
|
const notificationDropdown = document.getElementById('notificationDropdown');
|
|
const notificationWrapper = document.getElementById('notificationWrapper');
|
|
|
|
if (notificationBtn && notificationDropdown) {
|
|
notificationBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
notificationDropdown.classList.toggle('show');
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (notificationWrapper && !notificationWrapper.contains(e.target)) {
|
|
notificationDropdown.classList.remove('show');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== 알림 로드 =====
|
|
async function loadNotifications() {
|
|
try {
|
|
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
|
if (!token) return;
|
|
|
|
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (!response.ok) return;
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
const notifications = result.data || [];
|
|
updateNotificationBadge(notifications.length);
|
|
renderNotificationList(notifications);
|
|
}
|
|
} catch (error) {
|
|
console.warn('알림 로드 오류:', error.message);
|
|
}
|
|
}
|
|
|
|
function updateNotificationBadge(count) {
|
|
const badge = document.getElementById('notificationBadge');
|
|
const btn = document.getElementById('notificationBtn');
|
|
if (!badge) return;
|
|
|
|
if (count > 0) {
|
|
badge.textContent = count > 99 ? '99+' : count;
|
|
badge.style.display = 'flex';
|
|
btn?.classList.add('has-notifications');
|
|
} else {
|
|
badge.style.display = 'none';
|
|
btn?.classList.remove('has-notifications');
|
|
}
|
|
}
|
|
|
|
function renderNotificationList(notifications) {
|
|
const list = document.getElementById('notificationList');
|
|
if (!list) return;
|
|
|
|
if (notifications.length === 0) {
|
|
list.innerHTML = '<div class="notification-empty">새 알림이 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
const icons = { repair: '🔧', safety: '⚠️', system: '📢', equipment: '🔩', maintenance: '🛠️' };
|
|
|
|
list.innerHTML = notifications.slice(0, 5).map(n => `
|
|
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
|
|
<div class="notification-item-icon ${n.type || 'repair'}">${icons[n.type] || '🔔'}</div>
|
|
<div class="notification-item-content">
|
|
<div class="notification-item-title">${escapeHtml(n.title)}</div>
|
|
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
|
|
</div>
|
|
<div class="notification-item-time">${formatTimeAgo(n.created_at)}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
list.querySelectorAll('.notification-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const url = item.dataset.url;
|
|
// 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리해야 함)
|
|
window.location.href = url || '/pages/admin/notifications.html';
|
|
});
|
|
});
|
|
}
|
|
|
|
function formatTimeAgo(dateString) {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return '방금 전';
|
|
if (diffMins < 60) return `${diffMins}분 전`;
|
|
if (diffHours < 24) return `${diffHours}시간 전`;
|
|
if (diffDays < 7) return `${diffDays}일 전`;
|
|
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
|
var escapeHtml = window.escapeHtml;
|
|
|
|
// ===== 날짜/시간 업데이트 =====
|
|
function updateDateTime() {
|
|
const now = new Date();
|
|
const timeEl = document.getElementById('timeValue');
|
|
if (timeEl) {
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
timeEl.textContent = `${hours}시 ${minutes}분 ${seconds}초`;
|
|
}
|
|
|
|
const dateEl = document.getElementById('dateValue');
|
|
if (dateEl) {
|
|
const days = ['일', '월', '화', '수', '목', '토'];
|
|
dateEl.textContent = `${now.getMonth() + 1}월 ${now.getDate()}일 (${days[now.getDay()]})`;
|
|
}
|
|
}
|
|
|
|
// ===== 날씨 업데이트 =====
|
|
const WEATHER_ICONS = { clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥', cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷', cloudy: '⛅', overcast: '☁️' };
|
|
const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' };
|
|
|
|
async function updateWeather() {
|
|
try {
|
|
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
|
if (!token) return;
|
|
|
|
// 캐시 확인
|
|
const cached = sessionStorage.getItem('weatherCache');
|
|
let result;
|
|
if (cached) {
|
|
const cacheData = JSON.parse(cached);
|
|
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
|
|
result = cacheData.data;
|
|
}
|
|
}
|
|
|
|
if (!result) {
|
|
const response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (!response.ok) return;
|
|
result = await response.json();
|
|
sessionStorage.setItem('weatherCache', JSON.stringify({ data: result, timestamp: Date.now() }));
|
|
}
|
|
|
|
if (result.success && result.data) {
|
|
const { temperature, conditions } = result.data;
|
|
const tempEl = document.getElementById('weatherTemp');
|
|
if (tempEl && temperature != null) tempEl.textContent = `${Math.round(temperature)}°C`;
|
|
|
|
const iconEl = document.getElementById('weatherIcon');
|
|
const descEl = document.getElementById('weatherDesc');
|
|
if (conditions && conditions.length > 0) {
|
|
const primary = conditions[0];
|
|
if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '🌤️';
|
|
if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('날씨 정보 로드 실패');
|
|
}
|
|
}
|
|
|
|
// ===== 메인 초기화 =====
|
|
async function init() {
|
|
console.log('🚀 app-init 시작');
|
|
|
|
// 1. 인증 확인
|
|
if (!isLoggedIn()) {
|
|
clearAuthData();
|
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
|
return;
|
|
}
|
|
|
|
const currentUser = getUser();
|
|
if (!currentUser || !currentUser.username) {
|
|
clearAuthData();
|
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
|
return;
|
|
}
|
|
|
|
console.log('✅ 인증 확인:', currentUser.username);
|
|
|
|
const userRole = (currentUser.role || '').toLowerCase();
|
|
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
|
// role 또는 access_level로 관리자 확인
|
|
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
|
accessLevel === 'admin' || accessLevel === 'system';
|
|
|
|
// 2. 페이지 접근 권한 체크 (Admin은 건너뛰기)
|
|
let accessiblePageKeys = [];
|
|
if (!isAdmin) {
|
|
const pageKey = getCurrentPageKey();
|
|
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
|
|
accessiblePageKeys = await getAccessiblePageKeys(currentUser);
|
|
if (!accessiblePageKeys.includes(pageKey)) {
|
|
alert('이 페이지에 접근할 권한이 없습니다.');
|
|
window.location.href = '/pages/dashboard.html';
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. 네비바 로드 (모바일이면 사이드바 스킵)
|
|
var isMobile = window.innerWidth <= 768;
|
|
|
|
if (!isMobile) {
|
|
// 데스크톱: 사이드바 컨테이너 생성 및 로드
|
|
let sidebarContainer = document.getElementById('sidebar-container');
|
|
if (!sidebarContainer) {
|
|
sidebarContainer = document.createElement('div');
|
|
sidebarContainer.id = 'sidebar-container';
|
|
document.body.prepend(sidebarContainer);
|
|
}
|
|
|
|
console.log('📥 컴포넌트 로딩 시작 (데스크톱: 네비바+사이드바)');
|
|
await Promise.all([
|
|
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
|
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
|
]);
|
|
|
|
setupNavbarEvents();
|
|
setupSidebarEvents();
|
|
document.body.classList.add('has-sidebar');
|
|
} else {
|
|
// 모바일: 네비바만 로드, 사이드바 없음
|
|
console.log('📥 컴포넌트 로딩 시작 (모바일: 네비바만)');
|
|
await loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys));
|
|
setupNavbarEvents();
|
|
}
|
|
console.log('✅ 컴포넌트 로딩 완료');
|
|
|
|
// 6. 페이지 전환 로딩 인디케이터 설정
|
|
setupPageTransitionLoader();
|
|
|
|
// 7. 날짜/시간 (비동기)
|
|
updateDateTime();
|
|
setInterval(updateDateTime, 1000);
|
|
|
|
// 8. 날씨 (백그라운드)
|
|
setTimeout(updateWeather, 100);
|
|
|
|
// 9. 알림 로드 (30초마다 갱신)
|
|
setTimeout(loadNotifications, 200);
|
|
setInterval(loadNotifications, 30000);
|
|
|
|
// 10. PWA 설정 (manifest + 서비스 워커 + iOS 메타태그)
|
|
setupPWA();
|
|
|
|
console.log('✅ app-init 완료');
|
|
}
|
|
|
|
// ===== PWA 설정 =====
|
|
function setupPWA() {
|
|
// manifest.json 동적 추가
|
|
if (!document.querySelector('link[rel="manifest"]')) {
|
|
var manifest = document.createElement('link');
|
|
manifest.rel = 'manifest';
|
|
manifest.href = '/manifest.json';
|
|
document.head.appendChild(manifest);
|
|
}
|
|
|
|
// iOS 홈 화면 앱 메타태그
|
|
if (!document.querySelector('meta[name="apple-mobile-web-app-capable"]')) {
|
|
var metaTags = [
|
|
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
|
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
|
|
{ name: 'apple-mobile-web-app-title', content: 'TK공장' },
|
|
{ name: 'theme-color', content: '#1e40af' }
|
|
];
|
|
metaTags.forEach(function(tag) {
|
|
var meta = document.createElement('meta');
|
|
meta.name = tag.name;
|
|
meta.content = tag.content;
|
|
document.head.appendChild(meta);
|
|
});
|
|
|
|
// iOS 아이콘
|
|
var appleIcon = document.createElement('link');
|
|
appleIcon.rel = 'apple-touch-icon';
|
|
appleIcon.href = '/img/icon-192x192.png';
|
|
document.head.appendChild(appleIcon);
|
|
}
|
|
|
|
// 서비스 워커 등록 (킬스위치 포함)
|
|
if ('serviceWorker' in navigator) {
|
|
// 킬스위치: ?sw-kill 파라미터로 서비스 워커 해제
|
|
if (window.location.search.includes('sw-kill')) {
|
|
navigator.serviceWorker.getRegistrations().then(function(regs) {
|
|
regs.forEach(function(r) { r.unregister(); });
|
|
caches.keys().then(function(keys) {
|
|
keys.forEach(function(k) { caches.delete(k); });
|
|
});
|
|
console.log('SW 해제 완료');
|
|
window.location.replace(window.location.pathname);
|
|
});
|
|
return;
|
|
}
|
|
|
|
navigator.serviceWorker.register('/sw.js')
|
|
.then(function(reg) {
|
|
console.log('SW 등록 완료');
|
|
})
|
|
.catch(function(err) {
|
|
console.warn('SW 등록 실패:', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== 페이지 전환 로딩 인디케이터 =====
|
|
function setupPageTransitionLoader() {
|
|
// 로딩 바 스타일 추가
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#page-loader {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
|
z-index: 99999;
|
|
transition: width 0.3s ease;
|
|
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
|
|
}
|
|
#page-loader.loading {
|
|
width: 70%;
|
|
}
|
|
#page-loader.done {
|
|
width: 100%;
|
|
opacity: 0;
|
|
transition: width 0.2s ease, opacity 0.3s ease 0.2s;
|
|
}
|
|
body.page-loading {
|
|
cursor: wait;
|
|
}
|
|
body.page-loading * {
|
|
pointer-events: none;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// 로딩 바 엘리먼트 생성
|
|
const loader = document.createElement('div');
|
|
loader.id = 'page-loader';
|
|
document.body.appendChild(loader);
|
|
|
|
// 모든 내부 링크에 클릭 이벤트 추가
|
|
document.addEventListener('click', (e) => {
|
|
const link = e.target.closest('a');
|
|
if (!link) return;
|
|
|
|
const href = link.getAttribute('href');
|
|
if (!href) return;
|
|
|
|
// 외부 링크, 해시 링크, javascript: 링크 제외
|
|
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return;
|
|
|
|
// 새 탭 링크 제외
|
|
if (link.target === '_blank') return;
|
|
|
|
// 로딩 시작
|
|
loader.classList.remove('done');
|
|
loader.classList.add('loading');
|
|
document.body.classList.add('page-loading');
|
|
});
|
|
|
|
// 페이지 떠날 때 완료 표시
|
|
window.addEventListener('beforeunload', () => {
|
|
const loader = document.getElementById('page-loader');
|
|
if (loader) {
|
|
loader.classList.remove('loading');
|
|
loader.classList.add('done');
|
|
}
|
|
});
|
|
}
|
|
|
|
// DOMContentLoaded 시 실행
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// 전역 노출 (필요시)
|
|
window.appInit = { getUser, clearAuthData, isLoggedIn };
|
|
})();
|