/** * AI 어시스턴트 전용 API 클라이언트 * - VEXPLOR와 같은 서비스/같은 포트: /api/ai/v1 로 호출 (Next → backend-node → AI 서비스 프록시) * - 인증 토큰은 sessionStorage 'ai-assistant-auth' 사용 (VEXPLOR 인증과 분리) */ import axios, { AxiosError } from "axios"; import type { AiAssistantAuthState } from "./types"; const STORAGE_KEY = "ai-assistant-auth"; /** 같은 오리진 기준 AI API prefix (backend-node가 /api/ai/v1 을 AI 서비스로 프록시) */ function getBaseUrl(): string { if (typeof window === "undefined") return ""; return "/api/ai/v1"; } export function getAiAssistantAuth(): AiAssistantAuthState | null { if (typeof window === "undefined") return null; try { const raw = sessionStorage.getItem(STORAGE_KEY); return raw ? (JSON.parse(raw) as AiAssistantAuthState) : null; } catch { return null; } } export function setAiAssistantAuth(state: AiAssistantAuthState | null): void { if (typeof window === "undefined") return; if (state) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); else sessionStorage.removeItem(STORAGE_KEY); } export function getAiAssistantAccessToken(): string | null { return getAiAssistantAuth()?.accessToken ?? null; } let refreshing = false; const queue: Array<{ resolve: (t: string) => void; reject: (e: unknown) => void }> = []; function processQueue(error: unknown, token: string | null) { queue.forEach((p) => (error ? p.reject(error) : p.resolve(token!))); queue.length = 0; } const client = axios.create({ baseURL: "", headers: { "Content-Type": "application/json" }, }); client.interceptors.request.use((config) => { const base = getBaseUrl(); if (base) config.baseURL = base; const token = getAiAssistantAccessToken(); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); client.interceptors.response.use( (res) => res, async (err: AxiosError) => { const original = err.config as typeof err.config & { _retry?: boolean }; const isAuth = original?.url?.includes("/auth/"); if (isAuth || err.response?.status !== 401 || original?._retry) { return Promise.reject(err); } if (refreshing) { return new Promise((resolve, reject) => { queue.push({ resolve, reject }); }) .then((token) => { if (original.headers) original.headers.Authorization = `Bearer ${token}`; return client(original); }) .catch((e) => Promise.reject(e)); } original._retry = true; refreshing = true; const auth = getAiAssistantAuth(); try { if (!auth?.refreshToken) throw new Error("No refresh token"); const base = getBaseUrl(); const { data } = await axios.post<{ data: { accessToken: string } }>( `${base}/auth/refresh`, { refreshToken: auth.refreshToken }, { headers: { "Content-Type": "application/json" } }, ); const newToken = data?.data?.accessToken; if (!newToken) throw new Error("No access token"); const newState: AiAssistantAuthState = { ...auth, accessToken: newToken, }; setAiAssistantAuth(newState); processQueue(null, newToken); if (original.headers) original.headers.Authorization = `Bearer ${newToken}`; return client(original); } catch (e) { processQueue(e, null); setAiAssistantAuth(null); return Promise.reject(err); } finally { refreshing = false; } }, ); /** AI 어시스턴트 로그인 (이메일/비밀번호) */ export async function loginAiAssistant( email: string, password: string, ): Promise { const base = getBaseUrl(); const { data } = await axios.post<{ data: { user: AiAssistantAuthState["user"]; accessToken: string; refreshToken: string }; }>(`${base}/auth/login`, { email, password }, { headers: { "Content-Type": "application/json" }, withCredentials: true }); const state: AiAssistantAuthState = { user: data.data.user, accessToken: data.data.accessToken, refreshToken: data.data.refreshToken, }; setAiAssistantAuth(state); return state; } export default client;