// /js/api-base.js // API 기본 설정 및 보안 유틸리티 (비모듈 - 빠른 로딩용) // ==================== SW 캐시 강제 해제 (PWA 홈화면 추가 대응) ==================== if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(reg) { reg.unregister(); }); }); } if ('caches' in window) { caches.keys().then(function(keys) { keys.forEach(function(key) { caches.delete(key); }); }); } (function() { 'use strict'; // ==================== SSO 쿠키 유틸리티 ==================== function cookieGet(name) { var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); return match ? decodeURIComponent(match[1]) : null; } function cookieRemove(name) { var cookie = name + '=; path=/; max-age=0'; if (window.location.hostname.includes('technicalkorea.net')) { cookie += '; domain=.technicalkorea.net'; } document.cookie = cookie; } /** * SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백) * sso_token이 없으면 기존 token도 확인 (하위 호환) */ window.getSSOToken = function() { return cookieGet('sso_token') || localStorage.getItem('sso_token'); }; /** * SSO 사용자 정보 가져오기 (쿠키 우선, localStorage 폴백) */ window.getSSOUser = function() { var raw = cookieGet('sso_user') || localStorage.getItem('sso_user'); try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; } }; /** * 중앙 로그인 URL 반환 */ window.getLoginUrl = function() { var hostname = window.location.hostname; if (hostname.includes('technicalkorea.net')) { return window.location.protocol + '//tkfb.technicalkorea.net/dashboard?redirect=' + encodeURIComponent(window.location.href); } // 개발 환경: tkds 포트 (30780) return window.location.protocol + '//' + hostname + ':30780/dashboard?redirect=' + encodeURIComponent(window.location.href); }; /** * SSO 토큰 및 사용자 정보 삭제 */ window.clearSSOAuth = function() { cookieRemove('sso_token'); cookieRemove('sso_user'); cookieRemove('sso_refresh_token'); ['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) { localStorage.removeItem(k); }); }; // ==================== 보안 유틸리티 (XSS 방지) ==================== /** * HTML 특수문자 이스케이프 (XSS 방지) */ window.escapeHtml = function(str) { if (str === null || str === undefined) return ''; if (typeof str !== 'string') str = String(str); var htmlEntities = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=' }; return str.replace(/[&<>"'`=\/]/g, function(char) { return htmlEntities[char]; }); }; /** * URL 파라미터 이스케이프 */ window.escapeUrl = function(str) { if (str === null || str === undefined) return ''; return encodeURIComponent(String(str)); }; // ==================== API 설정 ==================== var API_PORT = 30005; var API_PATH = '/api'; function getApiBaseUrl() { var hostname = window.location.hostname; var protocol = window.location.protocol; // 프로덕션 환경 (technicalkorea.net 도메인) - 같은 도메인의 /api 경로 if (hostname.includes('technicalkorea.net')) { return protocol + '//' + hostname + API_PATH; } // 개발 환경 (localhost 또는 IP) return protocol + '//' + hostname + ':' + API_PORT + API_PATH; } // 전역 API 설정 var apiUrl = getApiBaseUrl(); window.API_BASE_URL = apiUrl; window.API = apiUrl; // 이전 호환성 // 인증 헤더 생성 (쿠키/localStorage에서 토큰 읽기) window.getAuthHeaders = function() { var token = window.getSSOToken(); return { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }; }; // API 호출 헬퍼 window.apiCall = async function(endpoint, method, data) { method = method || 'GET'; var url = window.API_BASE_URL + endpoint; var config = { method: method, headers: window.getAuthHeaders(), cache: 'no-store' }; if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) { config.body = JSON.stringify(data); } var response = await fetch(url, config); // 401 Unauthorized 처리 if (response.status === 401) { window.clearSSOAuth(); window.location.href = window.getLoginUrl() + '&logout=1'; throw new Error('인증이 만료되었습니다.'); } return response.json(); }; // ==================== 공통 유틸리티 ==================== /** * Toast 알림 표시 */ window.showToast = function(message, type, duration) { type = type || 'info'; duration = duration || 3000; var container = document.getElementById('toastContainer'); if (!container) { container = document.createElement('div'); container.id = 'toastContainer'; container.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:10px;'; document.body.appendChild(container); } if (!document.getElementById('toastStyles')) { var style = document.createElement('style'); style.id = 'toastStyles'; style.textContent = '.toast{display:flex;align-items:center;gap:12px;padding:12px 20px;background:#fff;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);opacity:0;transform:translateX(100px);transition:all .3s ease;min-width:250px;max-width:400px}' + '.toast.show{opacity:1;transform:translateX(0)}' + '.toast-success{border-left:4px solid #10b981}.toast-error{border-left:4px solid #ef4444}' + '.toast-warning{border-left:4px solid #f59e0b}.toast-info{border-left:4px solid #3b82f6}' + '.toast-icon{font-size:20px}.toast-message{font-size:14px;color:#374151}'; document.head.appendChild(style); } var iconMap = { success: '\u2705', error: '\u274C', warning: '\u26A0\uFE0F', info: '\u2139\uFE0F' }; var toast = document.createElement('div'); toast.className = 'toast toast-' + type; toast.innerHTML = '' + (iconMap[type] || '\u2139\uFE0F') + '' + escapeHtml(message) + ''; container.appendChild(toast); setTimeout(function() { toast.classList.add('show'); }, 10); setTimeout(function() { toast.classList.remove('show'); setTimeout(function() { toast.remove(); }, 300); }, duration); }; /** * 날짜를 YYYY-MM-DD 형식으로 변환 */ window.formatDate = function(dateString) { if (!dateString) return ''; if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) return dateString; var d = new Date(dateString); if (isNaN(d.getTime())) return ''; var y = d.getFullYear(); var m = String(d.getMonth() + 1).padStart(2, '0'); var day = String(d.getDate()).padStart(2, '0'); return y + '-' + m + '-' + day; }; /** * apiCall이 로드될 때까지 대기 */ window.waitForApi = function(timeout) { timeout = timeout || 5000; return new Promise(function(resolve, reject) { if (window.apiCall) return resolve(); var elapsed = 0; var iv = setInterval(function() { elapsed += 50; if (window.apiCall) { clearInterval(iv); resolve(); } else if (elapsed >= timeout) { clearInterval(iv); reject(new Error('apiCall timeout')); } }, 50); }); }; /** * UUID v4 생성 */ window.generateUUID = function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; console.log('API 설정 완료:', window.API_BASE_URL); })();