컴포넌트 화면편집기에 배치

This commit is contained in:
kjs
2025-09-10 14:09:32 +09:00
parent 3bf694ce24
commit 01860df8d7
56 changed files with 4572 additions and 778 deletions

View File

@@ -10,6 +10,13 @@ import { toast } from "sonner";
import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, ButtonTypeConfig } from "@/types/screen";
import { InteractiveDataTable } from "./InteractiveDataTable";
import { DynamicWebTypeRenderer } from "@/lib/registry";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
import "@/lib/registry/components/ButtonRenderer";
import "@/lib/registry/components/CardRenderer";
import "@/lib/registry/components/DashboardRenderer";
import "@/lib/registry/components/WidgetRenderer";
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { useParams } from "next/navigation";
import { screenApi } from "@/lib/api/screen";
@@ -152,9 +159,22 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
return renderFileComponent(comp as FileComponent);
}
// 위젯 컴포넌트가 아닌 경우
// 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용
if (comp.type !== "widget") {
return <div className="text-sm text-gray-500"> </div>;
console.log("🎯 InteractiveScreenViewer - DynamicComponentRenderer 사용:", {
componentId: comp.id,
componentType: comp.type,
componentConfig: comp.componentConfig,
});
return (
<DynamicComponentRenderer
component={comp}
isInteractive={true}
formData={formData}
onFormDataChange={handleFormDataChange}
/>
);
}
const widget = comp as WidgetComponent;
@@ -492,5 +512,3 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
export { InteractiveScreenViewerDynamic as InteractiveScreenViewer };
InteractiveScreenViewerDynamic.displayName = "InteractiveScreenViewerDynamic";

View File

