586 lines
19 KiB
TypeScript
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: "#374151",
|
||
|
|
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: "#374151",
|
||
|
|
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: "#374151",
|
||
|
|
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: "#374151",
|
||
|
|
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: "#374151",
|
||
|
|
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: "#374151",
|
||
|
|
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-6 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>
|
||
|
|
);
|
||
|
|
}
|