feat: 조건부 컨테이너를 화면 선택 방식으로 개선

- ConditionalSection 타입 변경 (components[] → screenId, screenName)
  * 각 조건마다 컴포넌트를 직접 배치하는 대신 기존 화면을 선택
  * 복잡한 입력 폼도 화면 재사용으로 간단히 구성

- ConditionalSectionDropZone을 ConditionalSectionViewer로 교체
  * 드롭존 대신 InteractiveScreenViewer 사용
  * 선택된 화면을 조건별로 렌더링
  * 디자인 모드에서 화면 미선택 시 안내 메시지 표시

- ConfigPanel에서 화면 선택 드롭다운 구현
  * screenManagementApi.getScreenList()로 화면 목록 로드
  * 각 섹션마다 화면 선택 Select 컴포넌트
  * 선택된 화면의 ID와 이름 자동 저장 및 표시
  * 로딩 상태 표시

- 기본 설정 업데이트
  * defaultConfig에서 components 제거, screenId 추가
  * 모든 섹션 기본값을 screenId: null로 설정

- README 문서 개선
  * 화면 선택 방식으로 사용법 업데이트
  * 사용 사례에 화면 ID 예시 추가
  * 구조 다이어그램 수정 (드롭존 → 화면 표시)
  * 디자인/실행 모드 설명 업데이트

장점:
- 기존 화면 재사용으로 생산성 향상
- 복잡한 입력 폼도 간단히 조건부 표시
- 화면 수정 시 자동 반영
- 유지보수 용이
This commit is contained in:
kjs 2025-11-14 17:40:07 +09:00
parent e6949bdd67
commit f5756e184f
9 changed files with 1078 additions and 1 deletions

View File

