ERP-node/frontend/components/unified/UnifiedComponentsDemo.tsx

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>
);
}