// api-config.js - nginx 프록시 대응 API 설정 import { config } from './config.js'; import { redirectToLogin } from './navigation.js'; function getApiBaseUrl() { const hostname = window.location.hostname; const protocol = window.location.protocol; const port = window.location.port; console.log('🌐 감지된 환경:', { hostname, protocol, port }); // 🔗 nginx 프록시를 통한 접근 (권장) // nginx가 /api/ 요청을 백엔드로 프록시하므로 포트 없이 접근 if (hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.') || hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local') || hostname.includes('hyungi')) { // 현재 웹서버의 도메인/IP를 그대로 사용하되 API 포트(config.api.port)로 직접 연결 const baseUrl = `${protocol}//${hostname}:${config.api.port}${config.api.path}`; console.log('✅ nginx 프록시 사용:', baseUrl); return baseUrl; } // 🚨 백업: 직접 접근 (nginx 프록시 실패시에만) console.warn('⚠️ 직접 API 접근 (백업 모드)'); return `${protocol}//${hostname}:${config.api.port}${config.api.path}`; } // API 설정 const API_URL = getApiBaseUrl(); // 전역 변수로 설정 window.API = API_URL; window.API_BASE_URL = API_URL; function ensureAuthenticated() { const token = localStorage.getItem('token'); if (!token || token === 'undefined' || token === 'null') { console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.'); clearAuthData(); // 만약을 위해 한번 더 정리 redirectToLogin(); return false; // 이후 코드 실행 방지 } // 토큰 만료 확인 if (isTokenExpired(token)) { console.log('🚨 토큰이 만료되었습니다. 로그인 페이지로 이동합니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); redirectToLogin(); return false; } return token; } // 토큰 만료 확인 함수 function isTokenExpired(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Math.floor(Date.now() / 1000); return payload.exp < currentTime; } catch (error) { console.error('토큰 파싱 오류:', error); return true; // 파싱 실패 시 만료된 것으로 간주 } } // 인증 데이터 정리 함수 function clearAuthData() { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('userInfo'); localStorage.removeItem('currentUser'); } function getAuthHeaders() { const token = localStorage.getItem('token'); return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }; } // 🔧 개선된 API 호출 함수 (에러 처리 강화) async function apiCall(url, method = 'GET', data = null) { // 상대 경로를 절대 경로로 변환 const fullUrl = url.startsWith('http') ? url : `${API}${url}`; const options = { method: method, headers: { 'Content-Type': 'application/json', ...getAuthHeaders() } }; // POST/PUT 요청시 데이터 추가 if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { options.body = JSON.stringify(data); } try { console.log(`📡 API 호출: ${fullUrl} (${method})`); const response = await fetch(fullUrl, options); // 인증 만료 처리 if (response.status === 401) { console.error('🚨 인증 실패: 토큰이 만료되었거나 유효하지 않습니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); redirectToLogin(); throw new Error('인증에 실패했습니다.'); } // 응답 실패 처리 if (!response.ok) { let errorMessage = `HTTP ${response.status}`; try { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const errorData = await response.json(); console.error('📋 서버 에러 상세:', errorData); // 에러 메시지 추출 (여러 형식 지원) if (typeof errorData === 'string') { errorMessage = errorData; } else if (errorData.error) { errorMessage = typeof errorData.error === 'string' ? errorData.error : JSON.stringify(errorData.error); } else if (errorData.message) { errorMessage = errorData.message; } else if (errorData.details) { errorMessage = errorData.details; } else { errorMessage = `HTTP ${response.status}: ${JSON.stringify(errorData)}`; } } else { const errorText = await response.text(); console.error('📋 서버 에러 텍스트:', errorText); errorMessage = errorText || errorMessage; } } catch (e) { console.error('📋 에러 파싱 중 예외 발생:', e.message); // 파싱 실패해도 HTTP 상태 코드는 전달 } throw new Error(errorMessage); } const result = await response.json(); console.log(`✅ API 성공: ${fullUrl}`); return result; } catch (error) { console.error(`❌ API 오류 (${fullUrl}):`, error); console.error('❌ 에러 전체 내용:', JSON.stringify(error, null, 2)); // 네트워크 오류 vs 서버 오류 구분 if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error('네트워크 연결 오류입니다. 인터넷 연결을 확인해주세요.'); } throw error; } } // 디버깅 정보 console.log('🔗 API Base URL:', API); console.log('🌐 Current Location:', { hostname: window.location.hostname, protocol: window.location.protocol, port: window.location.port, href: window.location.href }); // 🧪 API 연결 테스트 함수 (개발용) async function testApiConnection() { try { console.log('🧪 API 연결 테스트 시작...'); const response = await fetch(`${API}/health`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { console.log('✅ API 연결 성공!'); return true; } else { console.log('❌ API 연결 실패:', response.status); return false; } } catch (error) { console.log('❌ API 연결 오류:', error.message); return false; } } // API 헬퍼 함수들 async function apiGet(url) { return apiCall(url, 'GET'); } async function apiPost(url, data) { return apiCall(url, 'POST', data); } async function apiPut(url, data) { return apiCall(url, 'PUT', data); } async function apiDelete(url) { return apiCall(url, 'DELETE'); } // 전역 함수로 설정 window.ensureAuthenticated = ensureAuthenticated; window.getAuthHeaders = getAuthHeaders; window.apiCall = apiCall; window.apiGet = apiGet; window.apiPost = apiPost; window.apiPut = apiPut; window.apiDelete = apiDelete; window.testApiConnection = testApiConnection; window.isTokenExpired = isTokenExpired; window.clearAuthData = clearAuthData; // 개발 모드에서 자동 테스트 if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) { setTimeout(() => { testApiConnection(); }, 1000); } // 주기적으로 토큰 만료 확인 (5분마다) setInterval(() => { const token = localStorage.getItem('token'); if (token && isTokenExpired(token)) { console.log('🚨 주기적 확인: 토큰이 만료되었습니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); redirectToLogin(); } }, config.app.tokenRefreshInterval); // 5분마다 확인 // ES6 모듈 export export { API_URL as API_BASE_URL, API_URL as API, apiCall, getAuthHeaders };