@@ -1,17 +1,8 @@
"use client";
import React from "react";
import { ComponentData, WebType, WidgetComponent, FileComponent, AreaComponent, AreaLayoutType } from "@/types/screen";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { FileUpload } from "./widgets/FileUpload";
import { useAuth } from "@/hooks/useAuth";
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
import { ComponentData, WebType, WidgetComponent } from "@/types/screen";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import {
Database,
Type,
@@ -24,26 +15,11 @@ import {
Code,
Building,
File,
Group,
ChevronDown,
ChevronRight,
Search,
RotateCcw,
Plus,
Edit,
Trash2,
Upload,
Square,
CreditCard,
Layout,
Grid3x3,
Columns,
Rows,
SidebarOpen,
Folder,
ChevronUp,
} from "lucide-react";
// 컴포넌트 렌더러들 자동 등록
import "@/lib/registry/components";
interface RealtimePreviewProps {
component: ComponentData;
isSelected?: boolean;
@@ -54,147 +30,31 @@ interface RealtimePreviewProps {
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
}
// 영역 레이아웃에 따른 아이콘 반환
const getAreaIcon = (layoutType: AreaLayoutType) => {
switch (layoutType) {
case "flex":
return <Layout className="h-4 w-4 text-blue-600" />;
case "grid":
return <Grid3x3 className="h-4 w-4 text-green-600" />;
case "columns":
return <Columns className="h-4 w-4 text-purple-600" />;
case "rows":
return <Rows className="h-4 w-4 text-orange-600" />;
case "sidebar":
return <SidebarOpen className="h-4 w-4 text-indigo-600" />;
case "tabs":
return <Folder className="h-4 w-4 text-pink-600" />;
default:
return <Square className="h-4 w-4 text-gray-500" />;
}
};
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => {
if (!widgetType) return <Type className="h-3 w-3" />;
// 영역 렌더링
const renderArea = (component: ComponentData, children?: React.ReactNode) => {
const area = component as AreaComponent;
const { areaType, label } = area;
const renderPlaceholder = () => (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
{getAreaIcon(areaType)}
<p className="mt-2 text-sm text-gray-600">{label || `${areaType} 영역`}</p>
<p className="text-xs text-gray-400"> </p>
</div>
</div>
);
return (
<div className="relative h-full w-full">
<div className="absolute inset-0 h-full w-full">
{children && React.Children.count(children) > 0 ? children : renderPlaceholder()}
</div>
</div>
);
};
// 동적 웹 타입 위젯 렌더링
const renderWidget = (component: ComponentData) => {
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
if (component.type !== "widget") {
return <div className="text-xs text-gray-500"> </div>;
}
const widget = component as WidgetComponent;
const { widgetType, label, placeholder, required, readonly, columnName, style } = widget;
// 디버깅: 실제 widgetType 값 확인
console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName);
// 사용자가 테두리를 설정했는지 확인
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
// 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기
const borderClass = hasCustomBorder ? "!border-0" : "";
const commonProps = {
placeholder: placeholder || "입력하세요...",
disabled: readonly,
required: required,
className: `w-full h-full ${borderClass}`,
const iconMap: Record<string, React.ReactNode> = {
text: <span className="text-xs">Aa</span>,
number: <Hash className="h-3 w-3" />,
decimal: <Hash className="h-3 w-3" />,
date: <Calendar className="h-3 w-3" />,
datetime: <Calendar className="h-3 w-3" />,
select: <List className="h-3 w-3" />,
dropdown: <List className="h-3 w-3" />,
textarea: <AlignLeft className="h-3 w-3" />,
boolean: <CheckSquare className="h-3 w-3" />,
checkbox: <CheckSquare className="h-3 w-3" />,
radio: <Radio className="h-3 w-3" />,
code: <Code className="h-3 w-3" />,
entity: <Building className="h-3 w-3" />,
file: <File className="h-3 w-3" />,
email: <span className="text-xs">@</span>,
tel: <span className="text-xs"></span>,
button: <span className="text-xs">BTN</span>,
};
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
return (
<DynamicWebTypeRenderer
webType={widgetType}
props={{
...commonProps,
component: widget,
value: undefined, // 미리보기이므로 값은 없음
readonly: readonly,
}}
config={widget.webTypeConfig}
/>
);
} catch (error) {
console.error(`웹타입 "${widgetType}" 렌더링 실패:`, error);
// 오류 발생 시 폴백으로 기본 input 렌더링
return <Input type="text" {...commonProps} placeholder={`${widgetType} (렌더링 오류)`} />;
}
}
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
return <Input type="text" {...commonProps} />;
};
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
const getWidgetIcon = (widgetType: WebType | undefined) => {
if (!widgetType) {
return <Type className="h-4 w-4 text-gray-500" />;
}
// 레지스트리에서 웹타입 정의 조회
const webTypeDefinition = WebTypeRegistry.getWebType(widgetType);
if (webTypeDefinition && webTypeDefinition.icon) {
const IconComponent = webTypeDefinition.icon;
return <IconComponent className="h-4 w-4" />;
}
// 기본 아이콘 매핑 (하위 호환성)
switch (widgetType) {
case "text":
case "email":
case "tel":
return <Type className="h-4 w-4 text-blue-600" />;
case "number":
case "decimal":
return <Hash className="h-4 w-4 text-green-600" />;
case "date":
case "datetime":
return <Calendar className="h-4 w-4 text-purple-600" />;
case "select":
case "dropdown":
return <List className="h-4 w-4 text-orange-600" />;
case "textarea":
case "text_area":
return <AlignLeft className="h-4 w-4 text-indigo-600" />;
case "boolean":
case "checkbox":
return <CheckSquare className="h-4 w-4 text-blue-600" />;
case "radio":
return <Radio className="h-4 w-4 text-blue-600" />;
case "code":
return <Code className="h-4 w-4 text-gray-600" />;
case "entity":
return <Building className="h-4 w-4 text-cyan-600" />;
case "file":
return <File className="h-4 w-4 text-yellow-600" />;
default:
return <Type className="h-4 w-4 text-gray-500" />;
}
return iconMap[widgetType] || <Type className="h-3 w-3" />;
};
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
@@ -206,28 +66,27 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onGroupToggle,
children,
}) => {
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
const { id, type, position, size, style: componentStyle } = component;
// 컴포넌트 스타일 계산
const componentStyle = {
position: "absolute" as const,
left: position?.x || 0,
top: position?.y || 0,
width: size?.width || 200,
height: size?.height || 40,
zIndex: position?.z || 1,
...style,
};
// 선택된 컴포넌트 스타일
// 선택 상태에 따른 스타일
const selectionStyle = isSelected
? {
outline: "2px solid #3b82f6",
outlineOffset: "2px",
zIndex: 1000,
}
: {};
// 컴포넌트 기본 스타일
const baseStyle = {
left: `${position.x}px`,
top: `${position.y}px`,
width: `${size.width}px`,
height: `${size.height}px`,
zIndex: position.z || 1,
...componentStyle,
};
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.(e);
@@ -246,166 +105,22 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
<div
id={`component-${id}`}
className="absolute cursor-pointer"
style={{ ...componentStyle, ...selectionStyle }}
style={{ ...baseStyle, ...selectionStyle }}
onClick={handleClick}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{/* 컴포넌트 타입별 렌더링 */}
{/* 동적 컴포넌트 렌더링 */}
<div className="h-full w-full">
{/* 영역 타입 */}
{type === "area" && renderArea(component, children)}
{/* 데이터 테이블 타입 */}
{type === "datatable" &&
(() => {
const dataTableComponent = component as any; // DataTableComponent 타입
// 메모이제이션을 위한 계산 최적화
const visibleColumns = React.useMemo(
() => dataTableComponent.columns?.filter((col: any) => col.visible) || [],
[dataTableComponent.columns],
);
const filters = React.useMemo(() => dataTableComponent.filters || [], [dataTableComponent.filters]);
return (
<div className="flex h-full w-full flex-col overflow-hidden rounded border bg-white">
{/* 테이블 제목 */}
{dataTableComponent.title && (
<div className="border-b bg-gray-50 px-4 py-2">
<h3 className="text-sm font-medium">{dataTableComponent.title}</h3>
</div>
)}
{/* 검색 및 필터 영역 */}
{(dataTableComponent.showSearchButton || filters.length > 0) && (
<div className="border-b bg-gray-50 px-4 py-2">
<div className="flex items-center space-x-2">
{dataTableComponent.showSearchButton && (
<div className="flex items-center space-x-2">
<Input placeholder="검색..." className="h-8 w-48" />
<Button size="sm" variant="outline">
{dataTableComponent.searchButtonText || "검색"}
</Button>
</div>
)}
{filters.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500">:</span>
{filters.slice(0, 2).map((filter: any, index: number) => (
<Badge key={index} variant="secondary" className="text-xs">
{filter.label || filter.columnName}
</Badge>
))}
{filters.length > 2 && (
<Badge variant="secondary" className="text-xs">
+{filters.length - 2}
</Badge>
)}
</div>
)}
</div>
</div>
)}
{/* 테이블 본체 */}
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow>
{visibleColumns.length > 0 ? (
visibleColumns.map((col: any, index: number) => (
<TableHead key={col.id || index} className="text-xs">
{col.label || col.columnName}
{col.sortable && <span className="ml-1 text-gray-400"></span>}
</TableHead>
))
) : (
<>
<TableHead className="text-xs"> 1</TableHead>
<TableHead className="text-xs"> 2</TableHead>
<TableHead className="text-xs"> 3</TableHead>
</>
)}
</TableRow>
</TableHeader>
<TableBody>
{/* 샘플 데이터 행들 */}
{[1, 2, 3].map((rowIndex) => (
<TableRow key={rowIndex}>
{visibleColumns.length > 0 ? (
visibleColumns.map((col: any, colIndex: number) => (
<TableCell key={col.id || colIndex} className="text-xs">
{col.widgetType === "checkbox" ? (
<input type="checkbox" className="h-3 w-3" />
) : col.widgetType === "select" ? (
`옵션 ${rowIndex}`
) : col.widgetType === "date" ? (
"2024-01-01"
) : col.widgetType === "number" ? (
`${rowIndex * 100}`
) : (
`데이터 ${rowIndex}-${colIndex + 1}`
)}
</TableCell>
))
) : (
<>
<TableCell className="text-xs"> {rowIndex}-1</TableCell>
<TableCell className="text-xs"> {rowIndex}-2</TableCell>
<TableCell className="text-xs"> {rowIndex}-3</TableCell>
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* 페이지네이션 */}
{dataTableComponent.pagination && (
<div className="border-t bg-gray-50 px-4 py-2">
<div className="flex items-center justify-between text-xs text-gray-600">
<span> 3 </span>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" disabled>
</Button>
<span>1 / 1</span>
<Button size="sm" variant="outline" disabled>
</Button>
</div>
</div>
</div>
)}
</div>
);
})()}
{/* 그룹 타입 */}
{type === "group" && (
<div className="relative h-full w-full">
<div className="absolute inset-0">{children}</div>
</div>
)}
{/* 위젯 타입 - 동적 렌더링 */}
{type === "widget" && (
<div className="flex h-full flex-col">
<div className="pointer-events-none flex-1">{renderWidget(component)}</div>
</div>
)}
{/* 파일 타입 */}
{type === "file" && (
<div className="flex h-full flex-col">
<div className="pointer-events-none flex-1">
<FileUpload disabled placeholder="파일 업로드 미리보기" />
</div>
</div>
)}
<DynamicComponentRenderer
component={component}
isSelected={isSelected}
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
children={children}
/>
</div>
{/* 선택된 컴포넌트 정보 표시 */}
@@ -417,7 +132,11 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
{(component as WidgetComponent).widgetType || "widget"}
</div>
)}
{type !== "widget" && type}
{type !== "widget" && (
<div className="flex items-center gap-1">
<span>{component.componentConfig?.type || type}</span>
</div>
)}
</div>
)}
</div>
@@ -426,5 +145,4 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
// 기존 RealtimePreview와의 호환성을 위한 export
export { RealtimePreviewDynamic as RealtimePreview };
RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic";
export default RealtimePreviewDynamic;

