309 lines
11 KiB
TypeScript
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),
|
|
});
|
|
},
|
|
});
|
|
}
|