128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string>((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<AiAssistantAuthState> {
|
||
|
|
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;
|