2025-08-21 09:41:46 +09:00
|
|
|
import axios, { AxiosResponse, AxiosError } from "axios";
|
|
|
|
|
|
2025-09-04 15:39:29 +09:00
|
|
|
// API URL 동적 설정 - 환경별 명확한 분리
|
2025-09-04 15:18:25 +09:00
|
|
|
const getApiBaseUrl = (): string => {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
const currentHost = window.location.hostname;
|
2025-09-04 16:01:33 +09:00
|
|
|
const currentPort = window.location.port;
|
2025-09-04 15:18:25 +09:00
|
|
|
|
2025-09-09 18:42:01 +09:00
|
|
|
// 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080
|
2025-09-16 16:16:41 +09:00
|
|
|
if (
|
|
|
|
|
(currentHost === "localhost" || currentHost === "127.0.0.1") &&
|
|
|
|
|
(currentPort === "9771" || currentPort === "3000")
|
|
|
|
|
) {
|
2025-09-04 15:18:25 +09:00
|
|
|
return "http://localhost:8080/api";
|
|
|
|
|
}
|
2025-09-04 15:39:29 +09:00
|
|
|
|
2025-09-04 16:10:26 +09:00
|
|
|
// 서버 환경에서 localhost:5555 → 39.117.244.52:8080
|
|
|
|
|
if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "5555") {
|
|
|
|
|
return "http://39.117.244.52:8080/api";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기타 서버 환경 (내부/외부 IP): → 39.117.244.52:8080
|
2025-09-04 15:39:29 +09:00
|
|
|
return "http://39.117.244.52:8080/api";
|
2025-09-04 15:18:25 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 15:39:29 +09:00
|
|
|
// 서버 사이드 렌더링 기본값
|
2025-09-04 15:27:30 +09:00
|
|
|
return "http://39.117.244.52:8080/api";
|
2025-09-04 15:18:25 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-04 15:39:29 +09:00
|
|
|
export const API_BASE_URL = getApiBaseUrl();
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// JWT 토큰 관리 유틸리티
|
|
|
|
|
const TokenManager = {
|
|
|
|
|
getToken: (): string | null => {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
return localStorage.getItem("authToken");
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
isTokenExpired: (token: string): boolean => {
|
|
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
|
|
|
return payload.exp * 1000 < Date.now();
|
|
|
|
|
} catch {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-04 15:39:29 +09:00
|
|
|
// Axios 인스턴스 생성
|
2025-08-21 09:41:46 +09:00
|
|
|
export const apiClient = axios.create({
|
2025-09-04 15:39:29 +09:00
|
|
|
baseURL: API_BASE_URL,
|
2025-09-24 18:23:57 +09:00
|
|
|
timeout: 30000, // 30초로 증가 (다중 커넥션 처리 시간 고려)
|
2025-08-21 09:41:46 +09:00
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
withCredentials: true, // 쿠키 포함
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 요청 인터셉터
|
|
|
|
|
apiClient.interceptors.request.use(
|
|
|
|
|
(config) => {
|
|
|
|
|
// JWT 토큰 추가
|
|
|
|
|
const token = TokenManager.getToken();
|
2025-08-21 13:28:49 +09:00
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
if (token && !TokenManager.isTokenExpired(token)) {
|
|
|
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
} else if (token && TokenManager.isTokenExpired(token)) {
|
2025-08-21 13:28:49 +09:00
|
|
|
console.warn("❌ 토큰이 만료되었습니다.");
|
2025-08-21 09:41:46 +09:00
|
|
|
// 토큰 제거
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
localStorage.removeItem("authToken");
|
|
|
|
|
}
|
2025-08-21 13:28:49 +09:00
|
|
|
} else {
|
|
|
|
|
console.warn("⚠️ 토큰이 없습니다.");
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
// FormData 요청 시 Content-Type 자동 처리
|
|
|
|
|
if (config.data instanceof FormData) {
|
|
|
|
|
delete config.headers["Content-Type"];
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-25 17:22:20 +09:00
|
|
|
// 언어 정보를 쿼리 파라미터에 추가 (GET 요청 시에만)
|
2025-08-21 09:41:46 +09:00
|
|
|
if (config.method?.toUpperCase() === "GET") {
|
2025-08-26 18:33:04 +09:00
|
|
|
// 우선순위: 전역 변수 > localStorage > 기본값
|
|
|
|
|
let currentLang = "KR"; // 기본값
|
|
|
|
|
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
// 1순위: 전역 변수에서 확인
|
2025-09-03 11:20:43 +09:00
|
|
|
if ((window as unknown as { __GLOBAL_USER_LANG?: string }).__GLOBAL_USER_LANG) {
|
|
|
|
|
currentLang = (window as unknown as { __GLOBAL_USER_LANG: string }).__GLOBAL_USER_LANG;
|
2025-08-26 18:33:04 +09:00
|
|
|
}
|
|
|
|
|
// 2순위: localStorage에서 확인 (새 창이나 페이지 새로고침 시)
|
|
|
|
|
else {
|
|
|
|
|
const storedLocale = localStorage.getItem("userLocale");
|
|
|
|
|
if (storedLocale) {
|
|
|
|
|
currentLang = storedLocale;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
if (config.params) {
|
|
|
|
|
config.params.userLang = currentLang;
|
|
|
|
|
} else {
|
|
|
|
|
config.params = { userLang: currentLang };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
2025-08-21 13:28:49 +09:00
|
|
|
console.error("❌ API 요청 오류:", error);
|
2025-08-21 09:41:46 +09:00
|
|
|
return Promise.reject(error);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 응답 인터셉터
|
|
|
|
|
apiClient.interceptors.response.use(
|
|
|
|
|
(response: AxiosResponse) => {
|
|
|
|
|
return response;
|
|
|
|
|
},
|
|
|
|
|
(error: AxiosError) => {
|
2025-09-03 11:20:43 +09:00
|
|
|
const status = error.response?.status;
|
|
|
|
|
const url = error.config?.url;
|
|
|
|
|
|
|
|
|
|
// 409 에러 (중복 데이터)는 조용하게 처리
|
|
|
|
|
if (status === 409) {
|
2025-09-19 15:47:35 +09:00
|
|
|
// 중복 검사 API와 관계도 저장은 완전히 조용하게 처리
|
|
|
|
|
if (url?.includes("/check-duplicate") || url?.includes("/dataflow-diagrams")) {
|
|
|
|
|
// 중복 검사와 관계도 중복 이름은 정상적인 비즈니스 로직이므로 콘솔 출력 없음
|
2025-09-03 11:20:43 +09:00
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 일반 409 에러는 간단한 로그만 출력
|
|
|
|
|
console.warn("⚠️ 데이터 중복:", {
|
|
|
|
|
url: url,
|
|
|
|
|
message: (error.response?.data as { message?: string })?.message || "중복된 데이터입니다.",
|
|
|
|
|
});
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 다른 에러들은 기존처럼 상세 로그 출력
|
2025-08-21 13:28:49 +09:00
|
|
|
console.error("❌ API 응답 오류:", {
|
2025-09-03 11:20:43 +09:00
|
|
|
status: status,
|
2025-08-21 09:41:46 +09:00
|
|
|
statusText: error.response?.statusText,
|
2025-09-03 11:20:43 +09:00
|
|
|
url: url,
|
2025-08-21 09:41:46 +09:00
|
|
|
data: error.response?.data,
|
|
|
|
|
message: error.message,
|
2025-08-21 13:28:49 +09:00
|
|
|
headers: error.config?.headers,
|
2025-08-21 09:41:46 +09:00
|
|
|
});
|
|
|
|
|
|
2025-08-21 13:28:49 +09:00
|
|
|
// 401 에러 시 상세 정보 출력
|
2025-09-03 11:20:43 +09:00
|
|
|
if (status === 401) {
|
2025-08-21 13:28:49 +09:00
|
|
|
console.error("🚨 401 Unauthorized 오류 상세 정보:", {
|
2025-09-03 11:20:43 +09:00
|
|
|
url: url,
|
2025-08-21 13:28:49 +09:00
|
|
|
method: error.config?.method,
|
|
|
|
|
headers: error.config?.headers,
|
|
|
|
|
requestData: error.config?.data,
|
|
|
|
|
responseData: error.response?.data,
|
|
|
|
|
token: TokenManager.getToken() ? "존재" : "없음",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 401 에러 시 토큰 제거 및 로그인 페이지로 리다이렉트
|
2025-09-03 11:20:43 +09:00
|
|
|
if (status === 401 && typeof window !== "undefined") {
|
2025-08-21 09:41:46 +09:00
|
|
|
localStorage.removeItem("authToken");
|
|
|
|
|
|
|
|
|
|
// 로그인 페이지가 아닌 경우에만 리다이렉트
|
|
|
|
|
if (window.location.pathname !== "/login") {
|
|
|
|
|
window.location.href = "/login";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 공통 응답 타입
|
2025-09-03 11:20:43 +09:00
|
|
|
export interface ApiResponse<T = unknown> {
|
2025-08-21 09:41:46 +09:00
|
|
|
success: boolean;
|
|
|
|
|
data?: T;
|
|
|
|
|
message?: string;
|
|
|
|
|
errorCode?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
// 사용자 정보 타입
|
|
|
|
|
export interface UserInfo {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
deptName?: string;
|
|
|
|
|
companyCode?: string;
|
|
|
|
|
userType?: string;
|
|
|
|
|
userTypeName?: string;
|
|
|
|
|
email?: string;
|
|
|
|
|
photo?: string;
|
|
|
|
|
locale?: string;
|
|
|
|
|
isAdmin?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 사용자 정보 조회
|
|
|
|
|
export const getCurrentUser = async (): Promise<ApiResponse<UserInfo>> => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get("/auth/me");
|
|
|
|
|
return response.data;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error("현재 사용자 정보 조회 실패:", error);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: error.response?.data?.message || error.message || "사용자 정보를 가져올 수 없습니다.",
|
|
|
|
|
errorCode: error.response?.data?.errorCode,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// API 호출 헬퍼 함수
|
|
|
|
|
export const apiCall = async <T>(
|
|
|
|
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
|
|
|
url: string,
|
2025-09-03 11:20:43 +09:00
|
|
|
data?: unknown,
|
2025-08-21 09:41:46 +09:00
|
|
|
): Promise<ApiResponse<T>> => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.request({
|
|
|
|
|
method,
|
|
|
|
|
url,
|
|
|
|
|
data,
|
|
|
|
|
});
|
|
|
|
|
return response.data;
|
2025-09-03 11:20:43 +09:00
|
|
|
} catch (error: unknown) {
|
2025-08-21 09:41:46 +09:00
|
|
|
console.error("API 호출 실패:", error);
|
2025-09-03 11:20:43 +09:00
|
|
|
const axiosError = error as AxiosError;
|
2025-08-21 09:41:46 +09:00
|
|
|
return {
|
|
|
|
|
success: false,
|
2025-09-03 11:20:43 +09:00
|
|
|
message:
|
|
|
|
|
(axiosError.response?.data as { message?: string })?.message ||
|
|
|
|
|
axiosError.message ||
|
|
|
|
|
"알 수 없는 오류가 발생했습니다.",
|
|
|
|
|
errorCode: (axiosError.response?.data as { errorCode?: string })?.errorCode,
|
2025-08-21 09:41:46 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|