View File

@@ -40,7 +40,7 @@ import { toast } from "sonner";
import { MenuAssignmentModal } from "./MenuAssignmentModal";
import StyleEditor from "./StyleEditor";
import { RealtimePreview } from "./RealtimePreview";
import { RealtimePreview } from "./RealtimePreviewDynamic";
import FloatingPanel from "./FloatingPanel";
import DesignerToolbar from "./DesignerToolbar";
import TablesPanel from "./panels/TablesPanel";
@@ -1292,13 +1292,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
// 새 컴포넌트 생성
console.log("🔍 ScreenDesigner handleComponentDrop:", {
componentName: component.name,
componentType: component.componentType,
webType: component.webType,
componentConfig: component.componentConfig,
finalType: component.componentType || "widget",
});
const newComponent: ComponentData = {
id: generateComponentId(),
type: component.webType === "button" ? "button" : "widget",
type: component.componentType || "widget", // 데이터베이스의 componentType 사용
label: component.name,
widgetType: component.webType,
position: snappedPosition,
size: component.defaultSize,
componentConfig: component.componentConfig || {}, // 데이터베이스의 componentConfig 사용
webTypeConfig: getDefaultWebTypeConfig(component.webType),
style: {
labelDisplay: true,
@@ -3140,9 +3149,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
description: component.description,
category: component.category,
webType: component.webType,
componentType: component.componentType, // 추가!
componentConfig: component.componentConfig, // 추가!
defaultSize: component.defaultSize,
},
};
console.log("🚀 드래그 데이터 설정:", dragData);
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
}}
/>

