1120 lines
41 KiB
TypeScript
1120 lines
41 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* UnifiedComponentsDemo
|
||
|
|
*
|
||
|
|
* Unified 컴포넌트들을 테스트하고 미리볼 수 있는 데모 페이지
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useState } from "react";
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { ArrowLeft, Code, Eye, Zap } from "lucide-react";
|
||
|
|
|
||
|
|
// Unified 컴포넌트들
|
||
|
|
import { UnifiedInput } from "./UnifiedInput";
|
||
|
|
import { UnifiedSelect } from "./UnifiedSelect";
|
||
|
|
import { UnifiedDate } from "./UnifiedDate";
|
||
|
|
import { UnifiedList } from "./UnifiedList";
|
||
|
|
import { UnifiedLayout } from "./UnifiedLayout";
|
||
|
|
import { UnifiedGroup } from "./UnifiedGroup";
|
||
|
|
import { UnifiedMedia } from "./UnifiedMedia";
|
||
|
|
import { UnifiedBiz } from "./UnifiedBiz";
|
||
|
|
import { UnifiedHierarchy } from "./UnifiedHierarchy";
|
||
|
|
|
||
|
|
// 조건부 로직
|
||
|
|
import { UnifiedFormProvider, useUnifiedForm } from "./UnifiedFormContext";
|
||
|
|
import { ConditionalConfigPanel } from "./ConditionalConfigPanel";
|
||
|
|
|
||
|
|
// 타입
|
||
|
|
import { HierarchyNode, ConditionalConfig } from "@/types/unified-components";
|
||
|
|
|
||
|
|
interface UnifiedComponentsDemoProps {
|
||
|
|
onBack?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function UnifiedComponentsDemo({ onBack }: UnifiedComponentsDemoProps) {
|
||
|
|
const [activeTab, setActiveTab] = useState("conditional");
|
||
|
|
|
||
|
|
// 데모용 상태
|
||
|
|
const [inputValues, setInputValues] = useState({
|
||
|
|
text: "",
|
||
|
|
number: 50,
|
||
|
|
password: "",
|
||
|
|
slider: 30,
|
||
|
|
color: "#3b82f6",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [selectValues, setSelectValues] = useState({
|
||
|
|
dropdown: "",
|
||
|
|
radio: "",
|
||
|
|
check: [] as string[],
|
||
|
|
tag: [] as string[],
|
||
|
|
toggle: "false",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [dateValues, setDateValues] = useState({
|
||
|
|
date: "",
|
||
|
|
time: "",
|
||
|
|
datetime: "",
|
||
|
|
range: ["", ""] as [string, string],
|
||
|
|
});
|
||
|
|
|
||
|
|
// 샘플 데이터
|
||
|
|
const sampleOptions = [
|
||
|
|
{ value: "option1", label: "옵션 1" },
|
||
|
|
{ value: "option2", label: "옵션 2" },
|
||
|
|
{ value: "option3", label: "옵션 3" },
|
||
|
|
{ value: "option4", label: "옵션 4" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const sampleTableData = [
|
||
|
|
{ id: 1, name: "홍길동", email: "hong@test.com", status: "active", date: "2024-01-15" },
|
||
|
|
{ id: 2, name: "김철수", email: "kim@test.com", status: "inactive", date: "2024-02-20" },
|
||
|
|
{ id: 3, name: "이영희", email: "lee@test.com", status: "active", date: "2024-03-10" },
|
||
|
|
{ id: 4, name: "박민수", email: "park@test.com", status: "pending", date: "2024-04-05" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const sampleHierarchyData: HierarchyNode[] = [
|
||
|
|
{
|
||
|
|
id: "1",
|
||
|
|
label: "본사",
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
id: "1-1",
|
||
|
|
label: "영업부",
|
||
|
|
children: [
|
||
|
|
{ id: "1-1-1", label: "영업1팀" },
|
||
|
|
{ id: "1-1-2", label: "영업2팀" },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "1-2",
|
||
|
|
label: "개발부",
|
||
|
|
children: [
|
||
|
|
{ id: "1-2-1", label: "프론트엔드팀" },
|
||
|
|
{ id: "1-2-2", label: "백엔드팀" },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full flex-col bg-background">
|
||
|
|
{/* 헤더 */}
|
||
|
|
<div className="flex items-center gap-4 border-b p-4">
|
||
|
|
{onBack && (
|
||
|
|
<Button variant="outline" size="sm" onClick={onBack}>
|
||
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
|
|
뒤로
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
<div className="flex-1">
|
||
|
|
<h1 className="text-2xl font-bold">Unified 컴포넌트 테스트</h1>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
10개의 통합 컴포넌트를 테스트합니다
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<Badge variant="secondary">Phase 1-3 완료</Badge>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 탭 컨텐츠 */}
|
||
|
|
<div className="flex-1 overflow-auto p-4">
|
||
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
|
|
<TabsList className="grid w-full grid-cols-5 lg:grid-cols-10">
|
||
|
|
<TabsTrigger value="conditional" className="gap-1 text-orange-600">
|
||
|
|
<Zap className="h-3 w-3" />
|
||
|
|
조건부
|
||
|
|
</TabsTrigger>
|
||
|
|
<TabsTrigger value="input">Input</TabsTrigger>
|
||
|
|
<TabsTrigger value="select">Select</TabsTrigger>
|
||
|
|
<TabsTrigger value="date">Date</TabsTrigger>
|
||
|
|
<TabsTrigger value="list">List</TabsTrigger>
|
||
|
|
<TabsTrigger value="layout">Layout</TabsTrigger>
|
||
|
|
<TabsTrigger value="group" className="hidden lg:flex">Group</TabsTrigger>
|
||
|
|
<TabsTrigger value="media" className="hidden lg:flex">Media</TabsTrigger>
|
||
|
|
<TabsTrigger value="biz" className="hidden lg:flex">Biz</TabsTrigger>
|
||
|
|
<TabsTrigger value="hierarchy" className="hidden lg:flex">Hierarchy</TabsTrigger>
|
||
|
|
</TabsList>
|
||
|
|
|
||
|
|
{/* 조건부 동작 데모 탭 */}
|
||
|
|
<TabsContent value="conditional" className="mt-6">
|
||
|
|
<ConditionalDemo />
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedInput 탭 */}
|
||
|
|
<TabsContent value="input" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedInput</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 입력 컴포넌트 - text, number, password, slider, color, button
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
|
|
{/* Text Input */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-text"
|
||
|
|
label="텍스트 입력"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "text", placeholder: "텍스트를 입력하세요" }}
|
||
|
|
value={inputValues.text}
|
||
|
|
onChange={(v) => setInputValues({ ...inputValues, text: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Number Input */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-number"
|
||
|
|
label="숫자 입력"
|
||
|
|
required
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "number", min: 0, max: 100, step: 5 }}
|
||
|
|
value={inputValues.number}
|
||
|
|
onChange={(v) => setInputValues({ ...inputValues, number: Number(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Password Input */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-password"
|
||
|
|
label="비밀번호"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "password", placeholder: "비밀번호 입력" }}
|
||
|
|
value={inputValues.password}
|
||
|
|
onChange={(v) => setInputValues({ ...inputValues, password: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Slider Input */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-slider"
|
||
|
|
label="슬라이더"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "slider", min: 0, max: 100, step: 10 }}
|
||
|
|
value={inputValues.slider}
|
||
|
|
onChange={(v) => setInputValues({ ...inputValues, slider: Number(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Color Input */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-color"
|
||
|
|
label="색상 선택"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "color" }}
|
||
|
|
value={inputValues.color}
|
||
|
|
onChange={(v) => setInputValues({ ...inputValues, color: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Button */}
|
||
|
|
<UnifiedInput
|
||
|
|
id="demo-button"
|
||
|
|
label="버튼"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{
|
||
|
|
type: "button",
|
||
|
|
buttonText: "클릭하세요",
|
||
|
|
buttonVariant: "default"
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
<div className="p-4 bg-muted/50 rounded-lg">
|
||
|
|
<h4 className="text-sm font-medium mb-2">현재 값:</h4>
|
||
|
|
<pre className="text-xs overflow-auto">
|
||
|
|
{JSON.stringify(inputValues, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedSelect 탭 */}
|
||
|
|
<TabsContent value="select" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedSelect</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 선택 컴포넌트 - dropdown, radio, check, tag, toggle, swap
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* Dropdown */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="demo-dropdown"
|
||
|
|
label="드롭다운"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "dropdown",
|
||
|
|
source: "static",
|
||
|
|
options: sampleOptions,
|
||
|
|
searchable: true
|
||
|
|
}}
|
||
|
|
value={selectValues.dropdown}
|
||
|
|
onChange={(v) => setSelectValues({ ...selectValues, dropdown: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Radio */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="demo-radio"
|
||
|
|
label="라디오 버튼"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "radio",
|
||
|
|
source: "static",
|
||
|
|
options: sampleOptions
|
||
|
|
}}
|
||
|
|
value={selectValues.radio}
|
||
|
|
onChange={(v) => setSelectValues({ ...selectValues, radio: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Checkbox */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="demo-check"
|
||
|
|
label="체크박스 (다중선택)"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "check",
|
||
|
|
source: "static",
|
||
|
|
options: sampleOptions,
|
||
|
|
maxSelect: 3
|
||
|
|
}}
|
||
|
|
value={selectValues.check}
|
||
|
|
onChange={(v) => setSelectValues({ ...selectValues, check: v as string[] })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Tag */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="demo-tag"
|
||
|
|
label="태그 선택"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "tag",
|
||
|
|
source: "static",
|
||
|
|
options: sampleOptions
|
||
|
|
}}
|
||
|
|
value={selectValues.tag}
|
||
|
|
onChange={(v) => setSelectValues({ ...selectValues, tag: v as string[] })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Toggle */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="demo-toggle"
|
||
|
|
label="토글"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "toggle",
|
||
|
|
source: "static",
|
||
|
|
options: [
|
||
|
|
{ value: "false", label: "비활성" },
|
||
|
|
{ value: "true", label: "활성" }
|
||
|
|
]
|
||
|
|
}}
|
||
|
|
value={selectValues.toggle}
|
||
|
|
onChange={(v) => setSelectValues({ ...selectValues, toggle: String(v) })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
<div className="p-4 bg-muted/50 rounded-lg">
|
||
|
|
<h4 className="text-sm font-medium mb-2">현재 값:</h4>
|
||
|
|
<pre className="text-xs overflow-auto">
|
||
|
|
{JSON.stringify(selectValues, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedDate 탭 */}
|
||
|
|
<TabsContent value="date" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedDate</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 날짜/시간 컴포넌트 - date, time, datetime, range
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* Date */}
|
||
|
|
<UnifiedDate
|
||
|
|
id="demo-date"
|
||
|
|
label="날짜 선택"
|
||
|
|
unifiedType="UnifiedDate"
|
||
|
|
config={{ type: "date", format: "YYYY-MM-DD" }}
|
||
|
|
value={dateValues.date}
|
||
|
|
onChange={(v) => setDateValues({ ...dateValues, date: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Time */}
|
||
|
|
<UnifiedDate
|
||
|
|
id="demo-time"
|
||
|
|
label="시간 선택"
|
||
|
|
unifiedType="UnifiedDate"
|
||
|
|
config={{ type: "time" }}
|
||
|
|
value={dateValues.time}
|
||
|
|
onChange={(v) => setDateValues({ ...dateValues, time: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* DateTime */}
|
||
|
|
<UnifiedDate
|
||
|
|
id="demo-datetime"
|
||
|
|
label="날짜+시간"
|
||
|
|
unifiedType="UnifiedDate"
|
||
|
|
config={{ type: "datetime" }}
|
||
|
|
value={dateValues.datetime}
|
||
|
|
onChange={(v) => setDateValues({ ...dateValues, datetime: String(v) })}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Range */}
|
||
|
|
<UnifiedDate
|
||
|
|
id="demo-range"
|
||
|
|
label="기간 선택"
|
||
|
|
unifiedType="UnifiedDate"
|
||
|
|
config={{ type: "date", range: true }}
|
||
|
|
value={dateValues.range}
|
||
|
|
onChange={(v) => setDateValues({ ...dateValues, range: v as [string, string] })}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
<div className="p-4 bg-muted/50 rounded-lg">
|
||
|
|
<h4 className="text-sm font-medium mb-2">현재 값:</h4>
|
||
|
|
<pre className="text-xs overflow-auto">
|
||
|
|
{JSON.stringify(dateValues, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedList 탭 */}
|
||
|
|
<TabsContent value="list" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedList</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 리스트 컴포넌트 - table, card, list
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<UnifiedList
|
||
|
|
id="demo-list"
|
||
|
|
label="사용자 목록"
|
||
|
|
unifiedType="UnifiedList"
|
||
|
|
config={{
|
||
|
|
viewMode: "table",
|
||
|
|
searchable: true,
|
||
|
|
pageable: true,
|
||
|
|
pageSize: 5,
|
||
|
|
columns: [
|
||
|
|
{ field: "id", header: "ID", width: 60, sortable: true },
|
||
|
|
{ field: "name", header: "이름", sortable: true },
|
||
|
|
{ field: "email", header: "이메일" },
|
||
|
|
{ field: "status", header: "상태" },
|
||
|
|
{ field: "date", header: "등록일", format: "date", sortable: true },
|
||
|
|
],
|
||
|
|
}}
|
||
|
|
data={sampleTableData}
|
||
|
|
onRowClick={(row) => console.log("Row clicked:", row)}
|
||
|
|
/>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedLayout 탭 */}
|
||
|
|
<TabsContent value="layout" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedLayout</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 레이아웃 컴포넌트 - 12컬럼 그리드 시스템, split, flex
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
{/* 12컬럼 그리드 시스템 */}
|
||
|
|
<div>
|
||
|
|
<h4 className="text-sm font-medium mb-3">12컬럼 그리드 시스템</h4>
|
||
|
|
<p className="text-xs text-muted-foreground mb-4">
|
||
|
|
shadcn/Tailwind 표준 12컬럼 시스템. 자식 요소에 col-span-* 클래스로 너비 조절
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{/* 12컬럼 전체 보기 */}
|
||
|
|
<UnifiedLayout
|
||
|
|
id="demo-grid-12"
|
||
|
|
unifiedType="UnifiedLayout"
|
||
|
|
config={{ type: "grid", columns: 12, gap: "4px", use12Column: true }}
|
||
|
|
>
|
||
|
|
{Array.from({ length: 12 }).map((_, i) => (
|
||
|
|
<div key={i} className="p-2 bg-primary/20 rounded text-center text-xs">
|
||
|
|
{i + 1}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</UnifiedLayout>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* col-span 예시 */}
|
||
|
|
<div>
|
||
|
|
<h4 className="text-sm font-medium mb-3">col-span 활용 예시</h4>
|
||
|
|
<UnifiedLayout
|
||
|
|
id="demo-grid-span"
|
||
|
|
unifiedType="UnifiedLayout"
|
||
|
|
config={{ type: "grid", columns: 12, gap: "8px", use12Column: true }}
|
||
|
|
>
|
||
|
|
<div className="col-span-12 p-3 bg-blue-100 rounded text-center text-sm">
|
||
|
|
col-span-12 (전체)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-6 p-3 bg-green-100 rounded text-center text-sm">
|
||
|
|
col-span-6 (절반)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-6 p-3 bg-green-100 rounded text-center text-sm">
|
||
|
|
col-span-6 (절반)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-4 p-3 bg-yellow-100 rounded text-center text-sm">
|
||
|
|
col-span-4 (1/3)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-4 p-3 bg-yellow-100 rounded text-center text-sm">
|
||
|
|
col-span-4 (1/3)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-4 p-3 bg-yellow-100 rounded text-center text-sm">
|
||
|
|
col-span-4 (1/3)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-3 p-3 bg-purple-100 rounded text-center text-sm">
|
||
|
|
col-span-3 (1/4)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-3 p-3 bg-purple-100 rounded text-center text-sm">
|
||
|
|
col-span-3 (1/4)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-3 p-3 bg-purple-100 rounded text-center text-sm">
|
||
|
|
col-span-3 (1/4)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-3 p-3 bg-purple-100 rounded text-center text-sm">
|
||
|
|
col-span-3 (1/4)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-8 p-3 bg-red-100 rounded text-center text-sm">
|
||
|
|
col-span-8 (2/3)
|
||
|
|
</div>
|
||
|
|
<div className="col-span-4 p-3 bg-red-100 rounded text-center text-sm">
|
||
|
|
col-span-4 (1/3)
|
||
|
|
</div>
|
||
|
|
</UnifiedLayout>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* Split Layout */}
|
||
|
|
<div>
|
||
|
|
<h4 className="text-sm font-medium mb-3">Split Layout (리사이즈 가능)</h4>
|
||
|
|
<UnifiedLayout
|
||
|
|
id="demo-split"
|
||
|
|
unifiedType="UnifiedLayout"
|
||
|
|
config={{ type: "split", direction: "horizontal", splitRatio: [30, 70] }}
|
||
|
|
style={{ height: "200px" }}
|
||
|
|
>
|
||
|
|
<div className="p-4 bg-blue-100 h-full flex items-center justify-center">
|
||
|
|
왼쪽 패널
|
||
|
|
</div>
|
||
|
|
<div className="p-4 bg-green-100 h-full flex items-center justify-center">
|
||
|
|
오른쪽 패널
|
||
|
|
</div>
|
||
|
|
</UnifiedLayout>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedGroup 탭 */}
|
||
|
|
<TabsContent value="group" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedGroup</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 그룹 컴포넌트 - tabs, accordion, section, card-section
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
{/* Tabs */}
|
||
|
|
<UnifiedGroup
|
||
|
|
id="demo-tabs"
|
||
|
|
unifiedType="UnifiedGroup"
|
||
|
|
config={{
|
||
|
|
type: "tabs",
|
||
|
|
tabs: [
|
||
|
|
{ id: "tab1", title: "탭 1", content: <div className="p-4">탭 1 내용</div> },
|
||
|
|
{ id: "tab2", title: "탭 2", content: <div className="p-4">탭 2 내용</div> },
|
||
|
|
{ id: "tab3", title: "탭 3", content: <div className="p-4">탭 3 내용</div> },
|
||
|
|
],
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* Accordion */}
|
||
|
|
<UnifiedGroup
|
||
|
|
id="demo-accordion"
|
||
|
|
unifiedType="UnifiedGroup"
|
||
|
|
config={{
|
||
|
|
type: "accordion",
|
||
|
|
title: "접을 수 있는 섹션",
|
||
|
|
collapsible: true,
|
||
|
|
defaultExpanded: true,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<p>이 내용은 접었다 펼 수 있습니다.</p>
|
||
|
|
</UnifiedGroup>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* Card Section */}
|
||
|
|
<UnifiedGroup
|
||
|
|
id="demo-card"
|
||
|
|
unifiedType="UnifiedGroup"
|
||
|
|
config={{
|
||
|
|
type: "card-section",
|
||
|
|
title: "카드 섹션",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<p>카드 스타일 섹션 내용입니다.</p>
|
||
|
|
</UnifiedGroup>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedMedia 탭 */}
|
||
|
|
<TabsContent value="media" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedMedia</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 미디어 컴포넌트 - file, image, video, audio
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* File Upload */}
|
||
|
|
<UnifiedMedia
|
||
|
|
id="demo-file"
|
||
|
|
label="파일 업로드"
|
||
|
|
unifiedType="UnifiedMedia"
|
||
|
|
config={{ type: "file", multiple: true }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Image Upload */}
|
||
|
|
<UnifiedMedia
|
||
|
|
id="demo-image"
|
||
|
|
label="이미지 업로드"
|
||
|
|
unifiedType="UnifiedMedia"
|
||
|
|
config={{ type: "image", preview: true }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedBiz 탭 */}
|
||
|
|
<TabsContent value="biz" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedBiz</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 비즈니스 컴포넌트 - numbering, category, flow 등 (플레이스홀더)
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
|
|
<UnifiedBiz
|
||
|
|
id="demo-numbering"
|
||
|
|
unifiedType="UnifiedBiz"
|
||
|
|
config={{ type: "numbering" }}
|
||
|
|
/>
|
||
|
|
<UnifiedBiz
|
||
|
|
id="demo-category"
|
||
|
|
unifiedType="UnifiedBiz"
|
||
|
|
config={{ type: "category" }}
|
||
|
|
/>
|
||
|
|
<UnifiedBiz
|
||
|
|
id="demo-related"
|
||
|
|
unifiedType="UnifiedBiz"
|
||
|
|
config={{ type: "related-buttons" }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
|
||
|
|
{/* UnifiedHierarchy 탭 */}
|
||
|
|
<TabsContent value="hierarchy" className="mt-6">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>UnifiedHierarchy</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
통합 계층 구조 컴포넌트 - tree, org, bom, cascading
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* Tree View */}
|
||
|
|
<div>
|
||
|
|
<h4 className="text-sm font-medium mb-3">트리 뷰</h4>
|
||
|
|
<UnifiedHierarchy
|
||
|
|
id="demo-tree"
|
||
|
|
unifiedType="UnifiedHierarchy"
|
||
|
|
config={{ type: "tree", viewMode: "tree", editable: false }}
|
||
|
|
data={sampleHierarchyData}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Cascading Dropdowns */}
|
||
|
|
<div>
|
||
|
|
<h4 className="text-sm font-medium mb-3">연쇄 드롭다운</h4>
|
||
|
|
<UnifiedHierarchy
|
||
|
|
id="demo-cascading"
|
||
|
|
unifiedType="UnifiedHierarchy"
|
||
|
|
config={{ type: "cascading", viewMode: "dropdown", maxLevel: 3 }}
|
||
|
|
data={sampleHierarchyData}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</TabsContent>
|
||
|
|
</Tabs>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default UnifiedComponentsDemo;
|
||
|
|
|
||
|
|
// ===== 조건부 동작 데모 컴포넌트 =====
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 조건부 동작을 시연하는 데모 컴포넌트
|
||
|
|
*
|
||
|
|
* 시나리오:
|
||
|
|
* 1. 계약 유형 선택 → 유형별 다른 필드 표시
|
||
|
|
* 2. VIP 여부 체크 → VIP 전용 필드 활성화
|
||
|
|
* 3. 국가 선택 → 해당 국가의 도시만 표시 (연쇄)
|
||
|
|
*/
|
||
|
|
function ConditionalDemo() {
|
||
|
|
return (
|
||
|
|
<UnifiedFormProvider
|
||
|
|
initialValues={{
|
||
|
|
contractType: "",
|
||
|
|
isVip: false,
|
||
|
|
country: "",
|
||
|
|
city: "",
|
||
|
|
discountRate: 0,
|
||
|
|
employeeCount: 0,
|
||
|
|
startDate: "",
|
||
|
|
endDate: "",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<ConditionalDemoContent />
|
||
|
|
</UnifiedFormProvider>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ConditionalDemoContent() {
|
||
|
|
const { formData, setValue, evaluateCondition } = useUnifiedForm();
|
||
|
|
|
||
|
|
// 국가별 도시 데이터
|
||
|
|
const cityOptions: Record<string, Array<{ value: string; label: string }>> = {
|
||
|
|
korea: [
|
||
|
|
{ value: "seoul", label: "서울" },
|
||
|
|
{ value: "busan", label: "부산" },
|
||
|
|
{ value: "incheon", label: "인천" },
|
||
|
|
{ value: "daegu", label: "대구" },
|
||
|
|
],
|
||
|
|
japan: [
|
||
|
|
{ value: "tokyo", label: "도쿄" },
|
||
|
|
{ value: "osaka", label: "오사카" },
|
||
|
|
{ value: "kyoto", label: "교토" },
|
||
|
|
],
|
||
|
|
usa: [
|
||
|
|
{ value: "newyork", label: "뉴욕" },
|
||
|
|
{ value: "la", label: "로스앤젤레스" },
|
||
|
|
{ value: "chicago", label: "시카고" },
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
// 현재 선택된 국가의 도시 옵션
|
||
|
|
const currentCountry = formData.country as string;
|
||
|
|
const availableCities = currentCountry ? cityOptions[currentCountry] || [] : [];
|
||
|
|
|
||
|
|
// 조건부 설정
|
||
|
|
const showB2BFields: ConditionalConfig = {
|
||
|
|
enabled: true,
|
||
|
|
field: "contractType",
|
||
|
|
operator: "=",
|
||
|
|
value: "b2b",
|
||
|
|
action: "show",
|
||
|
|
};
|
||
|
|
|
||
|
|
const showB2CFields: ConditionalConfig = {
|
||
|
|
enabled: true,
|
||
|
|
field: "contractType",
|
||
|
|
operator: "=",
|
||
|
|
value: "b2c",
|
||
|
|
action: "show",
|
||
|
|
};
|
||
|
|
|
||
|
|
const showDiscountField: ConditionalConfig = {
|
||
|
|
enabled: true,
|
||
|
|
field: "isVip",
|
||
|
|
operator: "=",
|
||
|
|
value: true,
|
||
|
|
action: "show",
|
||
|
|
};
|
||
|
|
|
||
|
|
// 조건 평가
|
||
|
|
const b2bState = evaluateCondition("b2bFields", showB2BFields);
|
||
|
|
const b2cState = evaluateCondition("b2cFields", showB2CFields);
|
||
|
|
const discountState = evaluateCondition("discountRate", showDiscountField);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* 시나리오 1: 계약 유형에 따른 필드 표시 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">시나리오 1: 계약 유형별 필드 표시</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
계약 유형을 선택하면 해당 유형에 맞는 입력 필드가 나타납니다
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
{/* 계약 유형 선택 */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="contractType"
|
||
|
|
label="계약 유형"
|
||
|
|
required
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "radio",
|
||
|
|
source: "static",
|
||
|
|
options: [
|
||
|
|
{ value: "b2b", label: "B2B (기업간 거래)" },
|
||
|
|
{ value: "b2c", label: "B2C (소비자 거래)" },
|
||
|
|
],
|
||
|
|
}}
|
||
|
|
value={formData.contractType as string}
|
||
|
|
onChange={(v) => setValue("contractType", v)}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* B2B 전용 필드 */}
|
||
|
|
{b2bState.visible && (
|
||
|
|
<div className="p-4 bg-blue-50 rounded-lg space-y-4 border-l-4 border-blue-500">
|
||
|
|
<p className="text-sm font-medium text-blue-700">B2B 전용 입력 필드</p>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<UnifiedInput
|
||
|
|
id="companyName"
|
||
|
|
label="거래처 회사명"
|
||
|
|
required
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "text", placeholder: "회사명 입력" }}
|
||
|
|
value={formData.companyName as string || ""}
|
||
|
|
onChange={(v) => setValue("companyName", v)}
|
||
|
|
/>
|
||
|
|
<UnifiedInput
|
||
|
|
id="employeeCount"
|
||
|
|
label="직원 수"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "number", min: 1, max: 100000 }}
|
||
|
|
value={formData.employeeCount as number || 0}
|
||
|
|
onChange={(v) => setValue("employeeCount", v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* B2C 전용 필드 */}
|
||
|
|
{b2cState.visible && (
|
||
|
|
<div className="p-4 bg-green-50 rounded-lg space-y-4 border-l-4 border-green-500">
|
||
|
|
<p className="text-sm font-medium text-green-700">B2C 전용 입력 필드</p>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<UnifiedInput
|
||
|
|
id="customerName"
|
||
|
|
label="고객명"
|
||
|
|
required
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "text", placeholder: "고객명 입력" }}
|
||
|
|
value={formData.customerName as string || ""}
|
||
|
|
onChange={(v) => setValue("customerName", v)}
|
||
|
|
/>
|
||
|
|
<UnifiedInput
|
||
|
|
id="phone"
|
||
|
|
label="연락처"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "text", placeholder: "010-0000-0000" }}
|
||
|
|
value={formData.phone as string || ""}
|
||
|
|
onChange={(v) => setValue("phone", v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 시나리오 2: VIP 여부에 따른 필드 활성화 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">시나리오 2: 조건부 필드 활성화</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
VIP 고객 체크 시 할인율 입력 필드가 나타납니다
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<UnifiedSelect
|
||
|
|
id="isVip"
|
||
|
|
label="VIP 고객 여부"
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "toggle",
|
||
|
|
source: "static",
|
||
|
|
options: [
|
||
|
|
{ value: "false", label: "일반" },
|
||
|
|
{ value: "true", label: "VIP" },
|
||
|
|
],
|
||
|
|
}}
|
||
|
|
value={String(formData.isVip)}
|
||
|
|
onChange={(v) => setValue("isVip", v === "true")}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* VIP 전용 할인율 필드 */}
|
||
|
|
{discountState.visible && (
|
||
|
|
<div className="p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-500">
|
||
|
|
<p className="text-sm font-medium text-yellow-700 mb-3">VIP 전용 혜택 설정</p>
|
||
|
|
<UnifiedInput
|
||
|
|
id="discountRate"
|
||
|
|
label="할인율 (%)"
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{
|
||
|
|
type: "slider",
|
||
|
|
min: 0,
|
||
|
|
max: 50,
|
||
|
|
step: 5,
|
||
|
|
}}
|
||
|
|
value={formData.discountRate as number || 0}
|
||
|
|
onChange={(v) => setValue("discountRate", v)}
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-muted-foreground mt-2">
|
||
|
|
현재 할인율: {formData.discountRate || 0}%
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 시나리오 3: 연쇄 드롭다운 (국가 → 도시) */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">시나리오 3: 연쇄 드롭다운</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
국가를 선택하면 해당 국가의 도시만 표시됩니다
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
{/* 국가 선택 */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="country"
|
||
|
|
label="국가"
|
||
|
|
required
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "dropdown",
|
||
|
|
source: "static",
|
||
|
|
options: [
|
||
|
|
{ value: "korea", label: "대한민국" },
|
||
|
|
{ value: "japan", label: "일본" },
|
||
|
|
{ value: "usa", label: "미국" },
|
||
|
|
],
|
||
|
|
searchable: true,
|
||
|
|
}}
|
||
|
|
value={formData.country as string}
|
||
|
|
onChange={(v) => {
|
||
|
|
setValue("country", v);
|
||
|
|
setValue("city", ""); // 국가 변경 시 도시 초기화
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 도시 선택 (국가에 따라 옵션 변경) */}
|
||
|
|
<UnifiedSelect
|
||
|
|
id="city"
|
||
|
|
label="도시"
|
||
|
|
required
|
||
|
|
disabled={!currentCountry}
|
||
|
|
unifiedType="UnifiedSelect"
|
||
|
|
config={{
|
||
|
|
mode: "dropdown",
|
||
|
|
source: "static",
|
||
|
|
options: availableCities,
|
||
|
|
searchable: true,
|
||
|
|
}}
|
||
|
|
value={formData.city as string}
|
||
|
|
onChange={(v) => setValue("city", v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{!currentCountry && (
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
국가를 먼저 선택해주세요
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 조건부 설정 UI 데모 */}
|
||
|
|
<ConditionalConfigUIDemo formData={formData} />
|
||
|
|
|
||
|
|
{/* 현재 폼 데이터 표시 (하단으로 이동) */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">현재 폼 데이터</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
실시간으로 변경되는 폼 데이터를 확인합니다
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="p-4 bg-muted/50 rounded-lg">
|
||
|
|
<pre className="text-xs overflow-auto whitespace-pre-wrap">
|
||
|
|
{JSON.stringify(formData, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 조건부 설정 UI 패널 데모
|
||
|
|
*
|
||
|
|
* 비개발자도 UI로 조건부 설정을 할 수 있음을 보여주는 데모
|
||
|
|
*/
|
||
|
|
function ConditionalConfigUIDemo({ formData }: { formData: Record<string, unknown> }) {
|
||
|
|
const [demoConfig, setDemoConfig] = useState<ConditionalConfig | undefined>(undefined);
|
||
|
|
const { evaluateCondition } = useUnifiedForm();
|
||
|
|
|
||
|
|
// 데모용 필드 목록 (현재 폼의 필드들)
|
||
|
|
const availableFields = [
|
||
|
|
{ id: "contractType", label: "계약 유형", type: "select", options: [
|
||
|
|
{ value: "b2b", label: "B2B" },
|
||
|
|
{ value: "b2c", label: "B2C" },
|
||
|
|
]},
|
||
|
|
{ id: "isVip", label: "VIP 여부", type: "checkbox" },
|
||
|
|
{ id: "country", label: "국가", type: "select", options: [
|
||
|
|
{ value: "korea", label: "대한민국" },
|
||
|
|
{ value: "japan", label: "일본" },
|
||
|
|
{ value: "usa", label: "미국" },
|
||
|
|
]},
|
||
|
|
{ id: "discountRate", label: "할인율", type: "number" },
|
||
|
|
{ id: "employeeCount", label: "직원 수", type: "number" },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 현재 설정으로 조건 평가
|
||
|
|
const conditionResult = demoConfig ? evaluateCondition("demoField", demoConfig) : { visible: true, disabled: false };
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card className="border-orange-200 bg-orange-50/30">
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
||
|
|
<Zap className="h-5 w-5 text-orange-500" />
|
||
|
|
조건부 설정 UI 데모
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
화면관리에서 비개발자도 이 UI로 조건부 표시를 설정할 수 있습니다
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
|
{/* 왼쪽: 설정 UI */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium">설정 패널 (화면관리에 들어갈 UI)</h4>
|
||
|
|
<div className="border rounded-lg p-4 bg-white">
|
||
|
|
<ConditionalConfigPanel
|
||
|
|
config={demoConfig}
|
||
|
|
onChange={setDemoConfig}
|
||
|
|
availableFields={availableFields}
|
||
|
|
currentComponentId="demoField"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 오른쪽: 미리보기 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium">적용 결과 미리보기</h4>
|
||
|
|
<div className="border rounded-lg p-4 bg-white space-y-4">
|
||
|
|
{/* 조건 평가 결과 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<p className="text-xs text-muted-foreground">현재 조건 평가 결과:</p>
|
||
|
|
<div className="flex gap-4">
|
||
|
|
<Badge variant={conditionResult.visible ? "default" : "secondary"}>
|
||
|
|
{conditionResult.visible ? "표시됨" : "숨겨짐"}
|
||
|
|
</Badge>
|
||
|
|
<Badge variant={conditionResult.disabled ? "destructive" : "outline"}>
|
||
|
|
{conditionResult.disabled ? "비활성화" : "활성화"}
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* 대상 필드 미리보기 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<p className="text-xs text-muted-foreground">대상 필드:</p>
|
||
|
|
{conditionResult.visible ? (
|
||
|
|
<UnifiedInput
|
||
|
|
id="demoTargetField"
|
||
|
|
label="테스트 입력 필드"
|
||
|
|
disabled={conditionResult.disabled}
|
||
|
|
unifiedType="UnifiedInput"
|
||
|
|
config={{ type: "text", placeholder: "이 필드가 조건에 따라 표시/숨김됩니다" }}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<div className="p-4 border-2 border-dashed rounded-lg text-center text-muted-foreground text-sm">
|
||
|
|
(조건에 의해 숨겨진 필드)
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* 생성된 JSON */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<p className="text-xs text-muted-foreground">생성된 설정 JSON:</p>
|
||
|
|
<div className="p-3 bg-slate-900 rounded text-slate-100">
|
||
|
|
<pre className="text-[10px] overflow-auto">
|
||
|
|
{demoConfig ? JSON.stringify(demoConfig, null, 2) : "(설정 없음)"}
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 안내 메시지 */}
|
||
|
|
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||
|
|
<p className="text-xs text-blue-700">
|
||
|
|
위의 "조건부 표시" 스위치를 켜고 설정을 변경해보세요.
|
||
|
|
위 시나리오들의 필드 값을 변경하면 조건 평가 결과가 실시간으로 바뀝니다.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|