레이아웃 컴포넌트 단순화

This commit is contained in:
kjs
2025-09-11 16:21:00 +09:00
parent 4da06b2a56
commit 77a6b50761
14 changed files with 312 additions and 106 deletions

View File

@@ -28,7 +28,9 @@ interface RealtimePreviewProps {
onDragEnd?: () => void;
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
selectedScreen?: any; // 선택된 화면 정보
selectedScreen?: any;
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@@ -67,6 +69,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onGroupToggle,
children,
selectedScreen,
onZoneComponentDrop,
onZoneClick,
}) => {
const { id, type, position, size, style: componentStyle } = component;
@@ -79,13 +83,13 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
}
: {};
// 컴포넌트 기본 스타일
// 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래
const baseStyle = {
left: `${position.x}px`,
top: `${position.y}px`,
width: `${size.width}px`,
height: `${size.height}px`,
zIndex: position.z || 1,
width: `${size?.width || 100}px`,
height: `${size?.height || 36}px`,
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
...componentStyle,
};
@@ -123,6 +127,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onDragEnd={onDragEnd}
children={children}
selectedScreen={selectedScreen}
onZoneComponentDrop={onZoneComponentDrop}
onZoneClick={onZoneClick}
/>
</div>

View File

@@ -1345,9 +1345,81 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
[layout, gridInfo, screenResolution, snapToGrid, saveToHistory, openPanel],
);
// 컴포넌트 드래그 처리
// handleZoneComponentDrop은 handleComponentDrop으로 대체됨
// 존 클릭 핸들러
const handleZoneClick = useCallback((zoneId: string) => {
console.log("🎯 존 클릭:", zoneId);
// 필요시 존 선택 로직 추가
}, []);
// 웹타입별 기본 설정 생성 함수를 상위로 이동
const getDefaultWebTypeConfig = useCallback((webType: string) => {
switch (webType) {
case "button":
return {
actionType: "custom",
variant: "default",
confirmationMessage: "",
popupTitle: "",
popupContent: "",
navigateUrl: "",
};
case "date":
return {
format: "YYYY-MM-DD",
showTime: false,
placeholder: "날짜를 선택하세요",
};
case "number":
return {
format: "integer",
placeholder: "숫자를 입력하세요",
};
case "select":
return {
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
{ label: "옵션 3", value: "option3" },
],
multiple: false,
searchable: false,
placeholder: "옵션을 선택하세요",
};
case "file":
return {
accept: ["*/*"],
maxSize: 10485760, // 10MB
multiple: false,
showPreview: true,
autoUpload: false,
};
default:
return {};
}
}, []);
// 컴포넌트 드래그 처리 (캔버스 레벨 드롭)
const handleComponentDrop = useCallback(
(e: React.DragEvent, component: any) => {
(e: React.DragEvent, component?: any, zoneId?: string, layoutId?: string) => {
// 존별 드롭인 경우 dragData에서 컴포넌트 정보 추출
if (!component) {
const dragData = e.dataTransfer.getData("application/json");
if (!dragData) return;
try {
const parsedData = JSON.parse(dragData);
if (parsedData.type === "component") {
component = parsedData.component;
} else {
return;
}
} catch (error) {
console.error("드래그 데이터 파싱 오류:", error);
return;
}
}
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
@@ -1377,53 +1449,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
snappedPosition,
});
// 웹타입별 기본 설정 생성
const getDefaultWebTypeConfig = (webType: string) => {
switch (webType) {
case "button":
return {
actionType: "custom",
variant: "default",
confirmationMessage: "",
popupTitle: "",
popupContent: "",
navigateUrl: "",
};
case "date":
return {
format: "YYYY-MM-DD",
showTime: false,
placeholder: "날짜를 선택하세요",
};
case "number":
return {
format: "integer",
placeholder: "숫자를 입력하세요",
};
case "select":
return {
options: [
{ label: "옵션 1", value: "option1" },
{ label: "옵션 2", value: "option2" },
{ label: "옵션 3", value: "option3" },
],
multiple: false,
searchable: false,
placeholder: "옵션을 선택하세요",
};
case "file":
return {
accept: ["*/*"],
maxSize: 10485760, // 10MB
multiple: false,
showPreview: true,
autoUpload: false,
};
default:
return {};
}
};
// 새 컴포넌트 생성
console.log("🔍 ScreenDesigner handleComponentDrop:", {
componentName: component.name,
@@ -3105,8 +3130,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
onDragStart={(e) => startComponentDrag(component, e)}
onDragEnd={endDrag}
selectedScreen={selectedScreen}
// onZoneComponentDrop 제거
onZoneClick={handleZoneClick}
>
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 */}
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
@@ -3182,6 +3209,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
onDragStart={(e) => startComponentDrag(child, e)}
onDragEnd={endDrag}
selectedScreen={selectedScreen}
// onZoneComponentDrop 제거
onZoneClick={handleZoneClick}
/>
);
})}
@@ -3290,7 +3319,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
id="layouts"
title="레이아웃"
isOpen={panelStates.layouts?.isOpen || false}
onClose={() => closePanelState("layouts")}
onClose={() => closePanel("layouts")}
position="left"
width={380}
height={700}
@@ -3304,6 +3333,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
}}
gridSettings={layout.gridSettings || { columns: 12, gap: 16, padding: 16, snapToGrid: true }}
screenResolution={screenResolution}
/>
</FloatingPanel>

