feat: Section Card/Paper 레이아웃 컴포넌트 추가 및 설정 패널 통합

새로운 그룹화 레이아웃 컴포넌트 2종 추가:
- Section Card: 제목+테두리 기반 명확한 섹션 구분
- Section Paper: 배경색 기반 미니멀한 섹션 구분

주요 변경사항:
- 새 컴포넌트 등록 (각 4개 파일: Component, ConfigPanel, Renderer, index)
- UnifiedPropertiesPanel에 인라인 설정 UI 추가 (280줄)
- DetailSettingsPanel ConfigPanel 인터페이스 통일화 (config → componentConfig)
- getComponentConfigPanel에 동적 import 매핑 추가
- 기존 컴포넌트 타입 정리 (autocomplete, entity-search, modal-repeater)

특징:
- shadcn/ui 기반 일관된 디자인 시스템 준수
- 중첩 박스 금지 원칙 적용
- 반응형 지원 (모바일 우선)
- collapsible 기능 지원 (Section Card)
This commit is contained in:
SeongHyun Kim 2025-11-18 11:28:22 +09:00
parent b09bd64083
commit 108af2a68b
16 changed files with 1098 additions and 23 deletions

View File

@ -883,10 +883,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
const config = currentConfig.config || definition.defaultConfig || {};
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return (
@ -895,7 +897,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
</div>
);
};

View File

