ERP-node/frontend/app/(main)/admin/validation-demo/page.tsx

586 lines
19 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { toast } from "sonner";
import { EnhancedInteractiveScreenViewer } from "@/components/screen/EnhancedInteractiveScreenViewer";
import { FormValidationIndicator } from "@/components/common/FormValidationIndicator";
import { useFormValidation } from "@/hooks/useFormValidation";
import { enhancedFormService } from "@/lib/services/enhancedFormService";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { ComponentData, WidgetComponent, ColumnInfo, ScreenDefinition } from "@/types/screen";
import { normalizeWebType } from "@/types/unified-web-types";
// 테스트용 화면 정의
const TEST_SCREEN_DEFINITION: ScreenDefinition = {
id: 999,
screenName: "validation-demo",
tableName: "test_users", // 테스트용 테이블
screenResolution: { width: 800, height: 600 },
gridSettings: { size: 20, color: "#e0e0e0", opacity: 0.5 },
description: "검증 시스템 데모 화면",
};
// 테스트용 컴포넌트 데이터
const TEST_COMPONENTS: ComponentData[] = [
{
id: "container-1",
type: "container",
x: 0,
y: 0,
width: 800,
height: 600,
parentId: null,
children: ["widget-1", "widget-2", "widget-3", "widget-4", "widget-5", "widget-6"],
},
{
id: "widget-1",
type: "widget",
x: 20,
y: 20,
width: 200,
height: 40,
parentId: "container-1",
label: "사용자명",
widgetType: "text",
columnName: "user_name",
required: true,
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
{
id: "widget-2",
type: "widget",
x: 20,
y: 80,
width: 200,
height: 40,
parentId: "container-1",
label: "이메일",
widgetType: "email",
columnName: "email",
required: true,
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
{
id: "widget-3",
type: "widget",
x: 20,
y: 140,
width: 200,
height: 40,
parentId: "container-1",
label: "나이",
widgetType: "number",
columnName: "age",
required: false,
webTypeConfig: {
min: 0,
max: 120,
},
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
{
id: "widget-4",
type: "widget",
x: 20,
y: 200,
width: 200,
height: 40,
parentId: "container-1",
label: "생년월일",
widgetType: "date",
columnName: "birth_date",
required: false,
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
{
id: "widget-5",
type: "widget",
x: 20,
y: 260,
width: 200,
height: 40,
parentId: "container-1",
label: "전화번호",
widgetType: "tel",
columnName: "phone",
required: false,
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
{
id: "widget-6",
type: "widget",
x: 20,
y: 320,
width: 100,
height: 40,
parentId: "container-1",
label: "저장",
widgetType: "button",
columnName: "save_button",
required: false,
webTypeConfig: {
actionType: "save",
text: "저장하기",
},
style: {
labelFontSize: "14px",
labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
];
// 테스트용 테이블 컬럼 정보
const TEST_TABLE_COLUMNS: ColumnInfo[] = [
{
tableName: "test_users",
columnName: "id",
columnLabel: "ID",
dataType: "integer",
webType: "number",
widgetType: "number",
inputType: "auto",
isNullable: "N",
required: false,
isPrimaryKey: true,
isVisible: false,
displayOrder: 0,
description: "기본키",
},
{
tableName: "test_users",
columnName: "user_name",
columnLabel: "사용자명",
dataType: "character varying",
webType: "text",
widgetType: "text",
inputType: "direct",
isNullable: "N",
required: true,
characterMaximumLength: 50,
isVisible: true,
displayOrder: 1,
description: "사용자 이름",
},
{
tableName: "test_users",
columnName: "email",
columnLabel: "이메일",
dataType: "character varying",
webType: "email",
widgetType: "email",
inputType: "direct",
isNullable: "N",
required: true,
characterMaximumLength: 100,
isVisible: true,
displayOrder: 2,
description: "이메일 주소",
},
{
tableName: "test_users",
columnName: "age",
columnLabel: "나이",
dataType: "integer",
webType: "number",
widgetType: "number",
inputType: "direct",
isNullable: "Y",
required: false,
isVisible: true,
displayOrder: 3,
description: "나이",
},
{
tableName: "test_users",
columnName: "birth_date",
columnLabel: "생년월일",
dataType: "date",
webType: "date",
widgetType: "date",
inputType: "direct",
isNullable: "Y",
required: false,
isVisible: true,
displayOrder: 4,
description: "생년월일",
},
{
tableName: "test_users",
columnName: "phone",
columnLabel: "전화번호",
dataType: "character varying",
webType: "tel",
widgetType: "tel",
inputType: "direct",
isNullable: "Y",
required: false,
characterMaximumLength: 20,
isVisible: true,
displayOrder: 5,
description: "전화번호",
},
];
export default function ValidationDemoPage() {
const [formData, setFormData] = useState<Record<string, any>>({});
const [selectedTable, setSelectedTable] = useState<string>("test_users");
const [availableTables, setAvailableTables] = useState<string[]>([]);
const [tableColumns, setTableColumns] = useState<ColumnInfo[]>(TEST_TABLE_COLUMNS);
const [isLoading, setIsLoading] = useState(false);
// 폼 검증 훅 사용
const { validationState, saveState, validateForm, saveForm, canSave, getFieldError, hasFieldError, isFieldValid } =
useFormValidation(
formData,
TEST_COMPONENTS.filter((c) => c.type === "widget") as WidgetComponent[],
tableColumns,
TEST_SCREEN_DEFINITION,
{
enableRealTimeValidation: true,
validationDelay: 300,
enableAutoSave: false,
showToastMessages: true,
validateOnMount: false,
},
);
// 테이블 목록 로드
useEffect(() => {
const loadTables = async () => {
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setAvailableTables(response.data.map((table) => table.tableName));
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
}
};
loadTables();
}, []);
// 선택된 테이블의 컬럼 정보 로드
useEffect(() => {
if (selectedTable && selectedTable !== "test_users") {
const loadTableColumns = async () => {
setIsLoading(true);
try {
const response = await tableManagementApi.getColumnList(selectedTable);
if (response.success && response.data) {
setTableColumns(response.data.columns || []);
}
} catch (error) {
console.error("테이블 컬럼 정보 로드 실패:", error);
toast.error("테이블 컬럼 정보를 불러오는데 실패했습니다.");
} finally {
setIsLoading(false);
}
};
loadTableColumns();
} else {
setTableColumns(TEST_TABLE_COLUMNS);
}
}, [selectedTable]);
const handleFormDataChange = (fieldName: string, value: any) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
};
const handleTestFormSubmit = async () => {
const result = await saveForm();
if (result) {
toast.success("폼 데이터가 성공적으로 저장되었습니다!");
}
};
const handleManualValidation = async () => {
const result = await validateForm();
toast.info(
`검증 완료: ${result.isValid ? "성공" : "실패"} (오류 ${result.errors.length}개, 경고 ${result.warnings.length}개)`,
);
};
const generateTestData = () => {
setFormData({
user_name: "테스트 사용자",
email: "test@example.com",
age: 25,
birth_date: "1999-01-01",
phone: "010-1234-5678",
});
toast.info("테스트 데이터가 입력되었습니다.");
};
const generateInvalidData = () => {
setFormData({
user_name: "", // 필수 필드 누락
email: "invalid-email", // 잘못된 이메일 형식
age: -5, // 음수 나이
birth_date: "invalid-date", // 잘못된 날짜
phone: "123", // 잘못된 전화번호 형식
});
toast.info("잘못된 테스트 데이터가 입력되었습니다.");
};
const clearForm = () => {
setFormData({});
toast.info("폼이 초기화되었습니다.");
};
return (
<div className="container mx-auto space-y-8 py-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold"> </h1>
<p className="text-muted-foreground"> </p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline"> </Badge>
<Badge variant={validationState.isValid ? "default" : "destructive"}>
{validationState.isValid ? "검증 통과" : "검증 실패"}
</Badge>
</div>
</div>
<Tabs defaultValue="demo" className="space-y-4">
<TabsList>
<TabsTrigger value="demo"> </TabsTrigger>
<TabsTrigger value="validation"> </TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger>
</TabsList>
<TabsContent value="demo" className="space-y-4">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* 폼 영역 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> . .</CardDescription>
</CardHeader>
<CardContent>
<div className="relative min-h-[400px] rounded-lg border border-dashed border-gray-300 p-4">
<EnhancedInteractiveScreenViewer
component={TEST_COMPONENTS[0]} // container
allComponents={TEST_COMPONENTS}
formData={formData}
onFormDataChange={handleFormDataChange}
screenInfo={TEST_SCREEN_DEFINITION}
tableColumns={tableColumns}
validationOptions={{
enableRealTimeValidation: true,
validationDelay: 300,
enableAutoSave: false,
showToastMessages: true,
}}
showValidationPanel={false}
compactValidation={true}
/>
</div>
</CardContent>
</Card>
{/* 컨트롤 패널 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<div className="grid grid-cols-1 gap-2">
<Button onClick={generateTestData} variant="outline" size="sm">
</Button>
<Button onClick={generateInvalidData} variant="outline" size="sm">
</Button>
<Button onClick={clearForm} variant="outline" size="sm">
🧹
</Button>
</div>
</div>
<Separator />
<div className="space-y-2">
<Label> & </Label>
<div className="grid grid-cols-1 gap-2">
<Button onClick={handleManualValidation} variant="outline" size="sm">
🔍
</Button>
<Button
onClick={handleTestFormSubmit}
disabled={!canSave || saveState.status === "saving"}
size="sm"
>
{saveState.status === "saving" ? "저장 중..." : "💾 폼 저장"}
</Button>
</div>
</div>
<Separator />
<FormValidationIndicator
validationState={validationState}
saveState={saveState}
onSave={handleTestFormSubmit}
canSave={canSave}
compact={false}
showDetails={true}
/>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="validation" className="space-y-4">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormValidationIndicator
validationState={validationState}
saveState={saveState}
onSave={handleTestFormSubmit}
canSave={canSave}
compact={false}
showDetails={true}
showPerformance={true}
/>
<Separator />
<div className="space-y-2">
<h4 className="font-semibold"> </h4>
<pre className="max-h-60 overflow-auto rounded-md bg-gray-100 p-3 text-sm">
{JSON.stringify(formData, null, 2)}
</pre>
</div>
<div className="space-y-2">
<h4 className="font-semibold"> </h4>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-md bg-green-50 p-3">
<div className="text-lg font-bold text-green-600">
{Object.values(validationState.fieldStates).filter((f) => f.status === "valid").length}
</div>
<div className="text-sm text-green-700"> </div>
</div>
<div className="rounded-md bg-red-50 p-3">
<div className="text-lg font-bold text-red-600">{validationState.errors.length}</div>
<div className="text-sm text-red-700"> </div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="table-select"> </Label>
<Select value={selectedTable} onValueChange={setSelectedTable}>
<SelectTrigger>
<SelectValue placeholder="테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="test_users">test_users ()</SelectItem>
{availableTables.map((table) => (
<SelectItem key={table} value={table}>
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{isLoading && (
<div className="py-4 text-center">
<div className="text-muted-foreground text-sm"> ...</div>
</div>
)}
<div className="space-y-2">
<h4 className="font-semibold"> </h4>
<div className="max-h-60 overflow-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
</tr>
</thead>
<tbody>
{tableColumns.map((column) => (
<tr key={column.columnName} className="border-b">
<td className="p-2">{column.columnName}</td>
<td className="p-2">
<Badge variant="outline" className="text-xs">
{column.webType}
</Badge>
</td>
<td className="p-2">
{column.required ? (
<Badge variant="destructive" className="text-xs">
</Badge>
) : (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}