View File

@@ -9,6 +9,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Grid, Layout, LayoutDashboard, Table, Navigation, FileText, Building, Search, Plus } from "lucide-react";
import { LAYOUT_CATEGORIES, LayoutCategory } from "@/types/layout";
import { LayoutRegistry } from "@/lib/registry/LayoutRegistry";
import { calculateGridInfo, calculateWidthFromColumns } from "@/lib/utils/gridUtils";
// 카테고리 아이콘 매핑
const CATEGORY_ICONS = {
@@ -36,9 +37,25 @@ interface LayoutsPanelProps {
onDragStart: (e: React.DragEvent, layoutData: any) => void;
onLayoutSelect?: (layoutDefinition: any) => void;
className?: string;
gridSettings?: {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
};
screenResolution?: {
width: number;
height: number;
};
}
export default function LayoutsPanel({ onDragStart, onLayoutSelect, className }: LayoutsPanelProps) {
export default function LayoutsPanel({
onDragStart,
onLayoutSelect,
className,
gridSettings,
screenResolution,
}: LayoutsPanelProps) {
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
@@ -78,6 +95,29 @@ export default function LayoutsPanel({ onDragStart, onLayoutSelect, className }:
// 레이아웃 드래그 시작 핸들러
const handleDragStart = (e: React.DragEvent, layoutDefinition: any) => {
// 격자 기반 동적 크기 계산
let calculatedSize = layoutDefinition.defaultSize || { width: 400, height: 300 };
if (gridSettings && screenResolution && layoutDefinition.id === "card-layout") {
// 카드 레이아웃의 경우 8그리드 컬럼에 맞는 너비 계산
const gridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, gridSettings);
const calculatedWidth = calculateWidthFromColumns(8, gridInfo, gridSettings);
calculatedSize = {
width: Math.max(calculatedWidth, 400), // 최소 400px 보장
height: 400, // 높이는 고정
};
console.log("🎯 카드 레이아웃 동적 크기 계산:", {
gridColumns: 8,
screenResolution,
gridSettings,
gridInfo,
calculatedWidth,
finalSize: calculatedSize,
});
}
// 새 레이아웃 컴포넌트 데이터 생성
const layoutData = {
id: `layout_${Date.now()}`,
@@ -88,8 +128,9 @@ export default function LayoutsPanel({ onDragStart, onLayoutSelect, className }:
children: [],
allowedComponentTypes: [],
position: { x: 0, y: 0 },
size: layoutDefinition.defaultSize || { width: 400, height: 300 },
size: calculatedSize,
label: layoutDefinition.name,
gridColumns: layoutDefinition.id === "card-layout" ? 8 : 1, // 카드 레이아웃은 기본 8그리드
};
// 드래그 데이터 설정
@@ -192,4 +233,3 @@ export default function LayoutsPanel({ onDragStart, onLayoutSelect, className }:
</div>
);
}

View File

@@ -191,9 +191,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
positionX: currentPosition.x.toString(),
positionY: currentPosition.y.toString(),
positionZ: selectedComponent?.position.z?.toString() || "1",
width: selectedComponent?.size.width?.toString() || "0",
height: selectedComponent?.size.height?.toString() || "0",
gridColumns: selectedComponent?.gridColumns?.toString() || "1",
width: selectedComponent?.size?.width?.toString() || "0",
height: selectedComponent?.size?.height?.toString() || "0",
gridColumns:
selectedComponent?.gridColumns?.toString() ||
(selectedComponent?.type === "layout" && (selectedComponent as any)?.layoutType === "card-layout" ? "8" : "1"),
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151",
@@ -244,14 +246,18 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
description: area?.description || "",
positionX: currentPos.x.toString(),
positionY: currentPos.y.toString(),
positionZ: selectedComponent.position.z?.toString() || "1",
width: selectedComponent.size.width?.toString() || "0",
height: selectedComponent.size.height?.toString() || "0",
gridColumns: selectedComponent.gridColumns?.toString() || "1",
labelText: selectedComponent.style?.labelText || selectedComponent.label || "",
labelFontSize: selectedComponent.style?.labelFontSize || "12px",
labelColor: selectedComponent.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px",
positionZ: selectedComponent?.position?.z?.toString() || "1",
width: selectedComponent?.size?.width?.toString() || "0", // 안전한 접근
height: selectedComponent?.size?.height?.toString() || "0", // 안전한 접근
gridColumns:
selectedComponent?.gridColumns?.toString() ||
(selectedComponent?.type === "layout" && (selectedComponent as any)?.layoutType === "card-layout"
? "8"
: "1"),
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent?.style?.labelMarginBottom || "4px",
required: widget?.required || false,
readonly: widget?.readonly || false,
labelDisplay: selectedComponent.style?.labelDisplay !== false,
@@ -584,7 +590,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, width: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, width: Number(newValue) });
onUpdateProperty("size", { ...(selectedComponent.size || {}), width: Number(newValue) });
}}
className="mt-1"
/>
@@ -601,7 +607,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, height: newValue }));
onUpdateProperty("size", { ...selectedComponent.size, height: Number(newValue) });
onUpdateProperty("size", { ...(selectedComponent.size || {}), height: Number(newValue) });
}}
className="mt-1"
/>