@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
import {
@ -266,7 +267,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
const renderComponentConfigPanel = () => {
if (!selectedComponent) return null;
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
// 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지
const componentType =
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
selectedComponent.componentConfig?.type ||
selectedComponent.componentConfig?.id ||
selectedComponent.type;
const handleUpdateProperty = (path: string, value: any) => {
onUpdateProperty(selectedComponent.id, path, value);
@ -276,10 +282,15 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
const componentId =
selectedComponent.componentType || // ⭐ section-card 등
selectedComponent.componentConfig?.type ||
selectedComponent.componentConfig?.id;
if (componentId) {
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
const ConfigPanelComponent = definition.configPanel;
const currentConfig = selectedComponent.componentConfig || {};
@ -293,10 +304,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
const config = currentConfig.config || definition.defaultConfig || {};
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return (
@ -305,18 +318,19 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ConfigPanel 없음:", {
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
// ConfigPanel이 없으면 아래 switch case로 넘어감
}
}
@ -363,6 +377,280 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
case "badge-status":
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "section-card":
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Card </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 헤더 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={selectedComponent.componentConfig?.showHeader !== false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked);
}}
/>
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
</Label>
</div>
{/* 제목 */}
{selectedComponent.componentConfig?.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"></Label>
<Input
value={selectedComponent.componentConfig?.title || ""}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.title", e.target.value);
}}
placeholder="섹션 제목 입력"
className="h-9 text-xs"
/>
</div>
)}
{/* 설명 */}
{selectedComponent.componentConfig?.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"> ()</Label>
<Textarea
value={selectedComponent.componentConfig?.description || ""}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.description", e.target.value);
}}
placeholder="섹션 설명 입력"
className="text-xs resize-none"
rows={2}
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.padding || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (24px)</SelectItem>
<SelectItem value="lg"> (32px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.backgroundColor || "default"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ()</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="transparent"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 스타일 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.borderStyle || "solid"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.borderStyle", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid"></SelectItem>
<SelectItem value="dashed"></SelectItem>
<SelectItem value="none"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 접기/펼치기 기능 */}
<div className="space-y-2 pt-2 border-t">
<div className="flex items-center space-x-2">
<Checkbox
id="collapsible"
checked={selectedComponent.componentConfig?.collapsible || false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.collapsible", checked);
}}
/>
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
/
</Label>
</div>
{selectedComponent.componentConfig?.collapsible && (
<div className="flex items-center space-x-2 ml-6">
<Checkbox
id="defaultOpen"
checked={selectedComponent.componentConfig?.defaultOpen !== false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.defaultOpen", checked);
}}
/>
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
</Label>
</div>
)}
</div>
</div>
);
case "section-paper":
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Paper </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.backgroundColor || "default"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ( )</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="accent"> ( )</SelectItem>
<SelectItem value="primary"> </SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 커스텀 색상 */}
{selectedComponent.componentConfig?.backgroundColor === "custom" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="color"
value={selectedComponent.componentConfig?.customColor || "#f0f0f0"}
onChange={(e) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.customColor", e.target.value);
}}
className="h-9"
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.padding || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (16px)</SelectItem>
<SelectItem value="lg"> (24px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 둥근 모서리 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={selectedComponent.componentConfig?.roundedCorners || "md"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.roundedCorners", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (2px)</SelectItem>
<SelectItem value="md"> (6px)</SelectItem>
<SelectItem value="lg"> (8px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 그림자 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={selectedComponent.componentConfig?.shadow || "none"}
onValueChange={(value) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.shadow", value);
}}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"></SelectItem>
<SelectItem value="md"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showBorder"
checked={selectedComponent.componentConfig?.showBorder || false}
onCheckedChange={(checked) => {
handleUpdateProperty(selectedComponent.id, "componentConfig.showBorder", checked);
}}
/>
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
</Label>
</div>
</div>
);
default:
// ConfigPanel이 없는 경우 경고 표시
return (

View File

@ -4,6 +4,6 @@ import { ComponentRegistry } from "../../ComponentRegistry";
import { AutocompleteSearchInputDefinition } from "./index";
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(AutocompleteSearchInputDefinition);
console.log("✅ AutocompleteSearchInput 컴포넌트 등록 완료");
ComponentRegistry.registerComponent(AutocompleteSearchInputDefinition);
console.log("✅ AutocompleteSearchInput 컴포넌트 등록 완료");

View File

@ -4,6 +4,6 @@ import { ComponentRegistry } from "../../ComponentRegistry";
import { EntitySearchInputDefinition } from "./index";
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(EntitySearchInputDefinition);
console.log("✅ EntitySearchInput 컴포넌트 등록 완료");
ComponentRegistry.registerComponent(EntitySearchInputDefinition);
console.log("✅ EntitySearchInput 컴포넌트 등록 완료");

View File

@ -158,16 +158,16 @@ export function EntitySearchModal({
results.map((item, index) => {
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
return (
<tr
<tr
key={uniqueKey}
className="border-t hover:bg-accent cursor-pointer transition-colors"
onClick={() => handleSelect(item)}
>
className="border-t hover:bg-accent cursor-pointer transition-colors"
onClick={() => handleSelect(item)}
>
{displayColumns.map((col) => (
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
{item[col] || "-"}
</td>
))}
<td className="px-4 py-2">
<Button
size="sm"

View File

@ -55,6 +55,10 @@ import "./order-registration-modal/OrderRegistrationModalRenderer";
import "./conditional-container/ConditionalContainerRenderer";
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
// 🆕 섹션 그룹화 레이아웃 컴포넌트
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
/**
*
*/

View File

@ -4,6 +4,6 @@ import { ComponentRegistry } from "../../ComponentRegistry";
import { ModalRepeaterTableDefinition } from "./index";
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(ModalRepeaterTableDefinition);
console.log("✅ ModalRepeaterTable 컴포넌트 등록 완료");
ComponentRegistry.registerComponent(ModalRepeaterTableDefinition);
console.log("✅ ModalRepeaterTable 컴포넌트 등록 완료");

View File

@ -0,0 +1,178 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export interface SectionCardProps {
component?: {
id: string;
componentConfig?: {
title?: string;
description?: string;
showHeader?: boolean;
headerPosition?: "top" | "left";
padding?: "none" | "sm" | "md" | "lg";
backgroundColor?: "default" | "muted" | "transparent";
borderStyle?: "solid" | "dashed" | "none";
collapsible?: boolean;
defaultOpen?: boolean;
};
style?: React.CSSProperties;
};
children?: React.ReactNode;
className?: string;
onClick?: (e?: React.MouseEvent) => void;
isSelected?: boolean;
isDesignMode?: boolean;
}
/**
* Section Card
*
*/
export function SectionCardComponent({
component,
children,
className,
onClick,
isSelected = false,
isDesignMode = false,
}: SectionCardProps) {
const config = component?.componentConfig || {};
const [isOpen, setIsOpen] = React.useState(config.defaultOpen !== false);
// 🔄 실시간 업데이트를 위해 config에서 직접 읽기
const title = config.title || "";
const description = config.description || "";
const showHeader = config.showHeader !== false; // 기본값: true
const padding = config.padding || "md";
const backgroundColor = config.backgroundColor || "default";
const borderStyle = config.borderStyle || "solid";
const collapsible = config.collapsible || false;
// 🎯 디버깅: config 값 확인
React.useEffect(() => {
console.log("✅ Section Card Config:", {
title,
description,
showHeader,
fullConfig: config,
});
}, [config.title, config.description, config.showHeader]);
// 패딩 매핑
const paddingMap = {
none: "p-0",
sm: "p-3",
md: "p-6",
lg: "p-8",
};
// 배경색 매핑
const backgroundColorMap = {
default: "bg-card",
muted: "bg-muted/30",
transparent: "bg-transparent",
};
// 테두리 스타일 매핑
const borderStyleMap = {
solid: "border-solid",
dashed: "border-dashed",
none: "border-none",
};
const handleToggle = () => {
if (collapsible) {
setIsOpen(!isOpen);
}
};
return (
<Card
className={cn(
"transition-all",
backgroundColorMap[backgroundColor],
borderStyleMap[borderStyle],
borderStyle === "none" && "shadow-none",
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2",
isDesignMode && !children && "min-h-[150px]",
className
)}
style={component?.style}
onClick={onClick}
>
{/* 헤더 */}
{showHeader && (title || description || isDesignMode) && (
<CardHeader
className={cn(
"cursor-pointer",
collapsible && "hover:bg-accent/50 transition-colors"
)}
onClick={handleToggle}
>
<div className="flex items-center justify-between">
<div className="flex-1">
{(title || isDesignMode) && (
<CardTitle className="text-xl font-semibold">
{title || (isDesignMode ? "섹션 제목" : "")}
</CardTitle>
)}
{(description || isDesignMode) && (
<CardDescription className="text-sm text-muted-foreground mt-1.5">
{description || (isDesignMode ? "섹션 설명 (선택사항)" : "")}
</CardDescription>
)}
</div>
{collapsible && (
<div className={cn(
"ml-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
)}
</div>
</CardHeader>
)}
{/* 컨텐츠 */}
{(!collapsible || isOpen) && (
<CardContent className={cn(paddingMap[padding])}>
{/* 디자인 모드에서 빈 상태 안내 */}
{isDesignMode && !children && (
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
<div className="text-center">
<div className="mb-2">🃏 Section Card</div>
<div className="text-xs"> </div>
</div>
</div>
)}
{/* 자식 컴포넌트들 */}
{children}
</CardContent>
)}
</Card>
);
}

View File

@ -0,0 +1,172 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
interface SectionCardConfigPanelProps {
config: any;
onChange: (config: any) => void;
}
export function SectionCardConfigPanel({
config,
onChange,
}: SectionCardConfigPanelProps) {
const handleChange = (key: string, value: any) => {
const newConfig = {
...config,
[key]: value,
};
onChange(newConfig);
// 🎯 실시간 업데이트를 위한 이벤트 발생
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("componentConfigChanged", {
detail: { config: newConfig }
}));
}
};
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Card </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 헤더 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={config.showHeader !== false}
onCheckedChange={(checked) => handleChange("showHeader", checked)}
/>
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
</Label>
</div>
{/* 제목 */}
{config.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"></Label>
<Input
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="섹션 제목 입력"
className="h-9 text-xs"
/>
</div>
)}
{/* 설명 */}
{config.showHeader !== false && (
<div className="space-y-2">
<Label className="text-xs"> ()</Label>
<Textarea
value={config.description || ""}
onChange={(e) => handleChange("description", e.target.value)}
placeholder="섹션 설명 입력"
className="text-xs resize-none"
rows={2}
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={config.padding || "md"}
onValueChange={(value) => handleChange("padding", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (24px)</SelectItem>
<SelectItem value="lg"> (32px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={config.backgroundColor || "default"}
onValueChange={(value) => handleChange("backgroundColor", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ()</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="transparent"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 스타일 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={config.borderStyle || "solid"}
onValueChange={(value) => handleChange("borderStyle", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid"></SelectItem>
<SelectItem value="dashed"></SelectItem>
<SelectItem value="none"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 접기/펼치기 기능 */}
<div className="space-y-2 pt-2 border-t">
<div className="flex items-center space-x-2">
<Checkbox
id="collapsible"
checked={config.collapsible || false}
onCheckedChange={(checked) => handleChange("collapsible", checked)}
/>
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
/
</Label>
</div>
{config.collapsible && (
<div className="flex items-center space-x-2 ml-6">
<Checkbox
id="defaultOpen"
checked={config.defaultOpen !== false}
onCheckedChange={(checked) => handleChange("defaultOpen", checked)}
/>
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
</Label>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import React from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { SectionCardDefinition } from "./index";
import { SectionCardComponent } from "./SectionCardComponent";
/**
* Section Card
*
*/
export class SectionCardRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = SectionCardDefinition;
render(): React.ReactElement {
return <SectionCardComponent {...this.props} renderer={this} />;
}
}
// 자동 등록 실행
SectionCardRenderer.registerSelf();
// Hot Reload 지원 (개발 모드)
if (process.env.NODE_ENV === "development") {
SectionCardRenderer.enableHotReload();
}

View File

@ -0,0 +1,43 @@
"use client";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { SectionCardComponent } from "./SectionCardComponent";
import { SectionCardConfigPanel } from "./SectionCardConfigPanel";
/**
* Section Card
*
*/
export const SectionCardDefinition = createComponentDefinition({
id: "section-card",
name: "Section Card",
nameEng: "Section Card",
description: "제목과 테두리가 있는 명확한 그룹화 컨테이너",
category: ComponentCategory.LAYOUT,
webType: "custom",
component: SectionCardComponent,
defaultConfig: {
title: "섹션 제목",
description: "",
showHeader: true,
padding: "md",
backgroundColor: "default",
borderStyle: "solid",
collapsible: false,
defaultOpen: true,
},
defaultSize: { width: 800, height: 250 },
configPanel: SectionCardConfigPanel,
icon: "LayoutPanelTop",
tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"],
version: "1.0.0",
author: "WACE",
});
// 컴포넌트는 SectionCardRenderer에서 자동 등록됩니다
export { SectionCardComponent } from "./SectionCardComponent";
export { SectionCardConfigPanel } from "./SectionCardConfigPanel";
export { SectionCardRenderer } from "./SectionCardRenderer";

View File

@ -0,0 +1,138 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
export interface SectionPaperProps {
component?: {
id: string;
componentConfig?: {
backgroundColor?: "default" | "muted" | "accent" | "primary" | "custom";
customColor?: string;
showBorder?: boolean;
borderStyle?: "none" | "subtle";
padding?: "none" | "sm" | "md" | "lg";
roundedCorners?: "none" | "sm" | "md" | "lg";
shadow?: "none" | "sm" | "md";
};
style?: React.CSSProperties;
};
children?: React.ReactNode;
className?: string;
onClick?: (e?: React.MouseEvent) => void;
isSelected?: boolean;
isDesignMode?: boolean;
}
/**
* Section Paper
* ( )
*/
export function SectionPaperComponent({
component,
children,
className,
onClick,
isSelected = false,
isDesignMode = false,
}: SectionPaperProps) {
const config = component?.componentConfig || {};
// 배경색 매핑
const backgroundColorMap = {
default: "bg-muted/20",
muted: "bg-muted/30",
accent: "bg-accent/20",
primary: "bg-primary/5",
custom: "",
};
// 패딩 매핑
const paddingMap = {
none: "p-0",
sm: "p-3",
md: "p-4",
lg: "p-6",
};
// 둥근 모서리 매핑
const roundedMap = {
none: "rounded-none",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
};
// 그림자 매핑
const shadowMap = {
none: "",
sm: "shadow-sm",
md: "shadow-md",
};
const backgroundColor = config.backgroundColor || "default";
const padding = config.padding || "md";
const rounded = config.roundedCorners || "md";
const shadow = config.shadow || "none";
const showBorder = config.showBorder || false;
const borderStyle = config.borderStyle || "subtle";
// 커스텀 배경색 처리
const customBgStyle =
backgroundColor === "custom" && config.customColor
? { backgroundColor: config.customColor }
: {};
return (
<div
className={cn(
// 기본 스타일
"relative transition-colors",
// 배경색
backgroundColor !== "custom" && backgroundColorMap[backgroundColor],
// 패딩
paddingMap[padding],
// 둥근 모서리
roundedMap[rounded],
// 그림자
shadowMap[shadow],
// 테두리 (선택)
showBorder &&
borderStyle === "subtle" &&
"border border-border/30",
// 디자인 모드에서 선택된 상태
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2",
// 디자인 모드에서 빈 상태 표시
isDesignMode && !children && "min-h-[100px] border-2 border-dashed border-muted-foreground/30",
className
)}
style={{
...customBgStyle,
...component?.style,
}}
onClick={onClick}
>
{/* 디자인 모드에서 빈 상태 안내 */}
{isDesignMode && !children && (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
<div className="text-center">
<div className="mb-1">📄 Section Paper</div>
<div className="text-xs"> </div>
</div>
</div>
)}
{/* 자식 컴포넌트들 */}
{children}
</div>
);
}

View File

@ -0,0 +1,151 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
interface SectionPaperConfigPanelProps {
config: any;
onChange: (config: any) => void;
}
export function SectionPaperConfigPanel({
config,
onChange,
}: SectionPaperConfigPanelProps) {
const handleChange = (key: string, value: any) => {
const newConfig = {
...config,
[key]: value,
};
onChange(newConfig);
// 🎯 실시간 업데이트를 위한 이벤트 발생
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("componentConfigChanged", {
detail: { config: newConfig }
}));
}
};
return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold">Section Paper </h3>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={config.backgroundColor || "default"}
onValueChange={(value) => handleChange("backgroundColor", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ( )</SelectItem>
<SelectItem value="muted"></SelectItem>
<SelectItem value="accent"> ( )</SelectItem>
<SelectItem value="primary"> </SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 커스텀 색상 */}
{config.backgroundColor === "custom" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="color"
value={config.customColor || "#f0f0f0"}
onChange={(e) => handleChange("customColor", e.target.value)}
className="h-9"
/>
</div>
)}
{/* 패딩 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={config.padding || "md"}
onValueChange={(value) => handleChange("padding", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (12px)</SelectItem>
<SelectItem value="md"> (16px)</SelectItem>
<SelectItem value="lg"> (24px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 둥근 모서리 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={config.roundedCorners || "md"}
onValueChange={(value) => handleChange("roundedCorners", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"> (2px)</SelectItem>
<SelectItem value="md"> (6px)</SelectItem>
<SelectItem value="lg"> (8px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 그림자 */}
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select
value={config.shadow || "none"}
onValueChange={(value) => handleChange("shadow", value)}
>
<SelectTrigger className="h-9 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="sm"></SelectItem>
<SelectItem value="md"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 테두리 표시 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showBorder"
checked={config.showBorder || false}
onCheckedChange={(checked) => handleChange("showBorder", checked)}
/>
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
</Label>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import React from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { SectionPaperDefinition } from "./index";
import { SectionPaperComponent } from "./SectionPaperComponent";
/**
* Section Paper
*
*/
export class SectionPaperRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = SectionPaperDefinition;
render(): React.ReactElement {
return <SectionPaperComponent {...this.props} renderer={this} />;
}
}
// 자동 등록 실행
SectionPaperRenderer.registerSelf();
// Hot Reload 지원 (개발 모드)
if (process.env.NODE_ENV === "development") {
SectionPaperRenderer.enableHotReload();
}

View File

@ -0,0 +1,40 @@
"use client";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { SectionPaperComponent } from "./SectionPaperComponent";
import { SectionPaperConfigPanel } from "./SectionPaperConfigPanel";
/**
* Section Paper
* ( )
*/
export const SectionPaperDefinition = createComponentDefinition({
id: "section-paper",
name: "Section Paper",
nameEng: "Section Paper",
description: "배경색 기반의 미니멀한 그룹화 컨테이너 (색종이 컨셉)",
category: ComponentCategory.LAYOUT,
webType: "custom",
component: SectionPaperComponent,
defaultConfig: {
backgroundColor: "default",
padding: "md",
roundedCorners: "md",
shadow: "none",
showBorder: false,
},
defaultSize: { width: 800, height: 200 },
configPanel: SectionPaperConfigPanel,
icon: "Square",
tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"],
version: "1.0.0",
author: "WACE",
});
// 컴포넌트는 SectionPaperRenderer에서 자동 등록됩니다
export { SectionPaperComponent } from "./SectionPaperComponent";
export { SectionPaperConfigPanel } from "./SectionPaperConfigPanel";
export { SectionPaperRenderer } from "./SectionPaperRenderer";

View File

@ -39,6 +39,9 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
// 🆕 선택 항목 상세입력
"selected-items-detail-input": () =>
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
// 🆕 섹션 그룹화 레이아웃
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
};
// ConfigPanel 컴포넌트 캐시
@ -71,6 +74,8 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
module.ButtonConfigPanel || // button-primary의 export명
module.SectionCardConfigPanel || // section-card의 export명
module.SectionPaperConfigPanel || // section-paper의 export명
module.default;
if (!ConfigPanelComponent) {