@ -0,0 +1,162 @@
"use client";
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ConditionalContainerProps, ConditionalSection } from "./types";
import { ConditionalSectionViewer } from "./ConditionalSectionViewer";
import { cn } from "@/lib/utils";
/**
*
* UI를
*/
export function ConditionalContainerComponent({
config,
controlField: propControlField,
controlLabel: propControlLabel,
sections: propSections,
defaultValue: propDefaultValue,
showBorder: propShowBorder,
spacing: propSpacing,
value,
onChange,
formData,
onFormDataChange,
isDesignMode = false,
onUpdateComponent,
onDeleteComponent,
onSelectComponent,
selectedComponentId,
style,
className,
}: ConditionalContainerProps) {
// config prop 우선, 없으면 개별 prop 사용
const controlField = config?.controlField || propControlField || "condition";
const controlLabel = config?.controlLabel || propControlLabel || "조건 선택";
const sections = config?.sections || propSections || [];
const defaultValue = config?.defaultValue || propDefaultValue || sections[0]?.condition;
const showBorder = config?.showBorder ?? propShowBorder ?? true;
const spacing = config?.spacing || propSpacing || "normal";
// 현재 선택된 값
const [selectedValue, setSelectedValue] = useState<string>(
value || formData?.[controlField] || defaultValue || ""
);
// formData 변경 시 동기화
useEffect(() => {
if (formData?.[controlField]) {
setSelectedValue(formData[controlField]);
}
}, [formData, controlField]);
// 값 변경 핸들러
const handleValueChange = (newValue: string) => {
setSelectedValue(newValue);
if (onChange) {
onChange(newValue);
}
if (onFormDataChange) {
onFormDataChange(controlField, newValue);
}
};
// 간격 스타일
const spacingClass = {
tight: "space-y-2",
normal: "space-y-4",
loose: "space-y-8",
}[spacing];
return (
<div
className={cn("h-full w-full flex flex-col", spacingClass, className)}
style={style}
>
{/* 제어 셀렉트박스 */}
<div className="space-y-2 flex-shrink-0">
<Label htmlFor={controlField} className="text-xs sm:text-sm">
{controlLabel}
</Label>
<Select value={selectedValue} onValueChange={handleValueChange}>
<SelectTrigger
id={controlField}
className="h-8 text-xs sm:h-10 sm:text-sm"
>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
{sections.map((section) => (
<SelectItem key={section.id} value={section.condition}>
{section.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 조건별 섹션들 */}
<div className="flex-1 min-h-0 overflow-auto">
{isDesignMode ? (
// 디자인 모드: 모든 섹션 표시
<div className={spacingClass}>
{sections.map((section) => (
<ConditionalSectionViewer
key={section.id}
sectionId={section.id}
condition={section.condition}
label={section.label}
screenId={section.screenId}
screenName={section.screenName}
isActive={selectedValue === section.condition}
isDesignMode={isDesignMode}
showBorder={showBorder}
formData={formData}
onFormDataChange={onFormDataChange}
/>
))}
</div>
) : (
// 실행 모드: 활성 섹션만 표시
sections.map((section) =>
selectedValue === section.condition ? (
<ConditionalSectionViewer
key={section.id}
sectionId={section.id}
condition={section.condition}
label={section.label}
screenId={section.screenId}
screenName={section.screenName}
isActive={true}
isDesignMode={false}
showBorder={showBorder}
formData={formData}
onFormDataChange={onFormDataChange}
/>
) : null
)
)}
{/* 섹션이 없는 경우 안내 */}
{sections.length === 0 && isDesignMode && (
<div className="flex items-center justify-center min-h-[200px] border-2 border-dashed border-muted-foreground/30 rounded-lg bg-muted/20">
<p className="text-sm text-muted-foreground">
</p>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,336 @@
"use client";
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Trash2, GripVertical, Loader2 } from "lucide-react";
import { ConditionalContainerConfig, ConditionalSection } from "./types";
import { screenManagementApi } from "@/lib/api/screenManagement";
interface ConditionalContainerConfigPanelProps {
config: ConditionalContainerConfig;
onConfigChange: (config: ConditionalContainerConfig) => void;
}
export function ConditionalContainerConfigPanel({
config,
onConfigChange,
}: ConditionalContainerConfigPanelProps) {
const [localConfig, setLocalConfig] = useState<ConditionalContainerConfig>({
controlField: config.controlField || "condition",
controlLabel: config.controlLabel || "조건 선택",
sections: config.sections || [],
defaultValue: config.defaultValue || "",
showBorder: config.showBorder ?? true,
spacing: config.spacing || "normal",
});
// 화면 목록 상태
const [screens, setScreens] = useState<any[]>([]);
const [screensLoading, setScreensLoading] = useState(false);
// 화면 목록 로드
useEffect(() => {
const loadScreens = async () => {
setScreensLoading(true);
try {
const response = await screenManagementApi.getScreenList();
if (response.success && response.data) {
setScreens(response.data);
}
} catch (error) {
console.error("화면 목록 로드 실패:", error);
} finally {
setScreensLoading(false);
}
};
loadScreens();
}, []);
// 설정 업데이트 헬퍼
const updateConfig = (updates: Partial<ConditionalContainerConfig>) => {
const newConfig = { ...localConfig, ...updates };
setLocalConfig(newConfig);
onConfigChange(newConfig);
};
// 새 섹션 추가
const addSection = () => {
const newSection: ConditionalSection = {
id: `section_${Date.now()}`,
condition: `condition_${localConfig.sections.length + 1}`,
label: `조건 ${localConfig.sections.length + 1}`,
screenId: null,
screenName: undefined,
};
updateConfig({
sections: [...localConfig.sections, newSection],
});
};
// 섹션 삭제
const removeSection = (sectionId: string) => {
updateConfig({
sections: localConfig.sections.filter((s) => s.id !== sectionId),
});
};
// 섹션 업데이트
const updateSection = (
sectionId: string,
updates: Partial<ConditionalSection>
) => {
updateConfig({
sections: localConfig.sections.map((s) =>
s.id === sectionId ? { ...s, ...updates } : s
),
});
};
return (
<div className="space-y-6 p-4">
<div>
<h3 className="text-sm font-semibold mb-4"> </h3>
{/* 제어 필드 설정 */}
<div className="space-y-4 mb-6">
<div className="space-y-2">
<Label htmlFor="controlField" className="text-xs">
</Label>
<Input
id="controlField"
value={localConfig.controlField}
onChange={(e) => updateConfig({ controlField: e.target.value })}
placeholder="예: inputMode"
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
formData에
</p>
</div>
<div className="space-y-2">
<Label htmlFor="controlLabel" className="text-xs">
</Label>
<Input
id="controlLabel"
value={localConfig.controlLabel}
onChange={(e) => updateConfig({ controlLabel: e.target.value })}
placeholder="예: 입력 방식"
className="h-8 text-xs"
/>
</div>
</div>
{/* 조건별 섹션 설정 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold"> </Label>
<Button
onClick={addSection}
size="sm"
variant="outline"
className="h-7 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{localConfig.sections.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed rounded-lg">
<p className="text-xs text-muted-foreground">
</p>
</div>
) : (
<div className="space-y-3">
{localConfig.sections.map((section, index) => (
<div
key={section.id}
className="p-3 border rounded-lg space-y-3 bg-muted/20"
>
{/* 섹션 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">
{index + 1}
</span>
</div>
<Button
onClick={() => removeSection(section.id)}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 조건 값 */}
<div className="space-y-1.5">
<Label className="text-[10px] text-muted-foreground">
()
</Label>
<Input
value={section.condition}
onChange={(e) =>
updateSection(section.id, { condition: e.target.value })
}
placeholder="예: customer_first"
className="h-7 text-xs"
/>
</div>
{/* 조건 라벨 */}
<div className="space-y-1.5">
<Label className="text-[10px] text-muted-foreground">
</Label>
<Input
value={section.label}
onChange={(e) =>
updateSection(section.id, { label: e.target.value })
}
placeholder="예: 거래처 우선"
className="h-7 text-xs"
/>
</div>
{/* 화면 선택 */}
<div className="space-y-1">
<Label className="text-[10px] text-muted-foreground">
</Label>
{screensLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground h-7 px-3 border rounded">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : (
<Select
value={section.screenId?.toString() || ""}
onValueChange={(value) => {
const screenId = value ? parseInt(value) : null;
const selectedScreen = screens.find(
(s) => s.id === screenId
);
updateSection(section.id, {
screenId,
screenName: selectedScreen?.screenName,
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="화면 선택..." />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
{screens.map((screen) => (
<SelectItem
key={screen.id}
value={screen.id.toString()}
>
{screen.screenName}
{screen.description && (
<span className="text-[10px] text-muted-foreground ml-1">
({screen.description})
</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{section.screenId && (
<div className="text-[10px] text-muted-foreground">
ID: {section.screenId}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* 기본값 설정 */}
{localConfig.sections.length > 0 && (
<div className="space-y-2 mt-4">
<Label htmlFor="defaultValue" className="text-xs">
</Label>
<Select
value={localConfig.defaultValue || ""}
onValueChange={(value) => updateConfig({ defaultValue: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="기본값 선택" />
</SelectTrigger>
<SelectContent>
{localConfig.sections.map((section) => (
<SelectItem key={section.id} value={section.condition}>
{section.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 스타일 설정 */}
<div className="space-y-4 mt-6 pt-6 border-t">
<Label className="text-xs font-semibold"> </Label>
{/* 테두리 표시 */}
<div className="flex items-center justify-between">
<Label htmlFor="showBorder" className="text-xs">
</Label>
<Switch
id="showBorder"
checked={localConfig.showBorder}
onCheckedChange={(checked) =>
updateConfig({ showBorder: checked })
}
/>
</div>
{/* 간격 설정 */}
<div className="space-y-2">
<Label htmlFor="spacing" className="text-xs">
</Label>
<Select
value={localConfig.spacing || "normal"}
onValueChange={(value: any) => updateConfig({ spacing: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tight"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="loose"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,20 @@
"use client";
import React from "react";
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import ConditionalContainerDefinition from "./index";
import { ConditionalContainerComponent } from "./ConditionalContainerComponent";
import { ConditionalContainerConfigPanel } from "./ConditionalContainerConfigPanel";
// 컴포넌트 자동 등록
if (typeof window !== "undefined") {
ComponentRegistry.registerComponent({
...ConditionalContainerDefinition,
component: ConditionalContainerComponent,
renderer: ConditionalContainerComponent,
configPanel: ConditionalContainerConfigPanel,
} as any);
}
export { ConditionalContainerComponent };

View File

@ -0,0 +1,89 @@
"use client";
import React, { useState, useEffect } from "react";
import { ConditionalSectionViewerProps } from "./types";
import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewer";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
/**
*
*
*/
export function ConditionalSectionViewer({
sectionId,
condition,
label,
screenId,
screenName,
isActive,
isDesignMode,
showBorder = true,
formData,
onFormDataChange,
}: ConditionalSectionViewerProps) {
const [isLoading, setIsLoading] = useState(false);
// 디자인 모드가 아니고 비활성 섹션이면 렌더링하지 않음
if (!isDesignMode && !isActive) {
return null;
}
return (
<div
className={cn(
"relative min-h-[200px] transition-all",
showBorder && "rounded-lg border-2",
isDesignMode ? (
"border-dashed border-muted-foreground/30 bg-muted/20"
) : (
showBorder ? "border-border bg-card" : ""
),
!isDesignMode && !isActive && "hidden"
)}
data-section-id={sectionId}
>
{/* 섹션 라벨 (디자인 모드에서만 표시) */}
{isDesignMode && (
<div className="absolute -top-3 left-4 bg-background px-2 text-xs font-medium text-muted-foreground z-10">
{label} {isActive && "(활성)"}
{screenId && ` - 화면 ID: ${screenId}`}
</div>
)}
{/* 화면 미선택 안내 (디자인 모드 + 화면 없을 때) */}
{isDesignMode && !screenId && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<p className="text-sm"> </p>
<p className="text-xs mt-1">: {condition}</p>
</div>
</div>
)}
{/* 로딩 중 */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-20">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<p className="text-xs text-muted-foreground"> ...</p>
</div>
</div>
)}
{/* 화면 렌더링 */}
{screenId && (
<div className="relative p-4 min-h-[200px]">
<InteractiveScreenViewer
screenId={screenId}
formData={formData}
onFormDataChange={onFormDataChange}
mode="view" // 항상 뷰 모드로 표시
isInModal={true} // 모달 내부처럼 처리
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,293 @@
# 조건부 컨테이너 (ConditionalContainer) - 화면 선택 방식
제어 셀렉트박스 값에 따라 다른 **화면**을 표시하는 조건부 컨테이너 컴포넌트입니다.
## 📋 개요
화면 편집기에서 조건별로 표시할 화면을 선택하여 조건부 UI를 구성할 수 있는 컨테이너입니다. 상단의 셀렉트박스 값에 따라 하단에 미리 만들어진 화면을 표시합니다.
## ✨ 주요 기능
- ✅ **조건별 화면 전환**: 셀렉트박스 값에 따라 다른 화면 표시
- ✅ **화면 재사용**: 기존에 만든 화면을 조건별로 할당
- ✅ **간편한 구성**: 복잡한 입력 폼도 화면 선택으로 간단히 구성
- ✅ **자동 동기화**: 화면 수정 시 자동 반영
- ✅ **폼 데이터 연동**: formData와 자동 동기화
- ✅ **커스터마이징**: 테두리, 간격, 기본값 등 설정 가능
## 🎯 사용 사례
### 1. 입력 방식 선택
```
[셀렉트: 입력 방식]
├─ 거래처 우선: "거래처_우선_입력_화면" (화면 ID: 101)
├─ 견적서 기반: "견적서_업로드_화면" (화면 ID: 102)
└─ 단가 직접입력: "단가_직접입력_화면" (화면 ID: 103)
```
### 2. 판매 유형 선택
```
[셀렉트: 판매 유형]
├─ 국내 판매: "국내판매_기본폼" (화면 ID: 201)
└─ 해외 판매: "해외판매_무역정보폼" (화면 ID: 202)
```
### 3. 문서 유형 선택
```
[셀렉트: 문서 유형]
├─ 신규 작성: "신규문서_입력폼" (화면 ID: 301)
├─ 복사 생성: "문서복사_화면" (화면 ID: 302)
└─ 불러오기: "파일업로드_화면" (화면 ID: 303)
```
## 📐 구조
```
┌─────────────────────────────────┐
│ ConditionalContainer │
├─────────────────────────────────┤
│ [제어 셀렉트박스] │ ← controlField, controlLabel
├─────────────────────────────────┤
│ 📄 조건 1: "옵션 A" 선택 시 │ ← sections[0]
│ ┌─────────────────────────────┐│
│ │ [선택된 화면이 표시됨] ││ ← screenId로 지정된 화면
│ │ (화면 ID: 101) ││
│ │ ││
│ └─────────────────────────────┘│
├─────────────────────────────────┤
│ 📄 조건 2: "옵션 B" 선택 시 │ ← sections[1]
│ ┌─────────────────────────────┐│
│ │ [다른 화면이 표시됨] ││ ← screenId로 지정된 다른 화면
│ │ (화면 ID: 102) ││
│ └─────────────────────────────┘│
└─────────────────────────────────┘
```
## 🔧 설정 방법
### 1. 컴포넌트 추가
화면 편집기의 컴포넌트 패널에서 **"조건부 컨테이너"**를 드래그하여 캔버스에 배치합니다.
### 2. 설정 패널에서 구성
#### 제어 필드 설정
- **제어 필드명**: formData에 저장될 필드명 (예: `inputMode`)
- **셀렉트박스 라벨**: 화면에 표시될 라벨 (예: "입력 방식")
#### 조건별 섹션 추가
1. **"섹션 추가"** 버튼 클릭
2. 각 섹션 설정:
- **조건 값**: 고유한 값 (예: `customer_first`)
- **표시 라벨**: 사용자에게 보이는 텍스트 (예: "거래처 우선")
#### 기본값 설정
- 처음 화면 로드 시 선택될 기본 조건 선택
#### 스타일 설정
- **섹션 테두리 표시**: ON/OFF
- **섹션 간격**: 좁게 / 보통 / 넓게
### 3. 조건별 화면 선택
1. **디자인 모드**에서 모든 조건 섹션이 표시됩니다
2. 각 섹션의 **"표시할 화면"** 드롭다운에서 화면을 선택합니다
3. 선택된 화면 ID와 이름이 자동으로 저장됩니다
**장점:**
- ✅ 이미 만든 화면을 재사용
- ✅ 복잡한 입력 폼도 간단히 구성
- ✅ 화면 수정 시 자동 반영
### 4. 실행 모드 동작
- 셀렉트박스에서 조건 선택
- 선택된 조건의 **화면**이 표시됨
- 다른 조건의 화면은 자동으로 숨김
## 💻 기술 사양
### Props
```typescript
interface ConditionalContainerProps {
// 제어 필드
controlField: string; // 예: "inputMode"
controlLabel: string; // 예: "입력 방식"
// 조건별 섹션
sections: ConditionalSection[];
// 기본값
defaultValue?: string;
// 스타일
showBorder?: boolean; // 기본: true
spacing?: "tight" | "normal" | "loose"; // 기본: "normal"
// 폼 연동
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
}
interface ConditionalSection {
id: string; // 고유 ID
condition: string; // 조건 값
label: string; // 표시 라벨
screenId: number | null; // 표시할 화면 ID
screenName?: string; // 화면 이름 (표시용)
}
```
### 기본 설정
```typescript
defaultSize: {
width: 800,
height: 600,
}
defaultConfig: {
controlField: "condition",
controlLabel: "조건 선택",
sections: [
{
id: "section_1",
condition: "option1",
label: "옵션 1",
screenId: null, // 화면 미선택 상태
},
{
id: "section_2",
condition: "option2",
label: "옵션 2",
screenId: null, // 화면 미선택 상태
},
],
defaultValue: "option1",
showBorder: true,
spacing: "normal",
}
```
## 🎨 디자인 모드 vs 실행 모드
### 디자인 모드 (편집기)
- ✅ 모든 조건 섹션 표시
- ✅ 각 섹션에 "조건: XXX" 라벨 표시
- ✅ 화면 선택 안내 메시지 (미선택 시)
- ✅ 선택된 화면 ID 표시
- ✅ 활성 조건 "(활성)" 표시
### 실행 모드 (할당된 화면)
- ✅ 선택된 조건의 화면만 표시
- ✅ 다른 조건의 화면 자동 숨김
- ✅ 깔끔한 UI (라벨, 점선 테두리 제거)
- ✅ 선택된 화면이 완전히 통합되어 표시
## 📊 폼 데이터 연동
### 자동 동기화
```typescript
// formData 읽기
formData[controlField] // 현재 선택된 값
// formData 쓰기
onFormDataChange(controlField, newValue)
```
### 예시
```typescript
// controlField = "salesType"
formData = {
salesType: "export", // ← 자동으로 여기에 저장됨
// ... 다른 필드들
}
// 셀렉트박스 값 변경 시 자동으로 formData 업데이트
```
## 🔍 주의사항
1. **조건 값은 고유해야 함**: 각 섹션의 `condition` 값은 중복되면 안 됩니다
2. **최소 1개 섹션 필요**: 섹션이 없으면 안내 메시지 표시
3. **컴포넌트 ID 충돌 방지**: 각 섹션의 컴포넌트 ID는 전역적으로 고유해야 함
## 📝 예시: 수주 입력 방식 선택
```typescript
{
controlField: "inputMode",
controlLabel: "입력 방식",
sections: [
{
id: "customer_first",
condition: "customer_first",
label: "거래처 우선",
components: [
// 거래처 검색 컴포넌트
// 품목 선택 테이블
// 저장 버튼
]
},
{
id: "quotation",
condition: "quotation",
label: "견적서 기반",
components: [
// 견적서 검색 컴포넌트
// 견적서 내용 표시
// 수주 전환 버튼
]
},
{
id: "unit_price",
condition: "unit_price",
label: "단가 직접입력",
components: [
// 품목 입력 테이블
// 단가 입력 필드들
// 계산 위젯
]
}
],
defaultValue: "customer_first",
showBorder: true,
spacing: "normal"
}
```
## 🚀 로드맵
- [ ] 다중 제어 필드 지원 (AND/OR 조건)
- [ ] 섹션 전환 애니메이션
- [ ] 조건별 검증 규칙
- [ ] 템플릿 저장/불러오기
## 🐛 트러블슈팅
### Q: 섹션이 전환되지 않아요
A: `controlField` 값이 formData에 제대로 저장되고 있는지 확인하세요.
### Q: 컴포넌트가 드롭되지 않아요
A: 디자인 모드인지 확인하고, 드롭존 영역에 정확히 드롭하세요.
### Q: 다른 조건의 UI가 계속 보여요
A: 실행 모드로 전환했는지 확인하세요. 디자인 모드에서는 모든 조건이 표시됩니다.
## 📦 파일 구조
```
conditional-container/
├── types.ts # 타입 정의
├── ConditionalContainerComponent.tsx # 메인 컴포넌트
├── ConditionalSectionDropZone.tsx # 드롭존 컴포넌트
├── ConditionalContainerConfigPanel.tsx # 설정 패널
├── ConditionalContainerRenderer.tsx # 렌더러 및 등록
├── index.ts # 컴포넌트 정의
└── README.md # 이 파일
```
## 🎉 완료!
이제 화면 편집기에서 **조건부 컨테이너**를 사용하여 동적인 UI를 만들 수 있습니다! 🚀

View File

@ -0,0 +1,96 @@
/**
*
* UI를
*/
import { ComponentDefinition, ComponentCategory } from "@/types/component";
export const ConditionalContainerDefinition: Omit<
ComponentDefinition,
"renderer" | "configPanel" | "component"
> = {
id: "conditional-container",
name: "조건부 컨테이너",
category: ComponentCategory.LAYOUT,
webType: "container" as const,
description: "셀렉트박스 값에 따라 다른 UI를 표시하는 조건부 컨테이너",
icon: "GitBranch",
version: "1.0.0",
author: "WACE",
tags: ["조건부", "분기", "동적", "레이아웃"],
defaultSize: {
width: 800,
height: 600,
},
defaultConfig: {
controlField: "condition",
controlLabel: "조건 선택",
sections: [
{
id: "section_1",
condition: "option1",
label: "옵션 1",
screenId: null,
},
{
id: "section_2",
condition: "option2",
label: "옵션 2",
screenId: null,
},
],
defaultValue: "option1",
showBorder: true,
spacing: "normal",
},
defaultProps: {
style: {
width: "800px",
height: "600px",
},
},
configSchema: {
controlField: {
type: "string",
label: "제어 필드명",
defaultValue: "condition",
},
controlLabel: {
type: "string",
label: "셀렉트박스 라벨",
defaultValue: "조건 선택",
},
sections: {
type: "array",
label: "조건별 섹션",
defaultValue: [],
},
defaultValue: {
type: "string",
label: "기본 선택 값",
defaultValue: "",
},
showBorder: {
type: "boolean",
label: "섹션 테두리 표시",
defaultValue: true,
},
spacing: {
type: "select",
label: "섹션 간격",
options: [
{ label: "좁게", value: "tight" },
{ label: "보통", value: "normal" },
{ label: "넓게", value: "loose" },
],
defaultValue: "normal",
},
},
};
export default ConditionalContainerDefinition;

View File

@ -0,0 +1,75 @@
/**
* ConditionalContainer
* UI를
*/
import { ComponentData } from "@/types/screen";
export interface ConditionalSection {
id: string; // 고유 ID
condition: string; // 조건 값 (예: "customer_first", "quotation")
label: string; // 조건 라벨 (예: "거래처 우선", "견적서 기반")
screenId: number | null; // 이 조건일 때 표시할 화면 ID
screenName?: string; // 화면 이름 (표시용)
}
export interface ConditionalContainerConfig {
// 제어 셀렉트박스 설정
controlField: string; // 제어할 필드명 (예: "inputMode")
controlLabel: string; // 셀렉트박스 라벨 (예: "입력 방식")
// 조건별 섹션
sections: ConditionalSection[];
// 기본 선택 값
defaultValue?: string;
// 스타일
showBorder?: boolean; // 섹션별 테두리 표시
spacing?: "tight" | "normal" | "loose"; // 섹션 간격
}
export interface ConditionalContainerProps {
config?: ConditionalContainerConfig;
// 개별 props (config 우선)
controlField?: string;
controlLabel?: string;
sections?: ConditionalSection[];
defaultValue?: string;
showBorder?: boolean;
spacing?: "tight" | "normal" | "loose";
// 폼 데이터 연동
value?: any; // 현재 선택된 값
onChange?: (value: string) => void;
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
// 화면 편집기 관련
isDesignMode?: boolean; // 디자인 모드 여부
onUpdateComponent?: (componentId: string, updates: Partial<ComponentData>) => void;
onDeleteComponent?: (componentId: string) => void;
onSelectComponent?: (componentId: string) => void;
selectedComponentId?: string;
// 스타일
style?: React.CSSProperties;
className?: string;
}
// 조건부 섹션 뷰어 Props
export interface ConditionalSectionViewerProps {
sectionId: string;
condition: string;
label: string;
screenId: number | null; // 표시할 화면 ID
screenName?: string; // 화면 이름
isActive: boolean; // 현재 조건이 활성화되어 있는지
isDesignMode: boolean;
showBorder?: boolean;
// 폼 데이터 전달
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
}

View File

@ -50,6 +50,9 @@ import { EntitySearchInputRenderer } from "./entity-search-input/EntitySearchInp
import { ModalRepeaterTableRenderer } from "./modal-repeater-table/ModalRepeaterTableRenderer";
import "./order-registration-modal/OrderRegistrationModalRenderer";
// 🆕 조건부 컨테이너 컴포넌트
import "./conditional-container/ConditionalContainerRenderer";
/**
*
*/

View File

@ -31,6 +31,8 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"),
"modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"),
"order-registration-modal": () => import("@/lib/registry/components/order-registration-modal/OrderRegistrationModalConfigPanel"),
// 🆕 조건부 컨테이너
"conditional-container": () => import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"),
};
// ConfigPanel 컴포넌트 캐시
@ -261,7 +263,8 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
"autocomplete-search-input",
"entity-search-input",
"modal-repeater-table",
"order-registration-modal"
"order-registration-modal",
"conditional-container"
].includes(componentId);
if (isSimpleConfigPanel) {