템플릿관리, 컴포넌트 관리
This commit is contained in:
228
frontend/hooks/admin/useComponents.ts
Normal file
228
frontend/hooks/admin/useComponents.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
// 컴포넌트 표준 타입 정의
|
||||
export interface ComponentStandard {
|
||||
component_code: string;
|
||||
component_name: string;
|
||||
component_name_eng?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
icon_name?: string;
|
||||
default_size?: { width: number; height: number };
|
||||
component_config: any;
|
||||
preview_image?: string;
|
||||
sort_order?: number;
|
||||
is_active?: string;
|
||||
is_public?: string;
|
||||
company_code: string;
|
||||
created_date?: string;
|
||||
created_by?: string;
|
||||
updated_date?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface ComponentQueryParams {
|
||||
category?: string;
|
||||
active?: string;
|
||||
is_public?: string;
|
||||
search?: string;
|
||||
sort?: string;
|
||||
order?: "asc" | "desc";
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface ComponentListResponse {
|
||||
components: ComponentStandard[];
|
||||
total: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// API 함수들
|
||||
const componentApi = {
|
||||
// 컴포넌트 목록 조회
|
||||
getComponents: async (params: ComponentQueryParams = {}): Promise<ComponentListResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== "") {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const response = await apiClient.get<ApiResponse<ComponentListResponse>>(
|
||||
`/admin/component-standards?${searchParams.toString()}`,
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 컴포넌트 상세 조회
|
||||
getComponent: async (component_code: string): Promise<ComponentStandard> => {
|
||||
const response = await apiClient.get<ApiResponse<ComponentStandard>>(
|
||||
`/admin/component-standards/${component_code}`,
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 컴포넌트 생성
|
||||
createComponent: async (data: Partial<ComponentStandard>): Promise<ComponentStandard> => {
|
||||
const response = await apiClient.post<ApiResponse<ComponentStandard>>("/admin/component-standards", data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 컴포넌트 수정
|
||||
updateComponent: async (component_code: string, data: Partial<ComponentStandard>): Promise<ComponentStandard> => {
|
||||
const response = await apiClient.put<ApiResponse<ComponentStandard>>(
|
||||
`/admin/component-standards/${component_code}`,
|
||||
data,
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 컴포넌트 삭제
|
||||
deleteComponent: async (component_code: string): Promise<void> => {
|
||||
await apiClient.delete(`/admin/component-standards/${component_code}`);
|
||||
},
|
||||
|
||||
// 정렬 순서 업데이트
|
||||
updateSortOrder: async (updates: Array<{ component_code: string; sort_order: number }>): Promise<void> => {
|
||||
await apiClient.put("/admin/component-standards/sort/order", { updates });
|
||||
},
|
||||
|
||||
// 컴포넌트 복제
|
||||
duplicateComponent: async (data: {
|
||||
source_code: string;
|
||||
new_code: string;
|
||||
new_name: string;
|
||||
}): Promise<ComponentStandard> => {
|
||||
const response = await apiClient.post<ApiResponse<ComponentStandard>>("/admin/component-standards/duplicate", data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 카테고리 목록 조회
|
||||
getCategories: async (): Promise<string[]> => {
|
||||
const response = await apiClient.get<ApiResponse<string[]>>("/admin/component-standards/categories");
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 통계 조회
|
||||
getStatistics: async (): Promise<{
|
||||
total: number;
|
||||
byCategory: Array<{ category: string; count: number }>;
|
||||
byStatus: Array<{ status: string; count: number }>;
|
||||
}> => {
|
||||
const response = await apiClient.get<ApiResponse<any>>("/admin/component-standards/statistics");
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
// React Query 훅들
|
||||
export const useComponents = (params: ComponentQueryParams = {}) => {
|
||||
return useQuery({
|
||||
queryKey: ["components", params],
|
||||
queryFn: () => componentApi.getComponents(params),
|
||||
staleTime: 5 * 60 * 1000, // 5분
|
||||
});
|
||||
};
|
||||
|
||||
export const useComponent = (component_code: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["component", component_code],
|
||||
queryFn: () => componentApi.getComponent(component_code),
|
||||
enabled: !!component_code,
|
||||
});
|
||||
};
|
||||
|
||||
export const useComponentCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: ["component-categories"],
|
||||
queryFn: componentApi.getCategories,
|
||||
staleTime: 10 * 60 * 1000, // 10분
|
||||
});
|
||||
};
|
||||
|
||||
export const useComponentStatistics = () => {
|
||||
return useQuery({
|
||||
queryKey: ["component-statistics"],
|
||||
queryFn: componentApi.getStatistics,
|
||||
staleTime: 2 * 60 * 1000, // 2분
|
||||
});
|
||||
};
|
||||
|
||||
// Mutation 훅들
|
||||
export const useCreateComponent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: componentApi.createComponent,
|
||||
onSuccess: () => {
|
||||
// 컴포넌트 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["components"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-categories"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-statistics"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateComponent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ component_code, data }: { component_code: string; data: Partial<ComponentStandard> }) =>
|
||||
componentApi.updateComponent(component_code, data),
|
||||
onSuccess: (data, variables) => {
|
||||
// 특정 컴포넌트와 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["component", variables.component_code] });
|
||||
queryClient.invalidateQueries({ queryKey: ["components"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-statistics"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteComponent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: componentApi.deleteComponent,
|
||||
onSuccess: () => {
|
||||
// 컴포넌트 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["components"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-categories"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-statistics"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSortOrder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: componentApi.updateSortOrder,
|
||||
onSuccess: () => {
|
||||
// 컴포넌트 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["components"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDuplicateComponent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: componentApi.duplicateComponent,
|
||||
onSuccess: () => {
|
||||
// 컴포넌트 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["components"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["component-statistics"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
376
frontend/hooks/admin/useTemplates.ts
Normal file
376
frontend/hooks/admin/useTemplates.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
// 템플릿 데이터 인터페이스
|
||||
export interface TemplateStandard {
|
||||
template_code: string;
|
||||
template_name: string;
|
||||
template_name_eng?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
icon_name?: string;
|
||||
default_size?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
layout_config: any; // 템플릿의 컴포넌트 구조
|
||||
preview_image?: string;
|
||||
sort_order?: number;
|
||||
is_active: string;
|
||||
is_public?: string;
|
||||
company_code: string;
|
||||
created_date?: string;
|
||||
created_by?: string;
|
||||
updated_date?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
// 템플릿 생성/수정 데이터
|
||||
export interface TemplateFormData {
|
||||
template_code: string;
|
||||
template_name: string;
|
||||
template_name_eng?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
icon_name?: string;
|
||||
default_size?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
layout_config: any;
|
||||
preview_image?: string;
|
||||
sort_order?: number;
|
||||
is_active?: string;
|
||||
is_public?: string;
|
||||
}
|
||||
|
||||
// API 응답 인터페이스
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
pagination?: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 쿼리 파라미터 인터페이스
|
||||
interface TemplateQueryParams {
|
||||
active?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
company_code?: string;
|
||||
is_public?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 관리 훅
|
||||
*/
|
||||
export const useTemplates = (params?: TemplateQueryParams) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// 템플릿 목록 조회
|
||||
const {
|
||||
data: templatesData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["templates", params],
|
||||
queryFn: async (): Promise<{ templates: TemplateStandard[]; pagination: any }> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.active) searchParams.append("active", params.active);
|
||||
if (params?.category) searchParams.append("category", params.category);
|
||||
if (params?.search) searchParams.append("search", params.search);
|
||||
if (params?.company_code) searchParams.append("company_code", params.company_code);
|
||||
if (params?.is_public) searchParams.append("is_public", params.is_public);
|
||||
if (params?.page) searchParams.append("page", params.page.toString());
|
||||
if (params?.limit) searchParams.append("limit", params.limit.toString());
|
||||
|
||||
const response = await apiClient.get(`/admin/template-standards?${searchParams.toString()}`);
|
||||
|
||||
const result: ApiResponse<TemplateStandard[]> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to fetch templates");
|
||||
}
|
||||
|
||||
return {
|
||||
templates: result.data || [],
|
||||
pagination: result.pagination,
|
||||
};
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5분간 캐시 유지
|
||||
cacheTime: 10 * 60 * 1000, // 10분간 메모리 보관
|
||||
});
|
||||
|
||||
const templates = templatesData?.templates || [];
|
||||
const pagination = templatesData?.pagination;
|
||||
|
||||
// 템플릿 상세 조회
|
||||
const getTemplate = useCallback(async (templateCode: string) => {
|
||||
const response = await apiClient.get(`/admin/template-standards/${templateCode}`);
|
||||
const result: ApiResponse<TemplateStandard> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to fetch template");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
}, []);
|
||||
|
||||
// 템플릿 생성
|
||||
const createTemplateMutation = useMutation({
|
||||
mutationFn: async (data: TemplateFormData): Promise<TemplateStandard> => {
|
||||
const response = await apiClient.post("/admin/template-standards", data);
|
||||
|
||||
const result: ApiResponse<TemplateStandard> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to create template");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 템플릿 수정
|
||||
const updateTemplateMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
templateCode,
|
||||
data,
|
||||
}: {
|
||||
templateCode: string;
|
||||
data: Partial<TemplateFormData>;
|
||||
}): Promise<TemplateStandard> => {
|
||||
const response = await apiClient.put(`/admin/template-standards/${templateCode}`, data);
|
||||
|
||||
const result: ApiResponse<TemplateStandard> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to update template");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 템플릿 삭제
|
||||
const deleteTemplateMutation = useMutation({
|
||||
mutationFn: async (templateCode: string): Promise<void> => {
|
||||
const response = await apiClient.delete(`/admin/template-standards/${templateCode}`);
|
||||
|
||||
const result: ApiResponse<void> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to delete template");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 정렬 순서 업데이트
|
||||
const updateSortOrderMutation = useMutation({
|
||||
mutationFn: async (templates: { template_code: string; sort_order: number }[]): Promise<void> => {
|
||||
const response = await apiClient.put("/admin/template-standards/sort-order/bulk", { templates });
|
||||
|
||||
const result: ApiResponse<void> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to update sort order");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 템플릿 복제
|
||||
const duplicateTemplateMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
templateCode,
|
||||
newTemplateCode,
|
||||
newTemplateName,
|
||||
}: {
|
||||
templateCode: string;
|
||||
newTemplateCode: string;
|
||||
newTemplateName: string;
|
||||
}): Promise<TemplateStandard> => {
|
||||
const response = await apiClient.post(`/admin/template-standards/${templateCode}/duplicate`, {
|
||||
new_template_code: newTemplateCode,
|
||||
new_template_name: newTemplateName,
|
||||
});
|
||||
|
||||
const result: ApiResponse<TemplateStandard> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to duplicate template");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 템플릿 가져오기
|
||||
const importTemplateMutation = useMutation({
|
||||
mutationFn: async (templateData: any): Promise<TemplateStandard> => {
|
||||
const response = await apiClient.post("/admin/template-standards/import", templateData);
|
||||
|
||||
const result: ApiResponse<TemplateStandard> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to import template");
|
||||
}
|
||||
|
||||
return result.data!;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 목록 새로고침
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
// 템플릿 내보내기
|
||||
const exportTemplate = useCallback(async (templateCode: string) => {
|
||||
const response = await apiClient.get(`/admin/template-standards/${templateCode}/export`);
|
||||
const result: ApiResponse<any> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to export template");
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}, []);
|
||||
|
||||
// 카테고리 목록 조회
|
||||
const {
|
||||
data: categories,
|
||||
isLoading: categoriesLoading,
|
||||
error: categoriesError,
|
||||
} = useQuery({
|
||||
queryKey: ["template-categories"],
|
||||
queryFn: async (): Promise<string[]> => {
|
||||
const response = await apiClient.get("/admin/template-standards/categories");
|
||||
|
||||
const result: ApiResponse<string[]> = response.data;
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to fetch categories");
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
},
|
||||
staleTime: 10 * 60 * 1000, // 10분간 캐시 유지
|
||||
});
|
||||
|
||||
// 편의 메서드들
|
||||
const createTemplate = useCallback(
|
||||
(data: TemplateFormData) => {
|
||||
return createTemplateMutation.mutateAsync(data);
|
||||
},
|
||||
[createTemplateMutation],
|
||||
);
|
||||
|
||||
const updateTemplate = useCallback(
|
||||
(templateCode: string, data: Partial<TemplateFormData>) => {
|
||||
return updateTemplateMutation.mutateAsync({ templateCode, data });
|
||||
},
|
||||
[updateTemplateMutation],
|
||||
);
|
||||
|
||||
const deleteTemplate = useCallback(
|
||||
(templateCode: string) => {
|
||||
return deleteTemplateMutation.mutateAsync(templateCode);
|
||||
},
|
||||
[deleteTemplateMutation],
|
||||
);
|
||||
|
||||
const updateSortOrder = useCallback(
|
||||
(templates: { template_code: string; sort_order: number }[]) => {
|
||||
return updateSortOrderMutation.mutateAsync(templates);
|
||||
},
|
||||
[updateSortOrderMutation],
|
||||
);
|
||||
|
||||
const duplicateTemplate = useCallback(
|
||||
(templateCode: string, newTemplateCode: string, newTemplateName: string) => {
|
||||
return duplicateTemplateMutation.mutateAsync({
|
||||
templateCode,
|
||||
newTemplateCode,
|
||||
newTemplateName,
|
||||
});
|
||||
},
|
||||
[duplicateTemplateMutation],
|
||||
);
|
||||
|
||||
const importTemplate = useCallback(
|
||||
(templateData: any) => {
|
||||
return importTemplateMutation.mutateAsync(templateData);
|
||||
},
|
||||
[importTemplateMutation],
|
||||
);
|
||||
|
||||
return {
|
||||
// 데이터
|
||||
templates,
|
||||
pagination,
|
||||
categories: categories || [],
|
||||
|
||||
// 로딩 상태
|
||||
isLoading,
|
||||
categoriesLoading,
|
||||
isCreating: createTemplateMutation.isPending,
|
||||
isUpdating: updateTemplateMutation.isPending,
|
||||
isDeleting: deleteTemplateMutation.isPending,
|
||||
isDuplicating: duplicateTemplateMutation.isPending,
|
||||
isImporting: importTemplateMutation.isPending,
|
||||
isSortOrderUpdating: updateSortOrderMutation.isPending,
|
||||
|
||||
// 에러 상태
|
||||
error,
|
||||
categoriesError,
|
||||
createError: createTemplateMutation.error,
|
||||
updateError: updateTemplateMutation.error,
|
||||
deleteError: deleteTemplateMutation.error,
|
||||
duplicateError: duplicateTemplateMutation.error,
|
||||
importError: importTemplateMutation.error,
|
||||
sortOrderError: updateSortOrderMutation.error,
|
||||
|
||||
// 메서드
|
||||
getTemplate,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
updateSortOrder,
|
||||
duplicateTemplate,
|
||||
importTemplate,
|
||||
exportTemplate,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user