ERP-node/frontend/hooks/queries/useCodes.ts

309 lines
11 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { commonCodeApi } from "@/lib/api/commonCode";
import { tableTypeApi } from "@/lib/api/screen";
import { useMemo } from "react";
// Query Keys
export const queryKeys = {
codes: {
all: ["codes"] as const,
list: (categoryCode: string) => ["codes", "list", categoryCode] as const,
options: (categoryCode: string) => ["codes", "options", categoryCode] as const,
detail: (categoryCode: string, codeValue: string) => ["codes", "detail", categoryCode, codeValue] as const,
infiniteList: (categoryCode: string, filters?: any) => ["codes", "infiniteList", categoryCode, filters] as const,
},
tables: {
all: ["tables"] as const,
columns: (tableName: string) => ["tables", "columns", tableName] as const,
codeCategory: (tableName: string, columnName: string) => ["tables", "codeCategory", tableName, columnName] as const,
},
categories: {
all: ["categories"] as const,
list: (filters?: any) => ["categories", "list", filters] as const,
},
};
// 테이블 컬럼의 코드 카테고리 조회
export function useTableCodeCategory(tableName?: string, columnName?: string) {
return useQuery({
queryKey: queryKeys.tables.codeCategory(tableName || "", columnName || ""),
queryFn: async () => {
if (!tableName || !columnName) return null;
const columns = await tableTypeApi.getColumns(tableName);
const targetColumn = columns.find((col) => col.columnName === columnName);
const codeCategory =
targetColumn?.codeCategory && targetColumn.codeCategory !== "none" ? targetColumn.codeCategory : null;
return codeCategory;
},
enabled: !!(tableName && columnName),
staleTime: 10 * 60 * 1000, // 10분 캐시
gcTime: 30 * 60 * 1000, // 30분 가비지 컬렉션
});
}
// 🆕 테이블 컬럼의 계층구조 설정 조회 (대분류/중분류/소분류)
interface ColumnHierarchyInfo {
hierarchyRole?: "large" | "medium" | "small";
hierarchyParentField?: string;
codeCategory?: string;
}
export function useTableColumnHierarchy(tableName?: string, columnName?: string) {
return useQuery<ColumnHierarchyInfo | null>({
queryKey: ["tables", "hierarchy", tableName || "", columnName || ""],
queryFn: async () => {
if (!tableName || !columnName) return null;
const columns = await tableTypeApi.getColumns(tableName);
const targetColumn = columns.find((col) => col.columnName === columnName);
if (!targetColumn) return null;
// detailSettings에서 hierarchyRole 추출
let hierarchyRole: ColumnHierarchyInfo["hierarchyRole"];
let hierarchyParentField: string | undefined;
if (targetColumn.detailSettings) {
try {
const settings =
typeof targetColumn.detailSettings === "string"
? JSON.parse(targetColumn.detailSettings)
: targetColumn.detailSettings;
hierarchyRole = settings.hierarchyRole;
hierarchyParentField = settings.hierarchyParentField;
} catch {
// JSON 파싱 실패 시 무시
}
}
// hierarchyParentField가 없으면 같은 codeCategory를 가진 상위 역할 컬럼을 자동으로 찾음
if (hierarchyRole && !hierarchyParentField && targetColumn.codeCategory) {
const roleOrder = { large: 0, medium: 1, small: 2 };
const currentOrder = roleOrder[hierarchyRole];
if (currentOrder > 0) {
// 같은 codeCategory를 가진 컬럼들 중에서 상위 역할을 찾음
const parentRole = currentOrder === 1 ? "large" : "medium";
const parentColumn = columns.find((col) => {
if (col.codeCategory !== targetColumn.codeCategory) return false;
try {
const colSettings =
typeof col.detailSettings === "string" ? JSON.parse(col.detailSettings) : col.detailSettings;
return colSettings?.hierarchyRole === parentRole;
} catch {
return false;
}
});
if (parentColumn) {
hierarchyParentField = parentColumn.columnName;
}
}
}
return {
hierarchyRole,
hierarchyParentField,
codeCategory: targetColumn.codeCategory,
};
},
enabled: !!(tableName && columnName),
staleTime: 10 * 60 * 1000,
gcTime: 30 * 60 * 1000,
});
}
// 코드 옵션 조회 (select용)
export function useCodeOptions(codeCategory?: string, enabled: boolean = true, menuObjid?: number) {
const query = useQuery({
queryKey: menuObjid
? [...queryKeys.codes.options(codeCategory || ""), "menu", menuObjid]
: queryKeys.codes.options(codeCategory || ""),
queryFn: async () => {
if (!codeCategory || codeCategory === "none") return [];
const response = await commonCodeApi.codes.getList(codeCategory, {
isActive: true,
menuObjid,
});
if (response.success && response.data) {
const options = response.data.map((code: any) => {
const actualValue = code.code || code.CODE || code.value || code.code_value || code.codeValue;
const actualLabel =
code.codeName ||
code.code_name ||
code.name ||
code.CODE_NAME ||
code.NAME ||
code.label ||
code.LABEL ||
code.text ||
code.title ||
code.description ||
actualValue;
// 계층구조 정보 포함
const depth = code.depth ?? 1;
const parentCodeValue = code.parentCodeValue || code.parent_code_value || null;
return {
value: actualValue,
label: actualLabel,
depth,
parentCodeValue,
};
});
return options;
}
return [];
},
enabled: enabled && !!(codeCategory && codeCategory !== "none"),
staleTime: 10 * 60 * 1000, // 10분 캐시
gcTime: 30 * 60 * 1000, // 30분 가비지 컬렉션
});
return {
options: query.data || [],
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch: query.refetch,
};
}
// 코드 생성
export function useCreateCode() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ categoryCode, data }: { categoryCode: string; data: any }) =>
commonCodeApi.codes.create(categoryCode, data),
onSuccess: (_, variables) => {
// 해당 카테고리의 모든 코드 관련 쿼리 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.all,
});
// 무한 스크롤 쿼리도 명시적으로 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.infiniteList(variables.categoryCode),
});
},
});
}
// 코드 수정
export function useUpdateCode() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ categoryCode, codeValue, data }: { categoryCode: string; codeValue: string; data: any }) =>
commonCodeApi.codes.update(categoryCode, codeValue, data),
onSuccess: (_, variables) => {
// 해당 코드 상세 쿼리 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue),
});
// 해당 카테고리의 모든 코드 관련 쿼리 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.all,
});
// 무한 스크롤 쿼리도 명시적으로 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.infiniteList(variables.categoryCode),
});
},
});
}
// 코드 삭제
export function useDeleteCode() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ categoryCode, codeValue }: { categoryCode: string; codeValue: string }) =>
commonCodeApi.codes.delete(categoryCode, codeValue),
onSuccess: (_, variables) => {
// 해당 코드 관련 쿼리 무효화 및 캐시 제거
queryClient.invalidateQueries({
queryKey: queryKeys.codes.all,
});
// 무한 스크롤 쿼리도 명시적으로 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.infiniteList(variables.categoryCode),
});
queryClient.removeQueries({
queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue),
});
},
});
}
// 코드 순서 변경
export function useReorderCodes() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
categoryCode,
codes,
}: {
categoryCode: string;
codes: Array<{ codeValue: string; sortOrder: number }>;
}) => commonCodeApi.codes.reorder(categoryCode, codes),
onMutate: async ({ categoryCode, codes }) => {
// 진행 중인 쿼리들을 취소해서 optimistic update가 덮어쓰이지 않도록 함
await queryClient.cancelQueries({ queryKey: queryKeys.codes.list(categoryCode) });
// 이전 데이터를 백업
const previousCodes = queryClient.getQueryData(queryKeys.codes.list(categoryCode));
// Optimistic update: 새로운 순서로 즉시 업데이트
if (previousCodes && Array.isArray((previousCodes as any).data)) {
const previousCodesArray = (previousCodes as any).data;
// 기존 데이터를 복사하고 sort_order만 업데이트
const updatedCodes = [...previousCodesArray].map((code: any) => {
const newCodeData = codes.find((c) => c.codeValue === code.code_value);
return newCodeData ? { ...code, sort_order: newCodeData.sortOrder } : code;
});
// 순서대로 정렬
updatedCodes.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
// API 응답 형태로 캐시에 저장 (기존 구조 유지)
queryClient.setQueryData(queryKeys.codes.list(categoryCode), {
...(previousCodes as any),
data: updatedCodes,
});
}
return { previousCodes };
},
onError: (err, variables, context) => {
// 에러 시 이전 데이터로 롤백
if (context?.previousCodes) {
queryClient.setQueryData(queryKeys.codes.list(variables.categoryCode), context.previousCodes);
}
},
onSettled: (_, __, variables) => {
// 성공/실패와 관계없이 최종적으로 서버 데이터로 동기화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.all,
});
// 무한 스크롤 쿼리도 명시적으로 무효화
queryClient.invalidateQueries({
queryKey: queryKeys.codes.infiniteList(variables.categoryCode),
});
},
});
}