스타일 적용안되던 문제 수정
This commit is contained in:
@@ -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<string, any>;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
|
||||
@@ -40,6 +42,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
allComponents,
|
||||
formData: externalFormData,
|
||||
onFormDataChange,
|
||||
hideLabel = false,
|
||||
}) => {
|
||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||
@@ -96,10 +99,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
{ label: "카테고리", value: "category" },
|
||||
];
|
||||
|
||||
return applyStyles(
|
||||
return (
|
||||
<Select
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onValueChange={(value) => updateFormData(fieldName, value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className="h-full w-full">
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
style={{ height: "100%" }}
|
||||
style={{
|
||||
...comp.style,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<SelectValue placeholder={finalPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -655,6 +679,60 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
case "button": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (config?.actionType === "popup" && config.popupTitle) {
|
||||
alert(`${config.popupTitle}\n\n${config.popupContent || "팝업 내용이 없습니다."}`);
|
||||
} else if (config?.actionType === "navigate" && config.navigateUrl) {
|
||||
if (config.navigateTarget === "_blank") {
|
||||
window.open(config.navigateUrl, "_blank");
|
||||
} else {
|
||||
window.location.href = config.navigateUrl;
|
||||
}
|
||||
} else if (config?.actionType === "custom" && config.customAction) {
|
||||
try {
|
||||
// 간단한 JavaScript 실행 (보안상 제한적)
|
||||
eval(config.customAction);
|
||||
} catch (error) {
|
||||
console.error("커스텀 액션 실행 오류:", error);
|
||||
}
|
||||
} else if (config?.actionType === "delete" && config.confirmMessage) {
|
||||
if (confirm(config.confirmMessage)) {
|
||||
console.log("삭제 확인됨");
|
||||
}
|
||||
} else {
|
||||
console.log(`버튼 클릭: ${config?.actionType || "기본"} 액션`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
disabled={readonly}
|
||||
size={config?.size || "sm"}
|
||||
variant={config?.variant || "default"}
|
||||
className="w-full"
|
||||
style={{ height: "100%" }}
|
||||
style={{
|
||||
// 컴포넌트 스타일과 설정 스타일 모두 적용
|
||||
...comp.style,
|
||||
// 크기는 className으로 처리하므로 CSS 크기 속성 제거
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
// 설정값이 있으면 우선 적용, 없으면 컴포넌트 스타일 사용
|
||||
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
|
||||
color: config?.textColor || comp.style?.color,
|
||||
borderColor: config?.borderColor || comp.style?.borderColor,
|
||||
}}
|
||||
>
|
||||
{label || "버튼"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return applyStyles(
|
||||
<Input
|
||||
@@ -664,7 +742,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 라벨 표시 여부 계산
|
||||
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<InteractiveScreenViewerProps> = (
|
||||
)}
|
||||
|
||||
{/* 실제 위젯 */}
|
||||
<div className={shouldShowLabel ? "flex-1" : "h-full w-full"}>{renderInteractiveWidget(component)}</div>
|
||||
<div className="h-full w-full">{renderInteractiveWidget(component)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Button
|
||||
disabled={readonly}
|
||||
size="sm"
|
||||
variant={style?.backgroundColor === "transparent" ? "outline" : "default"}
|
||||
className="gap-1 text-xs"
|
||||
style={{
|
||||
...style, // 모든 스타일 속성 적용
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Input type="text" {...commonProps} />;
|
||||
}
|
||||
@@ -723,13 +738,8 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute cursor-move"
|
||||
className="h-full w-full cursor-move"
|
||||
style={{
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${size.width}px`,
|
||||
height: shouldShowLabel ? `${size.height + 20 + labelMarginBottomValue}px` : `${size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
...(isSelected ? { boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.5)" } : {}),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -940,13 +950,10 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
// 다른 컴포넌트들은 기존 구조 사용
|
||||
return (
|
||||
<div
|
||||
className={`absolute cursor-move transition-all ${defaultRingClass}`}
|
||||
className={`cursor-move transition-all ${defaultRingClass}`}
|
||||
style={{
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${size.width}px`,
|
||||
height: shouldShowLabel ? `${size.height + 20 + labelMarginBottomValue}px` : `${size.height}px`, // 라벨 공간 + 여백 추가
|
||||
zIndex: component.position.z || 1,
|
||||
height: `${size.height}px`, // 순수 컴포넌트 높이만 사용
|
||||
...selectionStyle,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -961,7 +968,7 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{/* 라벨 표시 */}
|
||||
{/* 라벨 표시 - 원래대로 컴포넌트 위쪽에 표시 */}
|
||||
{shouldShowLabel && (
|
||||
<div
|
||||
className="pointer-events-none absolute left-0 w-full truncate"
|
||||
@@ -983,14 +990,25 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
}}
|
||||
>
|
||||
{type === "container" && (
|
||||
<div className="pointer-events-none flex h-full flex-col items-center justify-center p-2">
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
<Database className="h-6 w-6 text-blue-600" />
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium">{label}</div>
|
||||
<div className="text-xs text-gray-500">{tableName}</div>
|
||||
<div
|
||||
className="relative h-full w-full"
|
||||
data-form-container={component.title === "새 데이터 입력" ? "true" : "false"}
|
||||
data-component-id={component.id}
|
||||
>
|
||||
{/* 컨테이너 자식 컴포넌트들 렌더링 */}
|
||||
{children && React.Children.count(children) > 0 ? (
|
||||
<div className="absolute inset-0">{children}</div>
|
||||
) : (
|
||||
<div className="pointer-events-none flex h-full flex-col items-center justify-center p-2">
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
<Database className="h-6 w-6 text-blue-600" />
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium">{label}</div>
|
||||
<div className="text-xs text-gray-500">{tableName}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
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 (
|
||||
<RealtimePreview
|
||||
key={`${component.id}-${component.type === "widget" ? JSON.stringify((component as any).webTypeConfig) : ""}`}
|
||||
component={displayComponent}
|
||||
isSelected={
|
||||
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
|
||||
}
|
||||
onClick={(e) => handleComponentClick(component, e)}
|
||||
onDragStart={(e) => startComponentDrag(component, e)}
|
||||
onDragEnd={endDrag}
|
||||
<div
|
||||
key={component.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${displayComponent.position.x}px`,
|
||||
top: `${displayComponent.position.y}px`,
|
||||
width: displayComponent.style?.width || `${displayComponent.size.width}px`,
|
||||
height: displayComponent.style?.height || `${displayComponent.size.height}px`,
|
||||
zIndex: displayComponent.position.z || 1,
|
||||
}}
|
||||
>
|
||||
{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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
<RealtimePreview
|
||||
component={displayComponent}
|
||||
isSelected={
|
||||
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
|
||||
}
|
||||
onClick={(e) => 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 (
|
||||
<RealtimePreview
|
||||
key={`${child.id}-${child.type === "widget" ? JSON.stringify((child as any).webTypeConfig) : ""}`}
|
||||
component={displayChild}
|
||||
isSelected={
|
||||
selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
|
||||
}
|
||||
onClick={(e) => handleComponentClick(child, e)}
|
||||
onDragStart={(e) => startComponentDrag(child, e)}
|
||||
onDragEnd={endDrag}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RealtimePreview>
|
||||
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 (
|
||||
<div
|
||||
key={child.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${displayChild.position.x - component.position.x}px`,
|
||||
top: `${displayChild.position.y - component.position.y}px`,
|
||||
width: `${displayChild.size.width}px`,
|
||||
height: `${displayChild.size.height}px`, // 순수 컴포넌트 높이만 사용
|
||||
zIndex: displayChild.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<RealtimePreview
|
||||
component={displayChild}
|
||||
isSelected={
|
||||
selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
|
||||
}
|
||||
onClick={(e) => handleComponentClick(child, e)}
|
||||
onDragStart={(e) => startComponentDrag(child, e)}
|
||||
onDragEnd={endDrag}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RealtimePreview>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2475,7 +2576,40 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
<div className="p-4">
|
||||
<StyleEditor
|
||||
style={selectedComponent.style || {}}
|
||||
onStyleChange={(newStyle) => 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="layout" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="layout">
|
||||
<Layout className="mr-1 h-3 w-3" />
|
||||
레이아웃
|
||||
</TabsTrigger>
|
||||
<Tabs defaultValue="spacing" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="spacing">
|
||||
<Box className="mr-1 h-3 w-3" />
|
||||
여백
|
||||
@@ -86,93 +82,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 레이아웃 탭 */}
|
||||
<TabsContent value="layout" className="space-y-4">
|
||||
{/* 너비/높이는 위젯 속성에서만 관리하도록 제거 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display">표시 방식</Label>
|
||||
<Select
|
||||
value={localStyle.display || "block"}
|
||||
onValueChange={(value) => handleStyleChange("display", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="block">Block</SelectItem>
|
||||
<SelectItem value="inline">Inline</SelectItem>
|
||||
<SelectItem value="inline-block">Inline-Block</SelectItem>
|
||||
<SelectItem value="flex">Flex</SelectItem>
|
||||
<SelectItem value="grid">Grid</SelectItem>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="position">위치</Label>
|
||||
<Select
|
||||
value={localStyle.position || "static"}
|
||||
onValueChange={(value) => handleStyleChange("position", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">Static</SelectItem>
|
||||
<SelectItem value="relative">Relative</SelectItem>
|
||||
<SelectItem value="absolute">Absolute</SelectItem>
|
||||
<SelectItem value="fixed">Fixed</SelectItem>
|
||||
<SelectItem value="sticky">Sticky</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{localStyle.display === "flex" && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="flexDirection">방향</Label>
|
||||
<Select
|
||||
value={localStyle.flexDirection || "row"}
|
||||
onValueChange={(value) => handleStyleChange("flexDirection", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="row">가로 (Row)</SelectItem>
|
||||
<SelectItem value="row-reverse">가로 역순</SelectItem>
|
||||
<SelectItem value="column">세로 (Column)</SelectItem>
|
||||
<SelectItem value="column-reverse">세로 역순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="justifyContent">가로 정렬</Label>
|
||||
<Select
|
||||
value={localStyle.justifyContent || "flex-start"}
|
||||
onValueChange={(value) => handleStyleChange("justifyContent", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="flex-start">시작</SelectItem>
|
||||
<SelectItem value="center">중앙</SelectItem>
|
||||
<SelectItem value="flex-end">끝</SelectItem>
|
||||
<SelectItem value="space-between">양끝</SelectItem>
|
||||
<SelectItem value="space-around">균등</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 여백 탭 */}
|
||||
<TabsContent value="spacing" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
437
frontend/components/screen/panels/ButtonConfigPanel.tsx
Normal file
437
frontend/components/screen/panels/ButtonConfigPanel.tsx
Normal file
@@ -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<WidgetComponent>) => void;
|
||||
}
|
||||
|
||||
const actionTypeOptions: { value: ButtonActionType; label: string; icon: React.ReactNode; color: string }[] = [
|
||||
{ value: "save", label: "저장", icon: <Save className="h-4 w-4" />, color: "#3b82f6" },
|
||||
{ value: "cancel", label: "취소", icon: <X className="h-4 w-4" />, color: "#6b7280" },
|
||||
{ value: "delete", label: "삭제", icon: <Trash2 className="h-4 w-4" />, color: "#ef4444" },
|
||||
{ value: "edit", label: "수정", icon: <Edit className="h-4 w-4" />, color: "#f59e0b" },
|
||||
{ value: "add", label: "추가", icon: <Plus className="h-4 w-4" />, color: "#10b981" },
|
||||
{ value: "search", label: "검색", icon: <MousePointer className="h-4 w-4" />, color: "#8b5cf6" },
|
||||
{ value: "reset", label: "초기화", icon: <RotateCcw className="h-4 w-4" />, color: "#6b7280" },
|
||||
{ value: "submit", label: "제출", icon: <Send className="h-4 w-4" />, color: "#059669" },
|
||||
{ value: "close", label: "닫기", icon: <X className="h-4 w-4" />, color: "#6b7280" },
|
||||
{ value: "popup", label: "팝업 열기", icon: <ExternalLink className="h-4 w-4" />, color: "#8b5cf6" },
|
||||
{ value: "navigate", label: "페이지 이동", icon: <ExternalLink className="h-4 w-4" />, color: "#0ea5e9" },
|
||||
{ value: "custom", label: "사용자 정의", icon: <Settings className="h-4 w-4" />, color: "#64748b" },
|
||||
];
|
||||
|
||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateComponent }) => {
|
||||
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
||||
|
||||
// 로컬 상태 관리
|
||||
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>({
|
||||
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<ButtonTypeConfig>) => {
|
||||
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<ButtonTypeConfig> = { 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 (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||
<Settings className="h-4 w-4" />
|
||||
버튼 기능 설정
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 액션 타입 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">버튼 기능</Label>
|
||||
<Select value={localConfig.actionType} onValueChange={handleActionTypeChange}>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{actionTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
{option.icon}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedActionOption && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
{selectedActionOption.icon}
|
||||
<span>{selectedActionOption.label}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
style={{ backgroundColor: selectedActionOption.color + "20", color: selectedActionOption.color }}
|
||||
>
|
||||
{selectedActionOption.value}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 기본 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-medium">기본 설정</Label>
|
||||
|
||||
{/* 버튼 텍스트 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">버튼 텍스트</Label>
|
||||
<Input
|
||||
value={component.label || ""}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
onUpdateComponent({ label: newValue });
|
||||
}}
|
||||
placeholder="버튼에 표시될 텍스트"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 버튼 스타일 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">스타일</Label>
|
||||
<Select value={localConfig.variant} onValueChange={(value) => updateConfig({ variant: value as any })}>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">기본</SelectItem>
|
||||
<SelectItem value="destructive">위험</SelectItem>
|
||||
<SelectItem value="outline">외곽선</SelectItem>
|
||||
<SelectItem value="secondary">보조</SelectItem>
|
||||
<SelectItem value="ghost">투명</SelectItem>
|
||||
<SelectItem value="link">링크</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">크기</Label>
|
||||
<Select value={localConfig.size} onValueChange={(value) => updateConfig({ size: value as any })}>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sm">작음</SelectItem>
|
||||
<SelectItem value="default">기본</SelectItem>
|
||||
<SelectItem value="lg">큼</SelectItem>
|
||||
<SelectItem value="icon">아이콘</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 아이콘 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">아이콘 (Lucide 아이콘 이름)</Label>
|
||||
<Input
|
||||
value={localConfig.icon || ""}
|
||||
onChange={(e) => updateConfig({ icon: e.target.value })}
|
||||
placeholder="예: Save, Edit, Trash2"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 액션별 세부 설정 */}
|
||||
{localConfig.actionType === "delete" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
||||
삭제 확인 설정
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">확인 메시지</Label>
|
||||
<Input
|
||||
value={localConfig.confirmMessage || ""}
|
||||
onChange={(e) => updateConfig({ confirmMessage: e.target.value })}
|
||||
placeholder="정말로 삭제하시겠습니까?"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localConfig.actionType === "popup" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
||||
<ExternalLink className="h-3 w-3 text-purple-500" />
|
||||
팝업 설정
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">팝업 제목</Label>
|
||||
<Input
|
||||
value={localConfig.popupTitle || ""}
|
||||
onChange={(e) => updateConfig({ popupTitle: e.target.value })}
|
||||
placeholder="상세 정보"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">팝업 크기</Label>
|
||||
<Select
|
||||
value={localConfig.popupSize}
|
||||
onValueChange={(value) => updateConfig({ popupSize: value as any })}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sm">작음</SelectItem>
|
||||
<SelectItem value="md">보통</SelectItem>
|
||||
<SelectItem value="lg">큼</SelectItem>
|
||||
<SelectItem value="xl">매우 큼</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">팝업 내용</Label>
|
||||
<Textarea
|
||||
value={localConfig.popupContent || ""}
|
||||
onChange={(e) => updateConfig({ popupContent: e.target.value })}
|
||||
placeholder="여기에 팝업 내용을 입력하세요."
|
||||
className="h-16 resize-none text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localConfig.actionType === "navigate" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
||||
<ExternalLink className="h-3 w-3 text-blue-500" />
|
||||
페이지 이동 설정
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">이동할 URL</Label>
|
||||
<Input
|
||||
value={localConfig.navigateUrl || ""}
|
||||
onChange={(e) => updateConfig({ navigateUrl: e.target.value })}
|
||||
placeholder="/admin/users"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">열기 방식</Label>
|
||||
<Select
|
||||
value={localConfig.navigateTarget}
|
||||
onValueChange={(value) => updateConfig({ navigateTarget: value as any })}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_self">현재 창</SelectItem>
|
||||
<SelectItem value="_blank">새 창</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localConfig.actionType === "custom" && (
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
||||
<Settings className="h-3 w-3 text-gray-500" />
|
||||
사용자 정의 액션
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">JavaScript 코드</Label>
|
||||
<Textarea
|
||||
value={localConfig.customAction || ""}
|
||||
onChange={(e) => updateConfig({ customAction: e.target.value })}
|
||||
placeholder="alert('버튼이 클릭되었습니다!');"
|
||||
className="h-16 resize-none font-mono text-xs"
|
||||
/>
|
||||
<div className="text-xs text-gray-500">
|
||||
JavaScript 코드를 입력하세요. 예: alert(), console.log(), 함수 호출 등
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FileTypeConfig,
|
||||
CodeTypeConfig,
|
||||
EntityTypeConfig,
|
||||
ButtonTypeConfig,
|
||||
} from "@/types/screen";
|
||||
import { DateTypeConfigPanel } from "./webtype-configs/DateTypeConfigPanel";
|
||||
import { NumberTypeConfigPanel } from "./webtype-configs/NumberTypeConfigPanel";
|
||||
@@ -27,6 +28,7 @@ import { RadioTypeConfigPanel } from "./webtype-configs/RadioTypeConfigPanel";
|
||||
import { FileTypeConfigPanel } from "./webtype-configs/FileTypeConfigPanel";
|
||||
import { CodeTypeConfigPanel } from "./webtype-configs/CodeTypeConfigPanel";
|
||||
import { EntityTypeConfigPanel } from "./webtype-configs/EntityTypeConfigPanel";
|
||||
import { ButtonConfigPanel } from "./ButtonConfigPanel";
|
||||
|
||||
interface DetailSettingsPanelProps {
|
||||
selectedComponent?: ComponentData;
|
||||
@@ -162,6 +164,19 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({ select
|
||||
/>
|
||||
);
|
||||
|
||||
case "button":
|
||||
return (
|
||||
<ButtonConfigPanel
|
||||
key={`${widget.id}-button`}
|
||||
component={widget}
|
||||
onUpdateComponent={(updates) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
onUpdateProperty(widget.id, key, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div className="text-sm text-gray-500 italic">해당 웹타입의 상세 설정이 지원되지 않습니다.</div>;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const webTypeOptions: { value: WebType; label: string }[] = [
|
||||
{ value: "code", label: "코드" },
|
||||
{ value: "entity", label: "엔티티" },
|
||||
{ value: "file", label: "파일" },
|
||||
{ value: "button", label: "버튼" },
|
||||
];
|
||||
|
||||
export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
||||
|
||||
@@ -5,7 +5,24 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Table, Search, FileText, Grid3x3, Info } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
Search,
|
||||
FileText,
|
||||
Grid3x3,
|
||||
Info,
|
||||
FormInput,
|
||||
Save,
|
||||
X,
|
||||
Trash2,
|
||||
Edit,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Send,
|
||||
ExternalLink,
|
||||
MousePointer,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
|
||||
// 템플릿 컴포넌트 타입 정의
|
||||
export interface TemplateComponent {
|
||||
@@ -54,6 +71,33 @@ const templateComponents: TemplateComponent[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// 범용 버튼 템플릿
|
||||
{
|
||||
id: "universal-button",
|
||||
name: "버튼",
|
||||
description: "다양한 기능을 설정할 수 있는 범용 버튼. 상세설정에서 기능을 선택하세요.",
|
||||
category: "button",
|
||||
icon: <MousePointer className="h-4 w-4" />,
|
||||
defaultSize: { width: 80, height: 36 },
|
||||
components: [
|
||||
{
|
||||
type: "widget",
|
||||
widgetType: "button",
|
||||
label: "버튼",
|
||||
position: { x: 0, y: 0 },
|
||||
size: { width: 80, height: 36 },
|
||||
style: {
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "#ffffff",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface TemplatesPanelProps {
|
||||
|
||||
Reference in New Issue
Block a user