View File

@@ -0,0 +1,70 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ComponentData } from "@/types/screen";
interface AlertConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const AlertConfigPanel: React.FC<AlertConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div>
<Label htmlFor="alert-title"></Label>
<Input
id="alert-title"
value={config.title || "알림 제목"}
onChange={(e) => onUpdateProperty("componentConfig.title", e.target.value)}
placeholder="알림 제목을 입력하세요"
/>
</div>
<div>
<Label htmlFor="alert-message"></Label>
<Textarea
id="alert-message"
value={config.message || "알림 메시지입니다."}
onChange={(e) => onUpdateProperty("componentConfig.message", e.target.value)}
placeholder="알림 메시지를 입력하세요"
rows={3}
/>
</div>
<div>
<Label htmlFor="alert-type"> </Label>
<Select
value={config.type || "info"}
onValueChange={(value) => onUpdateProperty("componentConfig.type", value)}
>
<SelectTrigger>
<SelectValue placeholder="알림 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="info"> (Info)</SelectItem>
<SelectItem value="warning"> (Warning)</SelectItem>
<SelectItem value="success"> (Success)</SelectItem>
<SelectItem value="error"> (Error)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Switch
id="show-icon"
checked={config.showIcon ?? true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.showIcon", checked)}
/>
<Label htmlFor="show-icon"> </Label>
</div>
</div>
);
};

View File

