- 모바일 하단 네비: 메뉴 제거, 4개 핵심 기능(홈/TBM/작업보고/출근) SVG 아이콘 - 모바일 사이드바 스킵: 768px 이하에서 사이드바 미로드, 레이아웃 오프셋 해결 - 모바일 헤더: 햄버거 메뉴 숨김, 본문 margin/overflow 정리 - TBM 모바일: 풀스크린 모달, 저장 버튼 하단 고정, 터치 UX 개선 - PWA: manifest.json, sw.js(network-first), 앱 아이콘, iOS 메타태그, 킬스위치 - 로그인 무한루프 수정: 토큰 만료 검증, 쿠키 정리, loginPage 경로 수정 - 신고 메뉴 tkreport 리다이렉트: navbar + sidebar cross-system-link 적용 - TBM API: 작업장별 안전점검 체크리스트 조회 엔드포인트 추가 - 안전점검 체크리스트 관리 UI 개선 - tkuser: 이슈유형 관리 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
8.3 KiB
JavaScript
259 lines
8.3 KiB
JavaScript
// 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 });
|
|
|
|
// 🔗 외부 도메인 (Cloudflare Tunnel) - Gateway nginx가 /api/를 프록시
|
|
if (hostname.includes('technicalkorea.net')) {
|
|
const baseUrl = `${protocol}//${hostname}${config.api.path}`;
|
|
console.log('✅ Gateway 프록시 사용:', baseUrl);
|
|
return baseUrl;
|
|
}
|
|
|
|
// 🔗 로컬/내부 네트워크 - 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')) {
|
|
const baseUrl = `${protocol}//${hostname}:${config.api.port}${config.api.path}`;
|
|
console.log('✅ 로컬 직접 접근:', baseUrl);
|
|
return baseUrl;
|
|
}
|
|
|
|
// 🚨 기타: 포트 없이 상대 경로
|
|
const baseUrl = `${protocol}//${hostname}${config.api.path}`;
|
|
console.log('✅ 기본 프록시 사용:', baseUrl);
|
|
return baseUrl;
|
|
}
|
|
|
|
// API 설정
|
|
const API_URL = getApiBaseUrl();
|
|
|
|
// 전역 변수로 설정
|
|
window.API = API_URL;
|
|
window.API_BASE_URL = API_URL;
|
|
|
|
function ensureAuthenticated() {
|
|
const token = localStorage.getItem('sso_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('sso_token');
|
|
localStorage.removeItem('sso_user');
|
|
localStorage.removeItem('userInfo');
|
|
localStorage.removeItem('currentUser');
|
|
// SSO 쿠키도 삭제 (로그인 페이지 자동 리다이렉트 방지)
|
|
var cookieDomain = window.location.hostname.includes('technicalkorea.net')
|
|
? '; domain=.technicalkorea.net' : '';
|
|
document.cookie = 'sso_token=; path=/; max-age=0' + cookieDomain;
|
|
document.cookie = 'sso_user=; path=/; max-age=0' + cookieDomain;
|
|
document.cookie = 'sso_refresh_token=; path=/; max-age=0' + cookieDomain;
|
|
}
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('sso_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('sso_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 }; |