스마트서비스 최종완료보고 버전

This commit is contained in:
CodeTempla
2026-05-19 20:58:19 +09:00
commit d26eb50875
405 changed files with 46755 additions and 0 deletions
+426
View File
@@ -0,0 +1,426 @@
// 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;
}
type AuthState = {
accessToken: string | null;
tokenPayload: JwtPayload | null;
setAccessToken: (token: string) => void;
clearAuth: () => void;
};
export const useAuthStore = create<AuthState>((set: any) => ({
accessToken: null,
tokenPayload: null,
setAccessToken: (token: string) => {
set({ accessToken: token, tokenPayload: jwtDecode<JwtPayload>(token) });
},
clearAuth: () => set({ accessToken: null, tokenPayload: null }),
}));
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<UiLoadingState>((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
let g_isRefreshing = false;
let g_queue_refresh_tok_value: {
resolve: (token: string) => void;
reject: (err: any) => void;
}[] = [];
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);
}
if (original._retry) {
return Promise.reject(error);
}
const status = error.response?.status;
if (status !== 401) {
return Promise.reject(error);
}
if (original.url === TOK_REFRESH_URI) {
useAuthStore.getState().clearAuth();
window.location.href = SIGN_IN_PAGE_URI;
return Promise.reject(error);
}
if (g_isRefreshing) {
return new Promise((resolve, reject) => {
g_queue_refresh_tok_value.push({ resolve, reject });
}).then((newToken) => {
original.headers = original.headers ?? {};
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
});
}
original._retry = true;
g_isRefreshing = true;
try {
const resp = await api.post(TOK_REFRESH_URI);
const newToken = resp.data.result.sval1;
useAuthStore.getState().setAccessToken(newToken);
g_queue_refresh_tok_value.forEach(({ resolve }) => resolve(newToken));
g_queue_refresh_tok_value = [];
original.headers = original.headers ?? {};
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
} catch (refreshError) {
g_queue_refresh_tok_value.forEach(({ reject }) => reject(refreshError));
g_queue_refresh_tok_value = [];
useAuthStore.getState().clearAuth();
window.location.href = SIGN_IN_PAGE_URI;
return Promise.reject(refreshError);
} finally {
g_isRefreshing = false;
}
}
);
export const clearAuthStore = async () => {
useAuthStore.getState().clearAuth();
};
export const refreshAuthStore = async () => {
if (useAuthStore.getState().tokenPayload != null)
return;
try {
const resp = await api.post(TOK_REFRESH_URI);
const newToken = resp.data.result.sval1;
useAuthStore.getState().setAccessToken(newToken);
}
catch (e) {}
};
//////////////////////////////////////////////////////////////////////////////
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']});
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);
});
}
// 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);
}