ERP-node/frontend/lib/meta-components/examples/MetaComponentExample.tsx

366 lines
12 KiB
TypeScript
Raw Normal View History

2026-03-01 03:39:00 +09:00
"use client";
import React, { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { FieldRenderer } from "../Field/FieldRenderer";
import { DataViewRenderer } from "../DataView/DataViewRenderer";
import { ActionRenderer } from "../Action/ActionRenderer";
import { ReactiveBindingEngine } from "../bindings/ReactiveBindingEngine";
import {
getFieldConfig,
getTableColumns,
saveLayout,
getLayout,
saveBindings,
getBindings,
type FieldConfigResponse,
type TableColumnsResponse,
} from "@/lib/api/metaComponent";
import type { FieldConfig } from "../Field/fieldTypes";
import type { DataViewConfig } from "../DataView/dataViewTypes";
import type { ActionConfig } from "../Action/actionTypes";
import type { ReactiveBinding } from "../bindings/bindingTypes";
/**
* V3
*
* Phase A :
* 1. Field ( )
* 2. DataView (/ )
* 3. Action +
* 4. Reactive Binding
*/
export function MetaComponentExample() {
const [loading, setLoading] = useState(true);
const [fieldConfigs, setFieldConfigs] = useState<FieldConfig[]>([]);
const [dataViewConfig, setDataViewConfig] = useState<DataViewConfig | null>(null);
const [bindingEngine] = useState(() => new ReactiveBindingEngine());
// 예시: user_info 테이블 기반 화면 자동 생성
useEffect(() => {
loadMetaComponents();
}, []);
/**
* API
*/
async function loadMetaComponents() {
try {
setLoading(true);
// 1. 테이블 컬럼 목록 조회 (webType + FK 관계 자동 감지)
const columnsResponse = await getTableColumns("user_info");
if (!columnsResponse.success || !columnsResponse.data) {
throw new Error("테이블 컬럼 조회 실패");
}
// 2. 각 컬럼별 Field Config 자동 생성
const configs: FieldConfig[] = [];
for (const column of columnsResponse.data.columns) {
const configResponse = await getFieldConfig("user_info", column.columnName);
if (configResponse.success && configResponse.data) {
configs.push({
id: `field_${column.columnName}`,
type: "meta-field",
webType: configResponse.data.webType,
label: configResponse.data.label,
binding: column.columnName,
placeholder: configResponse.data.placeholder,
defaultValue: configResponse.data.defaultValue,
required: configResponse.data.required,
maxLength: configResponse.data.maxLength,
validation: configResponse.data.validation,
options: configResponse.data.options,
join: configResponse.data.join,
});
}
}
setFieldConfigs(configs);
// 3. DataView 설정 (테이블 뷰)
setDataViewConfig({
id: "dataview_user_list",
type: "meta-dataview",
viewMode: "table",
tableName: "user_info",
columns: columnsResponse.data.columns.map(col => ({
field: col.columnName,
header: col.columnComment || col.columnName,
webType: col.webType,
})),
pagination: {
enabled: true,
pageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
},
actions: {
add: {
enabled: true,
label: "등록",
icon: "plus",
},
edit: {
enabled: true,
label: "수정",
icon: "pencil",
},
delete: {
enabled: true,
label: "삭제",
icon: "trash",
confirmMessage: "정말로 삭제하시겠습니까?",
},
},
});
// 4. Reactive Binding 설정 (예시: 부서 선택 → 사용자 목록 필터링)
const bindings: ReactiveBinding[] = [
{
id: "binding_dept_filter",
sourceComponent: "field_dept_code",
sourceEvent: "onChange",
targetComponent: "dataview_user_list",
targetAction: "filter",
condition: {
type: "expression",
expression: "source.value !== ''",
},
transform: {
config: {
filterField: "dept_code",
filterOperator: "equals",
filterValue: "{{source.value}}",
},
},
priority: 10,
enabled: true,
},
];
bindingEngine.setBindings(bindings);
toast.success("메타 컴포넌트 로드 완료!");
} catch (error: any) {
console.error("메타 컴포넌트 로드 실패:", error);
toast.error(error.message || "메타 컴포넌트 로드 실패");
} finally {
setLoading(false);
}
}
/**
* (screen_layouts_v3 version "3.0" )
*/
async function handleSaveLayout() {
try {
const layoutData = {
version: "3.0" as const,
screenId: 123, // 실제 화면 ID
layerId: 1,
components: [
...fieldConfigs,
...(dataViewConfig ? [dataViewConfig] : []),
],
layers: [
{ id: 1, name: "기본", visible: true, order: 1 },
],
metadata: {
lastModified: new Date().toISOString(),
description: "사용자 관리 화면 (메타 컴포넌트 V3)",
},
};
const response = await saveLayout(layoutData);
if (response.success) {
toast.success("레이아웃 저장 완료!");
} else {
throw new Error(response.error || "저장 실패");
}
} catch (error: any) {
console.error("레이아웃 저장 실패:", error);
toast.error(error.message || "레이아웃 저장 실패");
}
}
/**
* Reactive Binding
*/
async function handleSaveBindings() {
try {
const bindings = bindingEngine.getBindings();
const response = await saveBindings({
screenId: 123,
bindings,
});
if (response.success) {
toast.success("바인딩 저장 완료!");
} else {
throw new Error(response.error || "저장 실패");
}
} catch (error: any) {
console.error("바인딩 저장 실패:", error);
toast.error(error.message || "바인딩 저장 실패");
}
}
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground"> ...</p>
</div>
</div>
);
}
return (
<div className="container mx-auto space-y-6 p-4 sm:p-6">
{/* 헤더 */}
<Card>
<CardHeader>
<CardTitle className="text-2xl sm:text-3xl"> V3 </CardTitle>
<CardDescription className="text-sm sm:text-base">
(Phase A)
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Button onClick={handleSaveLayout} variant="default">
</Button>
<Button onClick={handleSaveBindings} variant="outline">
</Button>
<Button onClick={loadMetaComponents} variant="ghost">
</Button>
</CardContent>
</Card>
{/* Field 컴포넌트들 (자동 생성) */}
<Card>
<CardHeader>
<CardTitle className="text-lg sm:text-xl"> </CardTitle>
<CardDescription className="text-xs sm:text-sm">
table_type_columns의 webType
</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{fieldConfigs.map((config) => (
<div key={config.id} className="space-y-2">
<FieldRenderer
config={config}
value={config.defaultValue}
onChange={(value) => {
console.log(`${config.binding} 변경:`, value);
// Reactive Binding 실행
bindingEngine.emitEvent(config.id, "onChange", { value });
}}
/>
</div>
))}
</CardContent>
</Card>
{/* DataView 컴포넌트 (자동 생성) */}
{dataViewConfig && (
<Card>
<CardHeader>
<CardTitle className="text-lg sm:text-xl"> </CardTitle>
<CardDescription className="text-xs sm:text-sm">
//
</CardDescription>
</CardHeader>
<CardContent>
<DataViewRenderer
config={dataViewConfig}
data={[
{ user_id: "admin", user_name: "관리자", dept_code: "IT" },
{ user_id: "user1", user_name: "홍길동", dept_code: "HR" },
]}
onActionClick={(action, rowData) => {
console.log("액션 클릭:", action, rowData);
toast.info(`${action} 액션 실행`);
}}
/>
</CardContent>
</Card>
)}
{/* Action 컴포넌트 예시 */}
<Card>
<CardHeader>
<CardTitle className="text-lg sm:text-xl"> </CardTitle>
<CardDescription className="text-xs sm:text-sm">
+ steps (Phase B에서 )
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<ActionRenderer
config={{
id: "action_save",
type: "meta-action",
label: "저장",
buttonType: "primary",
icon: "save",
confirmDialog: {
enabled: true,
title: "저장 확인",
message: "변경 사항을 저장하시겠습니까?",
},
steps: [
{
id: "step1",
type: "validate",
name: "유효성 검증",
config: { fields: ["user_name", "dept_code"] },
},
{
id: "step2",
type: "api",
name: "API 호출",
config: {
endpoint: "/api/users",
method: "POST",
},
},
],
}}
onClick={async () => {
console.log("저장 버튼 클릭");
toast.success("저장 완료!");
}}
/>
<ActionRenderer
config={{
id: "action_delete",
type: "meta-action",
label: "삭제",
buttonType: "danger",
icon: "trash",
confirmDialog: {
enabled: true,
title: "삭제 확인",
message: "정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
},
}}
onClick={async () => {
console.log("삭제 버튼 클릭");
toast.error("삭제 완료!");
}}
/>
</CardContent>
</Card>
</div>
);
}