diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 24e9fd69..d4bf6448 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -147,30 +147,67 @@ export default function ScreenViewPage() { ); } + // 라벨 표시 여부 계산 + const templateTypes = ["datatable"]; + const shouldShowLabel = + component.style?.labelDisplay !== false && + (component.label || component.style?.labelText) && + !templateTypes.includes(component.type); + + const labelText = component.style?.labelText || component.label || ""; + const labelStyle = { + fontSize: component.style?.labelFontSize || "14px", + color: component.style?.labelColor || "#374151", + fontWeight: component.style?.labelFontWeight || "500", + backgroundColor: component.style?.labelBackgroundColor || "transparent", + padding: component.style?.labelPadding || "0", + borderRadius: component.style?.labelBorderRadius || "0", + marginBottom: component.style?.labelMarginBottom || "4px", + }; + // 일반 컴포넌트 렌더링 return ( -
- { - setFormData((prev) => ({ - ...prev, - [fieldName]: value, - })); +
+ {/* 라벨을 외부에 별도로 렌더링 */} + {shouldShowLabel && ( +
+ {labelText} + {component.required && *} +
+ )} + + {/* 실제 컴포넌트 */} +
+ > + { + setFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); + }} + hideLabel={true} // 라벨 숨김 플래그 전달 + /> +
); })} diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index df34cb11..be0cd1af 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -25,6 +25,7 @@ import { FileTypeConfig, CodeTypeConfig, EntityTypeConfig, + ButtonTypeConfig, } from "@/types/screen"; import { InteractiveDataTable } from "./InteractiveDataTable"; @@ -33,6 +34,7 @@ interface InteractiveScreenViewerProps { allComponents: ComponentData[]; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; + hideLabel?: boolean; } export const InteractiveScreenViewer: React.FC = ({ @@ -40,6 +42,7 @@ export const InteractiveScreenViewer: React.FC = ( allComponents, formData: externalFormData, onFormDataChange, + hideLabel = false, }) => { const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -96,10 +99,14 @@ export const InteractiveScreenViewer: React.FC = ( return React.cloneElement(element, { style: { + ...element.props.style, // 기존 스타일 유지 ...comp.style, - // 크기는 부모 컨테이너에서 처리하므로 제거 - width: undefined, - height: undefined, + // 크기는 부모 컨테이너에서 처리하므로 제거 (하지만 다른 스타일은 유지) + width: "100%", + height: "100%", + minHeight: "100%", + maxHeight: "100%", + boxSizing: "border-box", }, }); }; @@ -183,7 +190,12 @@ export const InteractiveScreenViewer: React.FC = ( minLength={config?.minLength} maxLength={config?.maxLength} pattern={getPatternByFormat(config?.format || "none")} - className="h-full w-full" + className="w-full" + style={{ + height: "100%", + minHeight: "100%", + maxHeight: "100%" + }} />, ); } @@ -223,7 +235,8 @@ export const InteractiveScreenViewer: React.FC = ( min={config?.min} max={config?.max} step={step} - className="h-full w-full" + className="w-full" + style={{ height: "100%" }} />, ); } @@ -454,7 +467,8 @@ export const InteractiveScreenViewer: React.FC = ( required={required} min={config?.minDate} max={config?.maxDate} - className="h-full w-full" + className="w-full" + style={{ height: "100%" }} />, ); } else { @@ -513,7 +527,8 @@ export const InteractiveScreenViewer: React.FC = ( required={required} min={config?.minDate} max={config?.maxDate} - className="h-full w-full" + className="w-full" + style={{ height: "100%" }} />, ); } @@ -561,7 +576,8 @@ export const InteractiveScreenViewer: React.FC = ( required={required} multiple={config?.multiple} accept={config?.accept} - className="h-full w-full" + className="w-full" + style={{ height: "100%" }} />, ); } @@ -632,14 +648,22 @@ export const InteractiveScreenViewer: React.FC = ( { label: "카테고리", value: "category" }, ]; - return applyStyles( + return ( = ( onChange={(e) => updateFormData(fieldName, e.target.value)} disabled={readonly} required={required} - className="h-full w-full" + className="w-full" + style={{ height: "100%" }} />, ); } @@ -707,6 +786,7 @@ export const InteractiveScreenViewer: React.FC = ( // 라벨 표시 여부 계산 const shouldShowLabel = + !hideLabel && // hideLabel이 true면 라벨 숨김 component.style?.labelDisplay !== false && (component.label || component.style?.labelText) && !templateTypes.includes(component.type); // 템플릿 컴포넌트는 라벨 표시 안함 @@ -735,7 +815,7 @@ export const InteractiveScreenViewer: React.FC = ( )} {/* 실제 위젯 */} -
{renderInteractiveWidget(component)}
+
{renderInteractiveWidget(component)}
); }; diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 448e4dde..716cb168 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -72,7 +72,7 @@ const renderWidget = (component: ComponentData) => { const borderClass = hasCustomBorder ? "!border-0" : ""; const commonProps = { - placeholder: placeholder || `입력하세요...`, + placeholder: placeholder || "입력하세요...", disabled: readonly, required: required, className: `w-full h-full ${borderClass}`, @@ -621,6 +621,21 @@ const renderWidget = (component: ComponentData) => { ); } + case "button": + return ( + + ); + default: return ; } @@ -723,13 +738,8 @@ export const RealtimePreview: React.FC = ({ return (
{ @@ -940,13 +950,10 @@ export const RealtimePreview: React.FC = ({ // 다른 컴포넌트들은 기존 구조 사용 return (
{ @@ -961,7 +968,7 @@ export const RealtimePreview: React.FC = ({ e.stopPropagation(); }} > - {/* 라벨 표시 */} + {/* 라벨 표시 - 원래대로 컴포넌트 위쪽에 표시 */} {shouldShowLabel && (
= ({ }} > {type === "container" && ( -
-
- -
-
{label}
-
{tableName}
+
+ {/* 컨테이너 자식 컴포넌트들 렌더링 */} + {children && React.Children.count(children) > 0 ? ( +
{children}
+ ) : ( +
+
+ +
+
{label}
+
{tableName}
+
+
-
+ )}
)} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index c6b3b76a..24433a26 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -633,7 +633,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 격자 스냅 적용 const snappedPosition = layout.gridSettings?.snapToGrid && gridInfo - ? snapToGrid({ x: dropX, y: dropY, z: 1 }, gridInfo, layout.gridSettings) + ? snapToGrid({ x: dropX, y: dropY, z: 1 }, gridInfo, layout.gridSettings as GridUtilSettings) : { x: dropX, y: dropY, z: 1 }; console.log("🎨 템플릿 드롭:", { @@ -644,8 +644,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); // 템플릿의 모든 컴포넌트들을 생성 + // 먼저 ID 매핑을 생성 (parentId 참조를 위해) + const idMapping: Record = {}; + template.components.forEach((templateComp, index) => { + const newId = generateComponentId(); + if (index === 0) { + // 첫 번째 컴포넌트(컨테이너)는 "form-container"로 매핑 + idMapping["form-container"] = newId; + } + idMapping[templateComp.parentId || `temp_${index}`] = newId; + }); + const newComponents: ComponentData[] = template.components.map((templateComp, index) => { - const componentId = generateComponentId(); + const componentId = index === 0 ? idMapping["form-container"] : generateComponentId(); // 템플릿 컴포넌트의 상대 위치를 드롭 위치 기준으로 조정 const absoluteX = snappedPosition.x + templateComp.position.x; @@ -654,17 +665,38 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 격자 스냅 적용 const finalPosition = layout.gridSettings?.snapToGrid && gridInfo - ? snapToGrid({ x: absoluteX, y: absoluteY, z: 1 }, gridInfo, layout.gridSettings) + ? snapToGrid({ x: absoluteX, y: absoluteY, z: 1 }, gridInfo, layout.gridSettings as GridUtilSettings) : { x: absoluteX, y: absoluteY, z: 1 }; if (templateComp.type === "container") { + // 그리드 컬럼 기반 크기 계산 + const gridColumns = + typeof templateComp.size.width === "number" && templateComp.size.width <= 12 ? templateComp.size.width : 4; // 기본 4컬럼 + + const calculatedSize = + gridInfo && layout.gridSettings?.snapToGrid + ? (() => { + const newWidth = calculateWidthFromColumns( + gridColumns, + gridInfo, + layout.gridSettings as GridUtilSettings, + ); + return { + width: newWidth, + height: templateComp.size.height, + }; + })() + : { width: 400, height: templateComp.size.height }; // 폴백 크기 + return { id: componentId, type: "container", label: templateComp.label, tableName: selectedScreen?.tableName || "", + title: templateComp.title || templateComp.label, position: finalPosition, - size: templateComp.size, + size: calculatedSize, + gridColumns, style: { labelDisplay: true, labelFontSize: "14px", @@ -817,6 +849,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD label: templateComp.label, placeholder: templateComp.placeholder, columnName: `field_${index + 1}`, + parentId: templateComp.parentId ? idMapping[templateComp.parentId] : undefined, position: finalPosition, size: templateComp.size, required: templateComp.required || false, @@ -878,6 +911,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 기존 테이블/컬럼 드래그 처리 const { type, table, column } = parsedData; + + // 드롭 대상이 폼 컨테이너인지 확인 + const dropTarget = e.target as HTMLElement; + const formContainer = dropTarget.closest('[data-form-container="true"]'); + const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; @@ -1031,29 +1069,65 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }; - // 컬럼 위젯 생성 - newComponent = { - id: generateComponentId(), - type: "widget", - label: column.columnName, - tableName: table.tableName, - columnName: column.columnName, - widgetType: column.widgetType, - // dataType: column.dataType, // WidgetComponent에 dataType 속성이 없음 - required: column.required, - readonly: false, // 누락된 속성 추가 - position: { x, y, z: 1 } as Position, - size: { width: columnWidth, height: 40 }, - gridColumns: 1, // 기본 그리드 컬럼 수 - style: { - labelDisplay: true, - labelFontSize: "12px", - labelColor: "#374151", - labelFontWeight: "500", - labelMarginBottom: "6px", - }, - webTypeConfig: getDefaultWebTypeConfig(column.widgetType), - }; + // 폼 컨테이너에 드롭한 경우 + if (formContainer) { + const formContainerId = formContainer.getAttribute("data-component-id"); + const formContainerComponent = layout.components.find((c) => c.id === formContainerId); + + if (formContainerComponent) { + // 폼 내부에서의 상대적 위치 계산 + const containerRect = formContainer.getBoundingClientRect(); + const relativeX = e.clientX - containerRect.left; + const relativeY = e.clientY - containerRect.top; + + newComponent = { + id: generateComponentId(), + type: "widget", + label: column.columnName, + tableName: table.tableName, + columnName: column.columnName, + widgetType: column.widgetType, + required: column.required, + readonly: false, + parentId: formContainerId, // 폼 컨테이너의 자식으로 설정 + position: { x: relativeX, y: relativeY, z: 1 } as Position, + size: { width: 200, height: 40 }, + style: { + labelDisplay: true, + labelFontSize: "12px", + labelColor: "#374151", + labelFontWeight: "500", + labelMarginBottom: "6px", + }, + webTypeConfig: getDefaultWebTypeConfig(column.widgetType), + }; + } else { + return; // 폼 컨테이너를 찾을 수 없으면 드롭 취소 + } + } else { + // 일반 캔버스에 드롭한 경우 (기존 로직) + newComponent = { + id: generateComponentId(), + type: "widget", + label: column.columnName, + tableName: table.tableName, + columnName: column.columnName, + widgetType: column.widgetType, + required: column.required, + readonly: false, + position: { x, y, z: 1 } as Position, + size: { width: columnWidth, height: 40 }, + gridColumns: 1, + style: { + labelDisplay: true, + labelFontSize: "12px", + labelColor: "#374151", + labelFontWeight: "500", + labelMarginBottom: "6px", + }, + webTypeConfig: getDefaultWebTypeConfig(column.widgetType), + }; + } } else { return; } @@ -2273,79 +2347,106 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } return ( - handleComponentClick(component, e)} - onDragStart={(e) => startComponentDrag(component, e)} - onDragEnd={endDrag} +
- {children.map((child) => { - // 자식 컴포넌트에도 드래그 피드백 적용 - const isChildDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === child.id; - const isChildBeingDragged = - dragState.isDragging && dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); - - let displayChild = child; - - if (isChildBeingDragged) { - if (isChildDraggingThis) { - // 주 드래그 자식 컴포넌트 - displayChild = { - ...child, - position: dragState.currentPosition, - style: { - ...child.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 9999, - }, - }; - } else { - // 다른 선택된 자식 컴포넌트들 - const originalChildComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === child.id, - ); - if (originalChildComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; - - displayChild = { - ...child, - position: { - x: originalChildComponent.position.x + deltaX, - y: originalChildComponent.position.y + deltaY, - z: originalChildComponent.position.z || 1, - } as Position, - style: { - ...child.style, - opacity: 0.8, - transition: "none", - zIndex: 8888, - }, - }; - } - } + handleComponentClick(component, e)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} + > + {/* 컨테이너 및 그룹의 자식 컴포넌트들 렌더링 */} + {(component.type === "group" || component.type === "container") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트에도 드래그 피드백 적용 + const isChildDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === child.id; + const isChildBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); - return ( - handleComponentClick(child, e)} - onDragStart={(e) => startComponentDrag(child, e)} - onDragEnd={endDrag} - /> - ); - })} - + let displayChild = child; + + if (isChildBeingDragged) { + if (isChildDraggingThis) { + // 주 드래그 자식 컴포넌트 + displayChild = { + ...child, + position: dragState.currentPosition, + style: { + ...child.style, + opacity: 0.8, + transform: "scale(1.02)", + transition: "none", + zIndex: 9999, + }, + }; + } else { + // 다른 선택된 자식 컴포넌트들 + const originalChildComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === child.id, + ); + if (originalChildComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayChild = { + ...child, + position: { + x: originalChildComponent.position.x + deltaX, + y: originalChildComponent.position.y + deltaY, + z: originalChildComponent.position.z || 1, + } as Position, + style: { + ...child.style, + opacity: 0.8, + transition: "none", + zIndex: 8888, + }, + }; + } + } + } + + return ( +
+ handleComponentClick(child, e)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + /> +
+ ); + })} +
+
); })} @@ -2475,7 +2576,40 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
updateComponentProperty(selectedComponent.id, "style", newStyle)} + onStyleChange={(newStyle) => { + console.log("🔧 StyleEditor 크기 변경:", { + componentId: selectedComponent.id, + newStyle, + currentSize: selectedComponent.size, + hasWidth: !!newStyle.width, + hasHeight: !!newStyle.height, + }); + + // 스타일 업데이트 + updateComponentProperty(selectedComponent.id, "style", newStyle); + + // 크기가 변경된 경우 component.size도 업데이트 + if (newStyle.width || newStyle.height) { + const width = newStyle.width + ? parseInt(newStyle.width.replace("px", "")) + : selectedComponent.size.width; + const height = newStyle.height + ? parseInt(newStyle.height.replace("px", "")) + : selectedComponent.size.height; + + console.log("📏 크기 업데이트:", { + originalWidth: selectedComponent.size.width, + originalHeight: selectedComponent.size.height, + newWidth: width, + newHeight: height, + styleWidth: newStyle.width, + styleHeight: newStyle.height, + }); + + updateComponentProperty(selectedComponent.id, "size.width", width); + updateComponentProperty(selectedComponent.id, "size.height", height); + } + }} />
) : ( diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index 749c51d5..8d45013e 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Separator } from "@/components/ui/separator"; -import { Palette, Layout, Type, Square, Box, Eye, RotateCcw } from "lucide-react"; +import { Palette, Type, Square, Box, Eye, RotateCcw } from "lucide-react"; import { ComponentStyle } from "@/types/screen"; interface StyleEditorProps { @@ -62,12 +62,8 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
- - - - - 레이아웃 - + + 여백 @@ -86,93 +82,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd - {/* 레이아웃 탭 */} - - {/* 너비/높이는 위젯 속성에서만 관리하도록 제거 */} -
-
- - -
-
- - -
-
- - {localStyle.display === "flex" && ( - <> - -
-
- - -
-
- - -
-
- - )} -
- {/* 여백 탭 */}
diff --git a/frontend/components/screen/panels/ButtonConfigPanel.tsx b/frontend/components/screen/panels/ButtonConfigPanel.tsx new file mode 100644 index 00000000..6aeac75c --- /dev/null +++ b/frontend/components/screen/panels/ButtonConfigPanel.tsx @@ -0,0 +1,437 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { + Save, + X, + Trash2, + Edit, + Plus, + RotateCcw, + Send, + ExternalLink, + MousePointer, + Settings, + AlertTriangle, +} from "lucide-react"; +import { ButtonActionType, ButtonTypeConfig, WidgetComponent } from "@/types/screen"; + +interface ButtonConfigPanelProps { + component: WidgetComponent; + onUpdateComponent: (updates: Partial) => void; +} + +const actionTypeOptions: { value: ButtonActionType; label: string; icon: React.ReactNode; color: string }[] = [ + { value: "save", label: "저장", icon: , color: "#3b82f6" }, + { value: "cancel", label: "취소", icon: , color: "#6b7280" }, + { value: "delete", label: "삭제", icon: , color: "#ef4444" }, + { value: "edit", label: "수정", icon: , color: "#f59e0b" }, + { value: "add", label: "추가", icon: , color: "#10b981" }, + { value: "search", label: "검색", icon: , color: "#8b5cf6" }, + { value: "reset", label: "초기화", icon: , color: "#6b7280" }, + { value: "submit", label: "제출", icon: , color: "#059669" }, + { value: "close", label: "닫기", icon: , color: "#6b7280" }, + { value: "popup", label: "팝업 열기", icon: , color: "#8b5cf6" }, + { value: "navigate", label: "페이지 이동", icon: , color: "#0ea5e9" }, + { value: "custom", label: "사용자 정의", icon: , color: "#64748b" }, +]; + +export const ButtonConfigPanel: React.FC = ({ component, onUpdateComponent }) => { + const config = (component.webTypeConfig as ButtonTypeConfig) || {}; + + // 로컬 상태 관리 + const [localConfig, setLocalConfig] = useState({ + actionType: "custom", + variant: "default", + size: "sm", + ...config, + }); + + // 컴포넌트 변경 시 로컬 상태 동기화 + useEffect(() => { + const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {}; + setLocalConfig({ + actionType: "custom", + variant: "default", + size: "sm", + ...newConfig, + }); + }, [component.webTypeConfig]); + + // 설정 업데이트 함수 + const updateConfig = (updates: Partial) => { + const newConfig = { ...localConfig, ...updates }; + setLocalConfig(newConfig); + + // 스타일 업데이트도 함께 적용 + const styleUpdates: any = {}; + if (updates.backgroundColor) styleUpdates.backgroundColor = updates.backgroundColor; + if (updates.textColor) styleUpdates.color = updates.textColor; + if (updates.borderColor) styleUpdates.borderColor = updates.borderColor; + + onUpdateComponent({ + webTypeConfig: newConfig, + ...(Object.keys(styleUpdates).length > 0 && { + style: { ...component.style, ...styleUpdates }, + }), + }); + }; + + // 액션 타입 변경 시 기본값 설정 + const handleActionTypeChange = (actionType: ButtonActionType) => { + const actionOption = actionTypeOptions.find((opt) => opt.value === actionType); + const updates: Partial = { actionType }; + + // 액션 타입에 따른 기본 설정 + switch (actionType) { + case "save": + updates.variant = "default"; + updates.backgroundColor = "#3b82f6"; + updates.textColor = "#ffffff"; + // 버튼 라벨과 스타일도 업데이트 + onUpdateComponent({ + label: "저장", + style: { ...component.style, backgroundColor: "#3b82f6", color: "#ffffff" }, + }); + break; + case "cancel": + case "close": + updates.variant = "outline"; + updates.backgroundColor = "transparent"; + updates.textColor = "#6b7280"; + onUpdateComponent({ + label: actionType === "cancel" ? "취소" : "닫기", + style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" }, + }); + break; + case "delete": + updates.variant = "destructive"; + updates.backgroundColor = "#ef4444"; + updates.textColor = "#ffffff"; + updates.confirmMessage = "정말로 삭제하시겠습니까?"; + onUpdateComponent({ + label: "삭제", + style: { ...component.style, backgroundColor: "#ef4444", color: "#ffffff" }, + }); + break; + case "edit": + updates.backgroundColor = "#f59e0b"; + updates.textColor = "#ffffff"; + onUpdateComponent({ + label: "수정", + style: { ...component.style, backgroundColor: "#f59e0b", color: "#ffffff" }, + }); + break; + case "add": + updates.backgroundColor = "#10b981"; + updates.textColor = "#ffffff"; + onUpdateComponent({ + label: "추가", + style: { ...component.style, backgroundColor: "#10b981", color: "#ffffff" }, + }); + break; + case "search": + updates.backgroundColor = "#8b5cf6"; + updates.textColor = "#ffffff"; + onUpdateComponent({ + label: "검색", + style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" }, + }); + break; + case "reset": + updates.variant = "outline"; + updates.backgroundColor = "transparent"; + updates.textColor = "#6b7280"; + onUpdateComponent({ + label: "초기화", + style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" }, + }); + break; + case "submit": + updates.backgroundColor = "#059669"; + updates.textColor = "#ffffff"; + onUpdateComponent({ + label: "제출", + style: { ...component.style, backgroundColor: "#059669", color: "#ffffff" }, + }); + break; + case "popup": + updates.backgroundColor = "#8b5cf6"; + updates.textColor = "#ffffff"; + updates.popupTitle = "상세 정보"; + updates.popupContent = "여기에 팝업 내용을 입력하세요."; + updates.popupSize = "md"; + onUpdateComponent({ + label: "상세보기", + style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" }, + }); + break; + case "navigate": + updates.backgroundColor = "#0ea5e9"; + updates.textColor = "#ffffff"; + updates.navigateUrl = "/"; + updates.navigateTarget = "_self"; + onUpdateComponent({ + label: "이동", + style: { ...component.style, backgroundColor: "#0ea5e9", color: "#ffffff" }, + }); + break; + case "custom": + updates.backgroundColor = "#64748b"; + updates.textColor = "#ffffff"; + onUpdateComponent({ + label: "버튼", + style: { ...component.style, backgroundColor: "#64748b", color: "#ffffff" }, + }); + break; + } + + updateConfig(updates); + }; + + const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType); + + return ( +
+ + + + + 버튼 기능 설정 + + + + {/* 액션 타입 선택 */} +
+ + + {selectedActionOption && ( +
+ {selectedActionOption.icon} + {selectedActionOption.label} + + {selectedActionOption.value} + +
+ )} +
+ + + + {/* 기본 설정 */} +
+ + + {/* 버튼 텍스트 */} +
+ + { + const newValue = e.target.value; + onUpdateComponent({ label: newValue }); + }} + placeholder="버튼에 표시될 텍스트" + className="h-8 text-xs" + /> +
+ + {/* 버튼 스타일 */} +
+
+ + +
+ +
+ + +
+
+ + {/* 아이콘 설정 */} +
+ + updateConfig({ icon: e.target.value })} + placeholder="예: Save, Edit, Trash2" + className="h-8 text-xs" + /> +
+
+ + + + {/* 액션별 세부 설정 */} + {localConfig.actionType === "delete" && ( +
+ +
+ + updateConfig({ confirmMessage: e.target.value })} + placeholder="정말로 삭제하시겠습니까?" + className="h-8 text-xs" + /> +
+
+ )} + + {localConfig.actionType === "popup" && ( +
+ +
+
+ + updateConfig({ popupTitle: e.target.value })} + placeholder="상세 정보" + className="h-8 text-xs" + /> +
+
+ + +
+
+ +