// App Global functions "use client"; import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { create } from "zustand"; //JWT import { jwtDecode } from "jwt-decode";//JWT const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, timeout: 7000, headers: { 'Content-Type': 'application/json', }, withCredentials: true, //JWT for access token }); export default api; export const api2 = axios.create({ baseURL: "http://localhost:17800", timeout: 7000, headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); //JWT access token part export const SIGN_IN_PAGE_URI = "/signin"; export const TOK_REFRESH_URI = "/api/sval2.do"; // security value 2(token refresh) type JwtPayload = { sub: string; // gid user_id: string; nick_name: string; biz_group_id: string; permission: number; } // "idle": not restored yet, "loading": restoring from the refresh cookie, // "authed": access token available, "guest": no valid session export type AuthStatus = "idle" | "loading" | "authed" | "guest"; type AuthState = { accessToken: string | null; tokenPayload: JwtPayload | null; authStatus: AuthStatus; setAccessToken: (token: string) => void; clearAuth: () => void; setAuthStatus: (status: AuthStatus) => void; }; export const useAuthStore = create((set: any) => ({ accessToken: null, tokenPayload: null, authStatus: "idle", setAccessToken: (token: string) => { set({ accessToken: token, tokenPayload: jwtDecode(token), authStatus: "authed" }); }, clearAuth: () => set({ accessToken: null, tokenPayload: null, authStatus: "guest" }), setAuthStatus: (status: AuthStatus) => set({ authStatus: status }), })); // The temporary ID and password typed on the sign in page, handed to the account change page. // Kept in memory only: a reload loses it and sends the user back to sign in. type InitAccountState = { user_id: string; user_pw: string; setInitAccount: (user_id: string, user_pw: string) => void; clearInitAccount: () => void; }; export const useInitAccountStore = create((set) => ({ user_id: "", user_pw: "", setInitAccount: (user_id: string, user_pw: string) => set({ user_id, user_pw }), clearInitAccount: () => set({ user_id: "", user_pw: "" }), })); type UiLoadingState = { pendingCount: number; treePendingCount: number; tablePendingCount: number; isLoading: boolean; isTreeLoading: boolean; isTableLoading: boolean; startLoading: (scope?: "tree" | "table" | "global") => void; endLoading: (scope?: "tree" | "table" | "global") => void; }; export const useUiLoadingStore = create((set, get) => ({ pendingCount: 0, treePendingCount: 0, tablePendingCount: 0, isLoading: false, isTreeLoading: false, isTableLoading: false, startLoading: (scope = "global") => { const pendingNext = get().pendingCount + 1; const treeNext = scope === "tree" ? get().treePendingCount + 1 : get().treePendingCount; const tableNext = scope === "table" ? get().tablePendingCount + 1 : get().tablePendingCount; set({ pendingCount: pendingNext, treePendingCount: treeNext, tablePendingCount: tableNext, isLoading: pendingNext > 0, isTreeLoading: treeNext > 0, isTableLoading: tableNext > 0, }); }, endLoading: (scope = "global") => { const pendingNext = Math.max(0, get().pendingCount - 1); const treeNext = scope === "tree" ? Math.max(0, get().treePendingCount - 1) : get().treePendingCount; const tableNext = scope === "table" ? Math.max(0, get().tablePendingCount - 1) : get().tablePendingCount; set({ pendingCount: pendingNext, treePendingCount: treeNext, tablePendingCount: tableNext, isLoading: pendingNext > 0, isTreeLoading: treeNext > 0, isTableLoading: tableNext > 0, }); }, })); const TREE_LOADING_ENDPOINTS = [ "/api/get-biz-group2.do", "/api/get-biz-group3.do", ]; const TABLE_LOADING_ENDPOINTS = [ "/api/get-account.do", "/api/get-device.do", "/api/get-goods.do", "/api/get-goods-templates.do", "/api/get-terminal.do", "/api/get-transaction.do", "/api/get-u-transaction.do", "/api/get-sales.do", "/api/get-notice.do", "/api/get-voc.do", "/api/get-resource-board.do", ]; const resolveLoadingScope = (url?: string): "tree" | "table" | "global" => { if (!url) return "global"; if (TREE_LOADING_ENDPOINTS.some((x) => url.includes(x))) return "tree"; if (TABLE_LOADING_ENDPOINTS.some((x) => url.includes(x))) return "table"; return "global"; }; api.interceptors.request.use((config) => { const token = useAuthStore.getState().accessToken; if (token) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${token}`; } const loadingScope = resolveLoadingScope(config.url); (config as AxiosRequestConfig & { __loadingScope?: "tree" | "table" | "global" }).__loadingScope = loadingScope; useUiLoadingStore.getState().startLoading(loadingScope); return config; }); // refresh token part // A single in-flight refresh call is shared by the 401 retry path and by the // startup restore, so concurrent 401s never hit /api/sval2.do more than once. let g_refreshPromise: Promise | null = null; const requestNewAccessToken = async (): Promise => { const resp = await api.post(TOK_REFRESH_URI); // The refresh endpoint answers 200 with an error code when the account is gone or stopped const newToken = resp?.data?.result?.sval1; if (resp?.data?.errCode != 0 || !newToken) throw new Error("token refresh failed, errCode: " + resp?.data?.errCode); useAuthStore.getState().setAccessToken(newToken); return newToken; }; export const refreshAccessToken = (): Promise => { if (g_refreshPromise == null) { g_refreshPromise = requestNewAccessToken().finally(() => { g_refreshPromise = null; }); } return g_refreshPromise; }; api.interceptors.response.use( (response) => { const loadingScope = (response.config as AxiosRequestConfig & { __loadingScope?: "tree" | "table" | "global" }).__loadingScope ?? resolveLoadingScope(response.config?.url); useUiLoadingStore.getState().endLoading(loadingScope); return response; }, async (error: AxiosError) => { const loadingScope = (error.config as AxiosRequestConfig & { __loadingScope?: "tree" | "table" | "global" } | undefined)?.__loadingScope ?? resolveLoadingScope(error.config?.url); useUiLoadingStore.getState().endLoading(loadingScope); const original = error.config as AxiosRequestConfig & { _retry?: boolean }; if (!original) { return Promise.reject(error); } const status = error.response?.status; if (status !== 401) { return Promise.reject(error); } // The refresh call itself failed: drop the session and let the caller decide where to go if (original.url === TOK_REFRESH_URI) { useAuthStore.getState().clearAuth(); return Promise.reject(error); } // Already retried once with a fresh token, do not loop if (original._retry) { return Promise.reject(error); } original._retry = true; try { const newToken = await refreshAccessToken(); original.headers = original.headers ?? {}; original.headers.Authorization = `Bearer ${newToken}`; return api(original); } catch (refreshError) { useAuthStore.getState().clearAuth(); window.location.href = SIGN_IN_PAGE_URI; return Promise.reject(refreshError); } } ); export const clearAuthStore = async () => { useAuthStore.getState().clearAuth(); }; /** * Restores the session from the sval2 refresh cookie on a full page load. * Returns true when an access token is available afterwards. */ export const bootstrapAuth = async (): Promise => { if (useAuthStore.getState().accessToken != null) return true; useAuthStore.getState().setAuthStatus("loading"); try { await refreshAccessToken(); return true; } catch (e) { useAuthStore.getState().clearAuth(); return false; } }; export const refreshAuthStore = async () => { await bootstrapAuth(); }; ////////////////////////////////////////////////////////////////////////////// export const postFileDownload = (url: string, params: any) => { api.post(url, params, {responseType: 'blob'}).then((resp) => { const disposition = resp.headers['content-disposition']; let filename : string = ""; if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, ''); } const blob = new Blob([resp.data], {type: resp.headers['content-type'] as string | undefined}); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); //link.download = filename; link.download = decodeURIComponent(filename); //link.download = unespace(filename); link.click(); }).catch((err) => { console.log(err); alert(err.message); }); } // YYYY-MM-DD export const todayYMD = () => { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } export const addDaysYMD = (days: number) => { const now = new Date(); const d = new Date(now); d.setDate(d.getDate() + days); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } // DB TIMESTAMP DATETIME modify (yyyy-MM-dd HH:mm:ss.0 -> yyyy-MM-dd HH:mm) export const convertDateTime = (tsDateTime: string) => { if (tsDateTime == undefined) return ""; return tsDateTime.replace(".0", ""); } // DB TIMESTAMP DATETIME modify (UTC time -> yyyy-MM-dd HH:mm:ss) export const convertDateTime2 = (tsDateTime: string) => { if (tsDateTime == undefined) return ""; const localDate = new Date(tsDateTime); const formatted = localDate.toLocaleString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, // 0~23hour }); return formatted; } // DB TIMESTAMP DATETIME modify (UTC time -> yyyy-MM-dd HH:mm) export const convertDateTime3 = (tsDateTime: string) => { if (tsDateTime == undefined || tsDateTime == "") return ""; const localDate = new Date(tsDateTime); const formatted = localDate.toLocaleString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", //second: "2-digit", hour12: false, // 0~23hour }); return formatted; } // yyyyMMddHHmmss => yyyy-MM-dd HH:mm:ss export const convertDateTime4 = (tsDateTime: string) => { if (tsDateTime == undefined || tsDateTime == "") return ""; const yyyy = tsDateTime.slice(0, 4); const mm = tsDateTime.slice(4, 6); const dd = tsDateTime.slice(6, 8); const HH = tsDateTime.slice(8, 10); const MM = tsDateTime.slice(10, 12); const SS = tsDateTime.slice(12, 14); return `${yyyy}-${mm}-${dd} ${HH}:${MM}:${SS}`; } // yyyyMMddHHmmss => yyyy-MM-dd HH:mm export const convertDateTime5 = (tsDateTime: string) => { if (tsDateTime == undefined || tsDateTime == "") return ""; const yyyy = tsDateTime.slice(0, 4); const mm = tsDateTime.slice(4, 6); const dd = tsDateTime.slice(6, 8); const HH = tsDateTime.slice(8, 10); const MM = tsDateTime.slice(10, 12); return `${yyyy}-${mm}-${dd} ${HH}:${MM}`; } // yyyyMMddHHmmss => yyyy-MM-dd HH:mm export const convertDateTime6 = (tsDateTime: string) => { if (tsDateTime == undefined || tsDateTime == "") return ""; let startPos = 0; let yy = tsDateTime.slice(startPos, startPos + 2); if (yy == "20") { startPos += 2; yy = tsDateTime.slice(startPos, startPos + 2); } startPos += 2; const mm = tsDateTime.slice(startPos, startPos + 2); startPos += 2; const dd = tsDateTime.slice(startPos, startPos + 2); startPos += 2; const HH = tsDateTime.slice(startPos, startPos + 2); startPos += 2; const MM = tsDateTime.slice(startPos, startPos + 2); return `20${yy}-${mm}-${dd} ${HH}:${MM}`; } export const getUrlParam = (key: string) => { if (window == undefined) return ""; const params = new URLSearchParams(window.location.search); const value = params.get(key); if (value == null) return ""; return value; } // 사업자등록번호 하이픈(-) 자동삽입 //const formatted = formatBusinessNumber("1234567890"); // "123-45-67890" export function formatBusinessNumber(input: string): string { const allowed = input.replace(/[^0-9-]/g, ""); const digits = allowed.replace(/-/g, "").substring(0, 10); let formatted = digits; if (digits.length > 3 && digits.length <= 5) { formatted = digits.replace(/^(\d{3})(\d{1,2})$/, "$1-$2"); } else if (digits.length > 5) { formatted = digits.replace(/^(\d{3})(\d{2})(\d{1,5})$/, "$1-$2-$3"); } if (allowed.endsWith("-") && !formatted.endsWith("-")) { formatted += "-"; } return formatted; } // 사업자등록번호 유효성 검사 //console.log(isValidBusinessNumber("123-45-67890")); // false //console.log(isValidBusinessNumber("220-81-62517")); // true (실제 유효 번호 예시) export function isValidBusinessNumber(input: string): boolean { //return true; const digits = input.replace(/\D/g, ""); if (digits.length !== 10) return false; const weights = [1, 3, 7, 1, 3, 7, 1, 3, 5]; let sum = 0; for (let i = 0; i < 9; i++) { const num = parseInt(digits.charAt(i), 10); sum += num * weights[i]; } sum += Math.floor((parseInt(digits.charAt(8), 10) * 5) / 10); const checkDigit = (10 - (sum % 10)) % 10; return checkDigit === parseInt(digits.charAt(9), 10); }