@@ -0,0 +1,65 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ComponentData } from "@/types/screen";
interface BadgeConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const BadgeConfigPanel: React.FC<BadgeConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div>
<Label htmlFor="badge-text"> </Label>
<Input
id="badge-text"
value={config.text || "상태"}
onChange={(e) => onUpdateProperty("componentConfig.text", e.target.value)}
placeholder="뱃지 텍스트를 입력하세요"
/>
</div>
<div>
<Label htmlFor="badge-variant"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.variant", value)}
>
<SelectTrigger>
<SelectValue placeholder="뱃지 스타일 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="secondary"> (Secondary)</SelectItem>
<SelectItem value="destructive"> (Destructive)</SelectItem>
<SelectItem value="outline"> (Outline)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="badge-size"> </Label>
<Select
value={config.size || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.size", value)}
>
<SelectTrigger>
<SelectValue placeholder="뱃지 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small"> (Small)</SelectItem>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="large"> (Large)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@@ -1,138 +1,92 @@
"use client";
import React, { useState, useEffect } from "react";
import { ConfigPanelProps } from "@/lib/registry/types";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { ComponentData } from "@/types/screen";
export const ButtonConfigPanel: React.FC<ConfigPanelProps> = ({ config: initialConfig, onConfigChange }) => {
const [localConfig, setLocalConfig] = useState({
label: "버튼",
text: "",
tooltip: "",
variant: "primary",
size: "medium",
disabled: false,
fullWidth: false,
...initialConfig,
});
interface ButtonConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
useEffect(() => {
setLocalConfig({
label: "버튼",
text: "",
tooltip: "",
variant: "primary",
size: "medium",
disabled: false,
fullWidth: false,
...initialConfig,
});
}, [initialConfig]);
const updateConfig = (key: string, value: any) => {
const newConfig = { ...localConfig, [key]: value };
setLocalConfig(newConfig);
onConfigChange(newConfig);
};
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div className="space-y-3">
<div>
<label htmlFor="button-label" className="mb-1 block text-sm font-medium text-gray-700">
</label>
<input
id="button-label"
type="text"
value={localConfig.label || ""}
onChange={(e) => updateConfig("label", e.target.value)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
placeholder="버튼에 표시될 텍스트"
/>
</div>
<div>
<label htmlFor="button-tooltip" className="mb-1 block text-sm font-medium text-gray-700">
()
</label>
<input
id="button-tooltip"
type="text"
value={localConfig.tooltip || ""}
onChange={(e) => updateConfig("tooltip", e.target.value)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
placeholder="마우스 오버 시 표시될 텍스트"
/>
</div>
<div>
<label htmlFor="button-variant" className="mb-1 block text-sm font-medium text-gray-700">
</label>
<select
id="button-variant"
value={localConfig.variant || "primary"}
onChange={(e) => updateConfig("variant", e.target.value)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<option value="primary"> ()</option>
<option value="secondary"> ()</option>
<option value="success"> ()</option>
<option value="warning"> ()</option>
<option value="danger"> ()</option>
<option value="outline"></option>
</select>
</div>
<div>
<label htmlFor="button-size" className="mb-1 block text-sm font-medium text-gray-700">
</label>
<select
id="button-size"
value={localConfig.size || "medium"}
onChange={(e) => updateConfig("size", e.target.value)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
<option value="small"></option>
<option value="medium"></option>
<option value="large"></option>
</select>
</div>
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={localConfig.disabled || false}
onChange={(e) => updateConfig("disabled", e.target.checked)}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/>
<span className="text-sm font-medium text-gray-700"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={localConfig.fullWidth || false}
onChange={(e) => updateConfig("fullWidth", e.target.checked)}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/>
<span className="text-sm font-medium text-gray-700"> </span>
</label>
</div>
<div>
<Label htmlFor="button-text"> </Label>
<Input
id="button-text"
value={config.text || "버튼"}
onChange={(e) => onUpdateProperty("componentConfig.text", e.target.value)}
placeholder="버튼 텍스트를 입력하세요"
/>
</div>
<div className="border-t border-gray-200 pt-3">
<h4 className="mb-2 text-sm font-medium text-gray-700"></h4>
<button
type="button"
disabled={localConfig.disabled}
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors duration-200 ${localConfig.size === "small" ? "px-3 py-1 text-xs" : ""} ${localConfig.size === "large" ? "px-6 py-3 text-base" : ""} ${localConfig.variant === "primary" ? "bg-blue-600 text-white hover:bg-blue-700" : ""} ${localConfig.variant === "secondary" ? "bg-gray-600 text-white hover:bg-gray-700" : ""} ${localConfig.variant === "success" ? "bg-green-600 text-white hover:bg-green-700" : ""} ${localConfig.variant === "warning" ? "bg-yellow-600 text-white hover:bg-yellow-700" : ""} ${localConfig.variant === "danger" ? "bg-red-600 text-white hover:bg-red-700" : ""} ${localConfig.variant === "outline" ? "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50" : ""} ${localConfig.fullWidth ? "w-full" : ""} ${localConfig.disabled ? "cursor-not-allowed opacity-50" : ""} `}
title={localConfig.tooltip}
<div>
<Label htmlFor="button-variant"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.variant", value)}
>
{localConfig.label || "버튼"}
</button>
<SelectTrigger>
<SelectValue placeholder="버튼 스타일 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="primary"> (Primary)</SelectItem>
<SelectItem value="secondary"> (Secondary)</SelectItem>
<SelectItem value="danger"> (Danger)</SelectItem>
<SelectItem value="success"> (Success)</SelectItem>
<SelectItem value="outline"> (Outline)</SelectItem>
<SelectItem value="ghost"> (Ghost)</SelectItem>
<SelectItem value="link"> (Link)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="button-size"> </Label>
<Select
value={config.size || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.size", value)}
>
<SelectTrigger>
<SelectValue placeholder="버튼 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small"> (Small)</SelectItem>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="large"> (Large)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="button-action"> </Label>
<Select
value={config.action || "custom"}
onValueChange={(value) => onUpdateProperty("componentConfig.action", value)}
>
<SelectTrigger>
<SelectValue placeholder="버튼 액션 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="save"></SelectItem>
<SelectItem value="cancel"></SelectItem>
<SelectItem value="delete"></SelectItem>
<SelectItem value="edit"></SelectItem>
<SelectItem value="add"></SelectItem>
<SelectItem value="search"></SelectItem>
<SelectItem value="reset"></SelectItem>
<SelectItem value="submit"></SelectItem>
<SelectItem value="close"></SelectItem>
<SelectItem value="custom"> </SelectItem>
</SelectContent>
</Select>
</div>
</div>
);

View File

@@ -0,0 +1,117 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { ComponentData } from "@/types/screen";
interface CardConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const CardConfigPanel: React.FC<CardConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
const handleConfigChange = (key: string, value: any) => {
onUpdateProperty(`componentConfig.${key}`, value);
};
return (
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 카드 제목 */}
<div className="space-y-2">
<Label htmlFor="card-title"> </Label>
<Input
id="card-title"
placeholder="카드 제목을 입력하세요"
value={config.title || "카드 제목"}
onChange={(e) => handleConfigChange("title", e.target.value)}
/>
</div>
{/* 카드 내용 */}
<div className="space-y-2">
<Label htmlFor="card-content"> </Label>
<Textarea
id="card-content"
placeholder="카드 내용을 입력하세요"
value={config.content || "카드 내용 영역"}
onChange={(e) => handleConfigChange("content", e.target.value)}
rows={3}
/>
</div>
{/* 카드 스타일 */}
<div className="space-y-2">
<Label htmlFor="card-variant"> </Label>
<Select value={config.variant || "default"} onValueChange={(value) => handleConfigChange("variant", value)}>
<SelectTrigger>
<SelectValue placeholder="카드 스타일 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="outlined"> (Outlined)</SelectItem>
<SelectItem value="elevated"> (Elevated)</SelectItem>
<SelectItem value="filled"> (Filled)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 헤더 표시 여부 */}
<div className="flex items-center space-x-2">
<Switch
id="show-header"
checked={config.showHeader !== false}
onCheckedChange={(checked) => handleConfigChange("showHeader", checked)}
/>
<Label htmlFor="show-header"> </Label>
</div>
{/* 패딩 설정 */}
<div className="space-y-2">
<Label htmlFor="card-padding"></Label>
<Select value={config.padding || "default"} onValueChange={(value) => handleConfigChange("padding", value)}>
<SelectTrigger>
<SelectValue placeholder="패딩 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
<SelectItem value="small"> (Small)</SelectItem>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="large"> (Large)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label htmlFor="background-color"></Label>
<Input
id="background-color"
type="color"
value={config.backgroundColor || "#ffffff"}
onChange={(e) => handleConfigChange("backgroundColor", e.target.value)}
/>
</div>
{/* 테두리 반경 */}
<div className="space-y-2">
<Label htmlFor="border-radius"> </Label>
<Input
id="border-radius"
placeholder="8px"
value={config.borderRadius || "8px"}
onChange={(e) => handleConfigChange("borderRadius", e.target.value)}
/>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,79 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ComponentData } from "@/types/screen";
interface ChartConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const ChartConfigPanel: React.FC<ChartConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div>
<Label htmlFor="chart-title"> </Label>
<Input
id="chart-title"
value={config.title || "차트 제목"}
onChange={(e) => onUpdateProperty("componentConfig.title", e.target.value)}
placeholder="차트 제목을 입력하세요"
/>
</div>
<div>
<Label htmlFor="chart-type"> </Label>
<Select
value={config.chartType || "bar"}
onValueChange={(value) => onUpdateProperty("componentConfig.chartType", value)}
>
<SelectTrigger>
<SelectValue placeholder="차트 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bar"> (Bar)</SelectItem>
<SelectItem value="line"> (Line)</SelectItem>
<SelectItem value="pie"> (Pie)</SelectItem>
<SelectItem value="area"> (Area)</SelectItem>
<SelectItem value="scatter"> (Scatter)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="chart-data-source"> </Label>
<Input
id="chart-data-source"
value={config.dataSource || ""}
onChange={(e) => onUpdateProperty("componentConfig.dataSource", e.target.value)}
placeholder="데이터 소스 URL 또는 API 엔드포인트"
/>
</div>
<div>
<Label htmlFor="chart-x-axis">X축 </Label>
<Input
id="chart-x-axis"
value={config.xAxisLabel || ""}
onChange={(e) => onUpdateProperty("componentConfig.xAxisLabel", e.target.value)}
placeholder="X축 라벨"
/>
</div>
<div>
<Label htmlFor="chart-y-axis">Y축 </Label>
<Input
id="chart-y-axis"
value={config.yAxisLabel || ""}
onChange={(e) => onUpdateProperty("componentConfig.yAxisLabel", e.target.value)}
placeholder="Y축 라벨"
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,148 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { ComponentData } from "@/types/screen";
interface DashboardConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const DashboardConfigPanel: React.FC<DashboardConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
const handleConfigChange = (key: string, value: any) => {
onUpdateProperty(`componentConfig.${key}`, value);
};
return (
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 그리드 제목 */}
<div className="space-y-2">
<Label htmlFor="grid-title"> </Label>
<Input
id="grid-title"
placeholder="그리드 제목을 입력하세요"
value={config.title || "대시보드 그리드"}
onChange={(e) => handleConfigChange("title", e.target.value)}
/>
</div>
{/* 행 개수 */}
<div className="space-y-2">
<Label htmlFor="grid-rows"> </Label>
<Select
value={String(config.rows || 2)}
onValueChange={(value) => handleConfigChange("rows", parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="행 개수 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
</div>
{/* 열 개수 */}
<div className="space-y-2">
<Label htmlFor="grid-columns"> </Label>
<Select
value={String(config.columns || 3)}
onValueChange={(value) => handleConfigChange("columns", parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="열 개수 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="6">6</SelectItem>
</SelectContent>
</Select>
</div>
{/* 간격 설정 */}
<div className="space-y-2">
<Label htmlFor="grid-gap"> </Label>
<Select value={config.gap || "medium"} onValueChange={(value) => handleConfigChange("gap", value)}>
<SelectTrigger>
<SelectValue placeholder="간격 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (0px)</SelectItem>
<SelectItem value="small"> (8px)</SelectItem>
<SelectItem value="medium"> (16px)</SelectItem>
<SelectItem value="large"> (24px)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 그리드 아이템 높이 */}
<div className="space-y-2">
<Label htmlFor="item-height"> </Label>
<Input
id="item-height"
placeholder="120px"
value={config.itemHeight || "120px"}
onChange={(e) => handleConfigChange("itemHeight", e.target.value)}
/>
</div>
{/* 반응형 설정 */}
<div className="flex items-center space-x-2">
<Switch
id="responsive"
checked={config.responsive !== false}
onCheckedChange={(checked) => handleConfigChange("responsive", checked)}
/>
<Label htmlFor="responsive"> </Label>
</div>
{/* 테두리 표시 */}
<div className="flex items-center space-x-2">
<Switch
id="show-borders"
checked={config.showBorders !== false}
onCheckedChange={(checked) => handleConfigChange("showBorders", checked)}
/>
<Label htmlFor="show-borders"> </Label>
</div>
{/* 배경색 */}
<div className="space-y-2">
<Label htmlFor="background-color"></Label>
<Input
id="background-color"
type="color"
value={config.backgroundColor || "#f8f9fa"}
onChange={(e) => handleConfigChange("backgroundColor", e.target.value)}
/>
</div>
{/* 테두리 반경 */}
<div className="space-y-2">
<Label htmlFor="border-radius"> </Label>
<Input
id="border-radius"
placeholder="8px"
value={config.borderRadius || "8px"}
onChange={(e) => handleConfigChange("borderRadius", e.target.value)}
/>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,82 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { ComponentData } from "@/types/screen";
interface ProgressBarConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const ProgressBarConfigPanel: React.FC<ProgressBarConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div>
<Label htmlFor="progress-label"></Label>
<Input
id="progress-label"
value={config.label || "진행률"}
onChange={(e) => onUpdateProperty("componentConfig.label", e.target.value)}
placeholder="진행률 라벨을 입력하세요"
/>
</div>
<div>
<Label htmlFor="progress-value"> </Label>
<Input
id="progress-value"
type="number"
value={config.value || 65}
onChange={(e) => onUpdateProperty("componentConfig.value", parseInt(e.target.value) || 0)}
placeholder="현재 값"
min="0"
/>
</div>
<div>
<Label htmlFor="progress-max"> </Label>
<Input
id="progress-max"
type="number"
value={config.max || 100}
onChange={(e) => onUpdateProperty("componentConfig.max", parseInt(e.target.value) || 100)}
placeholder="최대 값"
min="1"
/>
</div>
<div>
<Label htmlFor="progress-color"> </Label>
<Input
id="progress-color"
type="color"
value={config.color || "#3b82f6"}
onChange={(e) => onUpdateProperty("componentConfig.color", e.target.value)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="show-percentage"
checked={config.showPercentage ?? true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.showPercentage", checked)}
/>
<Label htmlFor="show-percentage"> </Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="show-value"
checked={config.showValue ?? true}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.showValue", checked)}
/>
<Label htmlFor="show-value"> </Label>
</div>
</div>
);
};

View File

@@ -0,0 +1,77 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ComponentData } from "@/types/screen";
interface StatsCardConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const StatsCardConfigPanel: React.FC<StatsCardConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
return (
<div className="space-y-4">
<div>
<Label htmlFor="stats-title"></Label>
<Input
id="stats-title"
value={config.title || "통계 제목"}
onChange={(e) => onUpdateProperty("componentConfig.title", e.target.value)}
placeholder="통계 제목을 입력하세요"
/>
</div>
<div>
<Label htmlFor="stats-value"></Label>
<Input
id="stats-value"
value={config.value || "1,234"}
onChange={(e) => onUpdateProperty("componentConfig.value", e.target.value)}
placeholder="통계 값을 입력하세요"
/>
</div>
<div>
<Label htmlFor="stats-change"></Label>
<Input
id="stats-change"
value={config.change || "+12.5%"}
onChange={(e) => onUpdateProperty("componentConfig.change", e.target.value)}
placeholder="변화량을 입력하세요 (예: +12.5%)"
/>
</div>
<div>
<Label htmlFor="stats-trend"></Label>
<Select
value={config.trend || "up"}
onValueChange={(value) => onUpdateProperty("componentConfig.trend", value)}
>
<SelectTrigger>
<SelectValue placeholder="트렌드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="up"> (Up)</SelectItem>
<SelectItem value="down"> (Down)</SelectItem>
<SelectItem value="neutral"> (Neutral)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="stats-description"></Label>
<Input
id="stats-description"
value={config.description || "전월 대비"}
onChange={(e) => onUpdateProperty("componentConfig.description", e.target.value)}
placeholder="설명을 입력하세요"
/>
</div>
</div>
);
};

View File

@@ -18,6 +18,7 @@ interface ComponentItem {
category: string;
componentType: string;
componentConfig: any;
webType: string; // webType 추가
icon: React.ReactNode;
defaultSize: { width: number; height: number };
}
@@ -30,6 +31,7 @@ const COMPONENT_CATEGORIES = [
{ id: "navigation", name: "네비게이션", description: "화면 이동을 도와주는 컴포넌트" },
{ id: "feedback", name: "피드백", description: "사용자 피드백을 제공하는 컴포넌트" },
{ id: "input", name: "입력", description: "사용자 입력을 받는 컴포넌트" },
{ id: "표시", name: "표시", description: "정보를 표시하고 알리는 컴포넌트" },
{ id: "other", name: "기타", description: "기타 컴포넌트" },
];
@@ -50,16 +52,41 @@ export const ComponentsPanel: React.FC<ComponentsPanelProps> = ({ onDragStart })
const componentItems = useMemo(() => {
if (!componentsData?.components) return [];
return componentsData.components.map((component) => ({
id: component.component_code,
name: component.component_name,
description: component.description || `${component.component_name} 컴포넌트`,
category: component.category || "other",
componentType: component.component_config?.type || component.component_code,
componentConfig: component.component_config,
icon: getComponentIcon(component.icon_name || component.component_config?.type),
defaultSize: component.default_size || getDefaultSize(component.component_config?.type),
}));
return componentsData.components.map((component) => {
console.log("🔍 ComponentsPanel 컴포넌트 매핑:", {
component_code: component.component_code,
component_name: component.component_name,
component_config: component.component_config,
componentType: component.component_config?.type || component.component_code,
webType: component.component_config?.type || component.component_code,
category: component.category,
});
// 카테고리 매핑 (영어 -> 한국어)
const categoryMapping: Record<string, string> = {
display: "표시",
action: "액션",
layout: "레이아웃",
data: "데이터",
navigation: "네비게이션",
feedback: "피드백",
input: "입력",
};
const mappedCategory = categoryMapping[component.category] || component.category || "other";
return {
id: component.component_code,
name: component.component_name,
description: component.description || `${component.component_name} 컴포넌트`,
category: mappedCategory,
componentType: component.component_config?.type || component.component_code,
componentConfig: component.component_config,
webType: component.component_config?.type || component.component_code, // webType 추가
icon: getComponentIcon(component.icon_name || component.component_config?.type),
defaultSize: component.default_size || getDefaultSize(component.component_config?.type),
};
});
}, [componentsData]);
// 필터링된 컴포넌트

View File

@@ -9,6 +9,16 @@ import { ComponentData, WidgetComponent, FileComponent, WebTypeConfig, TableInfo
import { ButtonConfigPanel } from "./ButtonConfigPanel";
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
// 새로운 컴포넌트 설정 패널들 import
import { ButtonConfigPanel as NewButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
interface DetailSettingsPanelProps {
selectedComponent?: ComponentData;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
@@ -106,6 +116,121 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
);
}
// 컴포넌트 타입별 설정 패널 렌더링
const renderComponentConfigPanel = () => {
console.log("🔍 renderComponentConfigPanel - selectedComponent:", selectedComponent);
if (!selectedComponent) {
console.error("❌ selectedComponent가 undefined입니다!");
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-red-400" />
<h3 className="mb-2 text-lg font-medium text-red-900"></h3>
<p className="text-sm text-red-500"> .</p>
</div>
);
}
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
const handleUpdateProperty = (path: string, value: any) => {
onUpdateProperty(selectedComponent.id, path, value);
};
switch (componentType) {
case "button":
case "button-primary":
case "button-secondary":
return <NewButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "card":
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "dashboard":
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "stats":
case "stats-card":
return <StatsCardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "progress":
case "progress-bar":
return <ProgressBarConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "chart":
case "chart-basic":
return <ChartConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "alert":
case "alert-info":
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "badge":
case "badge-status":
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
default:
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> "{componentType}" .</p>
</div>
);
}
};
// 새로운 컴포넌트 타입들에 대한 설정 패널 확인
const componentType = selectedComponent?.componentConfig?.type || selectedComponent?.type;
console.log("🔍 DetailSettingsPanel componentType 확인:", {
selectedComponentType: selectedComponent?.type,
componentConfigType: selectedComponent?.componentConfig?.type,
finalComponentType: componentType,
});
const hasNewConfigPanel =
componentType &&
[
"button",
"button-primary",
"button-secondary",
"card",
"dashboard",
"stats",
"stats-card",
"progress",
"progress-bar",
"chart",
"chart-basic",
"alert",
"alert-info",
"badge",
"badge-status",
].includes(componentType);
console.log("🔍 hasNewConfigPanel:", hasNewConfigPanel);
if (hasNewConfigPanel) {
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
<Settings className="h-4 w-4 text-gray-600" />
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-sm text-gray-600">:</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
</div>
</div>
{/* 설정 패널 영역 */}
<div className="flex-1 overflow-y-auto p-4">{renderComponentConfigPanel()}</div>
</div>
);
}
if (selectedComponent.type !== "widget" && selectedComponent.type !== "file" && selectedComponent.type !== "button") {
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">

View File

@@ -7,11 +7,11 @@ import { WidgetComponent, TextTypeConfig } from "@/types/screen";
export const TextWidget: React.FC<WebTypeComponentProps> = ({ component, value, onChange, readonly = false }) => {
const widget = component as WidgetComponent;
const { placeholder, required, style } = widget;
const config = widget.webTypeConfig as TextTypeConfig | undefined;
const { placeholder, required, style } = widget || {};
const config = widget?.webTypeConfig as TextTypeConfig | undefined;
// 입력 타입에 따른 처리
const isAutoInput = widget.inputType === "auto";
const isAutoInput = widget?.inputType === "auto";
// 자동 값 생성 함수
const getAutoValue = (autoValueType: string) => {
@@ -63,11 +63,11 @@ export const TextWidget: React.FC<WebTypeComponentProps> = ({ component, value,
// 플레이스홀더 처리
const finalPlaceholder = isAutoInput
? getAutoPlaceholder(widget.autoValueType || "")
? getAutoPlaceholder(widget?.autoValueType || "")
: placeholder || config?.placeholder || "입력하세요...";
// 값 처리
const finalValue = isAutoInput ? getAutoValue(widget.autoValueType || "") : value || "";
const finalValue = isAutoInput ? getAutoValue(widget?.autoValueType || "") : value || "";
// 사용자가 테두리를 설정했는지 확인
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
@@ -77,7 +77,7 @@ export const TextWidget: React.FC<WebTypeComponentProps> = ({ component, value,
// 웹타입에 따른 input type 결정
const getInputType = () => {
switch (widget.widgetType) {
switch (widget?.widgetType) {
case "email":
return "email";
case "tel":
@@ -106,5 +106,3 @@ export const TextWidget: React.FC<WebTypeComponentProps> = ({ component, value,
};
TextWidget.displayName = "TextWidget";