ERP-node/frontend/lib/api/aiAssistant/client.ts

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;