Compare commits
7 Commits
e1a5befdf7
...
5f026e88ab
| Author | SHA1 | Date |
|---|---|---|
|
|
5f026e88ab | |
|
|
8272361063 | |
|
|
1a82c8ea94 | |
|
|
108af2a68b | |
|
|
b09bd64083 | |
|
|
23cd677413 | |
|
|
aee4e86036 |
|
|
@ -17,6 +17,15 @@ export async function searchEntity(req: Request, res: Response) {
|
||||||
limit = "20",
|
limit = "20",
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
|
// tableName 유효성 검증
|
||||||
|
if (!tableName || tableName === "undefined" || tableName === "null") {
|
||||||
|
logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName });
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 멀티테넌시
|
// 멀티테넌시
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { ComponentData } from "@/types/screen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
|
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||||
|
|
||||||
interface ScreenModalState {
|
interface ScreenModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -354,7 +355,13 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
if (newModalId) {
|
if (newModalId) {
|
||||||
setPersistedModalId(newModalId);
|
setPersistedModalId(newModalId);
|
||||||
}
|
}
|
||||||
}, [modalState.isOpen, modalState.screenId, modalState.title, screenData?.screenInfo?.tableName, screenData?.screenInfo?.screenName]);
|
}, [
|
||||||
|
modalState.isOpen,
|
||||||
|
modalState.screenId,
|
||||||
|
modalState.title,
|
||||||
|
screenData?.screenInfo?.tableName,
|
||||||
|
screenData?.screenInfo?.screenName,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||||
|
|
@ -397,7 +404,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
) : screenData ? (
|
) : screenData ? (
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div
|
<div
|
||||||
className="relative bg-white mx-auto"
|
className="relative mx-auto bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: `${screenDimensions?.width || 800}px`,
|
width: `${screenDimensions?.width || 800}px`,
|
||||||
height: `${screenDimensions?.height || 600}px`,
|
height: `${screenDimensions?.height || 600}px`,
|
||||||
|
|
@ -410,14 +417,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const offsetY = screenDimensions?.offsetY || 0;
|
const offsetY = screenDimensions?.offsetY || 0;
|
||||||
|
|
||||||
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
|
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
|
||||||
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
|
const adjustedComponent =
|
||||||
...component,
|
offsetX === 0 && offsetY === 0
|
||||||
position: {
|
? component
|
||||||
...component.position,
|
: {
|
||||||
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
...component,
|
||||||
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
position: {
|
||||||
},
|
...component.position,
|
||||||
};
|
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
||||||
|
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveScreenViewerDynamic
|
<InteractiveScreenViewerDynamic
|
||||||
|
|
@ -472,10 +482,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// console.log("🔄 연속 모드 변경:", isChecked);
|
// console.log("🔄 연속 모드 변경:", isChecked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
||||||
htmlFor="continuous-mode"
|
|
||||||
className="text-sm font-normal cursor-pointer select-none"
|
|
||||||
>
|
|
||||||
저장 후 계속 입력 (연속 등록 모드)
|
저장 후 계속 입력 (연속 등록 모드)
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -272,19 +272,15 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
right: undefined,
|
right: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 디버깅: 크기 정보 로그
|
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
||||||
if (component.id && isSelected) {
|
// if (component.id && isSelected) {
|
||||||
console.log("📐 RealtimePreview baseStyle:", {
|
// console.log("📐 RealtimePreview baseStyle:", {
|
||||||
componentId: component.id,
|
// componentId: component.id,
|
||||||
componentType: (component as any).componentType || component.type,
|
// componentType: (component as any).componentType || component.type,
|
||||||
sizeWidth: size?.width,
|
// sizeWidth: size?.width,
|
||||||
sizeHeight: size?.height,
|
// sizeHeight: size?.height,
|
||||||
styleWidth: componentStyle?.width,
|
// });
|
||||||
styleHeight: componentStyle?.height,
|
// }
|
||||||
baseStyleWidth: baseStyle.width,
|
|
||||||
baseStyleHeight: baseStyle.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔍 DOM 렌더링 후 실제 크기 측정
|
// 🔍 DOM 렌더링 후 실제 크기 측정
|
||||||
const innerDivRef = React.useRef<HTMLDivElement>(null);
|
const innerDivRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -883,10 +883,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
|
|
||||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||||
const ConfigPanelWrapper = () => {
|
const ConfigPanelWrapper = () => {
|
||||||
const config = currentConfig.config || definition.defaultConfig || {};
|
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||||
|
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||||
|
|
||||||
const handleConfigChange = (newConfig: any) => {
|
const handleConfigChange = (newConfig: any) => {
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
// componentConfig 전체를 업데이트
|
||||||
|
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -895,7 +897,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<Settings className="h-4 w-4 text-primary" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
|
@ -266,7 +267,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
const renderComponentConfigPanel = () => {
|
const renderComponentConfigPanel = () => {
|
||||||
if (!selectedComponent) return null;
|
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) => {
|
const handleUpdateProperty = (path: string, value: any) => {
|
||||||
onUpdateProperty(selectedComponent.id, path, value);
|
onUpdateProperty(selectedComponent.id, path, value);
|
||||||
|
|
@ -276,10 +282,15 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
|
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
|
||||||
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
|
const componentId =
|
||||||
|
selectedComponent.componentType || // ⭐ section-card 등
|
||||||
|
selectedComponent.componentConfig?.type ||
|
||||||
|
selectedComponent.componentConfig?.id;
|
||||||
|
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
const definition = ComponentRegistry.getComponent(componentId);
|
const definition = ComponentRegistry.getComponent(componentId);
|
||||||
|
|
||||||
if (definition?.configPanel) {
|
if (definition?.configPanel) {
|
||||||
const ConfigPanelComponent = definition.configPanel;
|
const ConfigPanelComponent = definition.configPanel;
|
||||||
const currentConfig = selectedComponent.componentConfig || {};
|
const currentConfig = selectedComponent.componentConfig || {};
|
||||||
|
|
@ -293,10 +304,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||||
const ConfigPanelWrapper = () => {
|
const ConfigPanelWrapper = () => {
|
||||||
const config = currentConfig.config || definition.defaultConfig || {};
|
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||||
|
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||||
|
|
||||||
const handleConfigChange = (newConfig: any) => {
|
const handleConfigChange = (newConfig: any) => {
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
// componentConfig 전체를 업데이트
|
||||||
|
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -305,18 +318,19 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Settings className="h-4 w-4 text-primary" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ ConfigPanel 없음:", {
|
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
|
||||||
componentId,
|
componentId,
|
||||||
definitionName: definition?.name,
|
definitionName: definition?.name,
|
||||||
hasDefinition: !!definition,
|
hasDefinition: !!definition,
|
||||||
});
|
});
|
||||||
|
// ConfigPanel이 없으면 아래 switch case로 넘어감
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,6 +377,280 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
case "badge-status":
|
case "badge-status":
|
||||||
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
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:
|
default:
|
||||||
// ConfigPanel이 없는 경우 경고 표시
|
// ConfigPanel이 없는 경우 경고 표시
|
||||||
return (
|
return (
|
||||||
|
|
@ -634,11 +922,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
||||||
const renderDetailTab = () => {
|
const renderDetailTab = () => {
|
||||||
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
|
|
||||||
|
|
||||||
// 1. DataTable 컴포넌트
|
// 1. DataTable 컴포넌트
|
||||||
if (selectedComponent.type === "datatable") {
|
if (selectedComponent.type === "datatable") {
|
||||||
console.log("✅ [renderDetailTab] DataTable 컴포넌트");
|
|
||||||
return (
|
return (
|
||||||
<DataTableConfigPanel
|
<DataTableConfigPanel
|
||||||
component={selectedComponent as DataTableComponent}
|
component={selectedComponent as DataTableComponent}
|
||||||
|
|
@ -695,7 +980,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
||||||
if (selectedComponent.type === "component") {
|
if (selectedComponent.type === "component") {
|
||||||
console.log("✅ [renderDetailTab] Component 타입");
|
|
||||||
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
||||||
const webType = selectedComponent.componentConfig?.webType;
|
const webType = selectedComponent.componentConfig?.webType;
|
||||||
|
|
||||||
|
|
@ -755,7 +1039,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
tables={tables}
|
tables={tables}
|
||||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
onChange={(newConfig) => {
|
onChange={(newConfig) => {
|
||||||
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
|
||||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
Object.entries(newConfig).forEach(([key, value]) => {
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
handleUpdate(`componentConfig.${key}`, value);
|
handleUpdate(`componentConfig.${key}`, value);
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,14 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
const valueCode = generateCode(valueLabel);
|
const valueCode = generateCode(valueLabel);
|
||||||
|
|
||||||
onAdd({
|
onAdd({
|
||||||
tableName: "",
|
tableName: "", // CategoryValueManager에서 오버라이드됨
|
||||||
columnName: "",
|
columnName: "", // CategoryValueManager에서 오버라이드됨
|
||||||
valueCode,
|
valueCode,
|
||||||
valueLabel: valueLabel.trim(),
|
valueLabel: valueLabel.trim(),
|
||||||
description: description.trim(),
|
description: description.trim() || undefined, // 빈 문자열 대신 undefined
|
||||||
color: color,
|
color: color === "none" ? undefined : color, // "none"은 undefined로
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
});
|
} as TableCategoryValue);
|
||||||
|
|
||||||
// 초기화
|
// 초기화
|
||||||
setValueLabel("");
|
setValueLabel("");
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
style: finalStyle, // size를 포함한 최종 style
|
style: finalStyle, // size를 포함한 최종 style
|
||||||
config: component.componentConfig,
|
config: component.componentConfig,
|
||||||
componentConfig: component.componentConfig,
|
componentConfig: component.componentConfig,
|
||||||
|
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
|
||||||
|
...(component.componentConfig || {}),
|
||||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||||
// 새로운 기능들 전달
|
// 새로운 기능들 전달
|
||||||
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
||||||
|
|
|
||||||
|
|
@ -155,14 +155,16 @@ export function EntitySearchModal({
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
results.map((item, index) => (
|
results.map((item, index) => {
|
||||||
|
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
|
||||||
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={item[valueField] || index}
|
key={uniqueKey}
|
||||||
className="border-t hover:bg-accent cursor-pointer transition-colors"
|
className="border-t hover:bg-accent cursor-pointer transition-colors"
|
||||||
onClick={() => handleSelect(item)}
|
onClick={() => handleSelect(item)}
|
||||||
>
|
>
|
||||||
{displayColumns.map((col, colIndex) => (
|
{displayColumns.map((col) => (
|
||||||
<td key={`${item[valueField] || index}-${col}-${colIndex}`} className="px-4 py-2">
|
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
|
||||||
{item[col] || "-"}
|
{item[col] || "-"}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -180,7 +182,8 @@ export function EntitySearchModal({
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ export function useEntitySearch({
|
||||||
|
|
||||||
const search = useCallback(
|
const search = useCallback(
|
||||||
async (text: string, page: number = 1) => {
|
async (text: string, page: number = 1) => {
|
||||||
|
// tableName 유효성 검증
|
||||||
|
if (!tableName || tableName === "undefined" || tableName === "null") {
|
||||||
|
console.warn("엔티티 검색 건너뜀: tableName이 없음", { tableName });
|
||||||
|
setError("테이블명이 설정되지 않았습니다. 컴포넌트 설정을 확인해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -60,7 +67,8 @@ export function useEntitySearch({
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Entity search error:", err);
|
console.error("Entity search error:", err);
|
||||||
setError(err.response?.data?.message || "검색 중 오류가 발생했습니다");
|
const errorMessage = err.response?.data?.message || "검색 중 오류가 발생했습니다";
|
||||||
|
setError(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ import "./order-registration-modal/OrderRegistrationModalRenderer";
|
||||||
import "./conditional-container/ConditionalContainerRenderer";
|
import "./conditional-container/ConditionalContainerRenderer";
|
||||||
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
||||||
|
|
||||||
|
// 🆕 섹션 그룹화 레이아웃 컴포넌트
|
||||||
|
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
|
||||||
|
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -39,6 +39,9 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
// 🆕 선택 항목 상세입력
|
// 🆕 선택 항목 상세입력
|
||||||
"selected-items-detail-input": () =>
|
"selected-items-detail-input": () =>
|
||||||
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
|
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 컴포넌트 캐시
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
|
@ -71,6 +74,8 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
||||||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||||
module.ButtonConfigPanel || // button-primary의 export명
|
module.ButtonConfigPanel || // button-primary의 export명
|
||||||
|
module.SectionCardConfigPanel || // section-card의 export명
|
||||||
|
module.SectionPaperConfigPanel || // section-paper의 export명
|
||||||
module.default;
|
module.default;
|
||||||
|
|
||||||
if (!ConfigPanelComponent) {
|
if (!ConfigPanelComponent) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue