레이아웃 컴포넌트 단순화

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,40 +1345,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
[layout, gridInfo, screenResolution, snapToGrid, saveToHistory, openPanel],
);
// 컴포넌트 드래그 처리
const handleComponentDrop = useCallback(
(e: React.DragEvent, component: any) => {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
// handleZoneComponentDrop은 handleComponentDrop으로 대체됨
const dropX = e.clientX - rect.left;
const dropY = e.clientY - rect.top;
// 존 클릭 핸들러
const handleZoneClick = useCallback((zoneId: string) => {
console.log("🎯 존 클릭:", zoneId);
// 필요시 존 선택 로직 추가
}, []);
// 현재 해상도에 맞는 격자 정보 계산
const currentGridInfo = layout.gridSettings
? calculateGridInfo(screenResolution.width, screenResolution.height, {
columns: layout.gridSettings.columns,
gap: layout.gridSettings.gap,
padding: layout.gridSettings.padding,
snapToGrid: layout.gridSettings.snapToGrid || false,
})
: null;
// 격자 스냅 적용
const snappedPosition =
layout.gridSettings?.snapToGrid && currentGridInfo
? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings)
: { x: dropX, y: dropY, z: 1 };
console.log("🧩 컴포넌트 드롭:", {
componentName: component.name,
webType: component.webType,
dropPosition: { x: dropX, y: dropY },
snappedPosition,
});
// 웹타입별 기본 설정 생성
const getDefaultWebTypeConfig = (webType: string) => {
// 웹타입별 기본 설정 생성 함수를 상위로 이동
const getDefaultWebTypeConfig = useCallback((webType: string) => {
switch (webType) {
case "button":
return {
@ -1422,7 +1398,56 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
default:
return {};
}
};
}, []);
// 컴포넌트 드래그 처리 (캔버스 레벨 드롭)
const handleComponentDrop = useCallback(
(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;
const dropX = e.clientX - rect.left;
const dropY = e.clientY - rect.top;
// 현재 해상도에 맞는 격자 정보 계산
const currentGridInfo = layout.gridSettings
? calculateGridInfo(screenResolution.width, screenResolution.height, {
columns: layout.gridSettings.columns,
gap: layout.gridSettings.gap,
padding: layout.gridSettings.padding,
snapToGrid: layout.gridSettings.snapToGrid || false,
})
: null;
// 격자 스냅 적용
const snappedPosition =
layout.gridSettings?.snapToGrid && currentGridInfo
? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings)
: { x: dropX, y: dropY, z: 1 };
console.log("🧩 컴포넌트 드롭:", {
componentName: component.name,
webType: component.webType,
dropPosition: { x: dropX, y: dropY },
snappedPosition,
});
// 새 컴포넌트 생성
console.log("🔍 ScreenDesigner handleComponentDrop:", {
@ -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"
/>

View File

@ -8,7 +8,7 @@
2. [CLI를 이용한 자동 생성](#cli를-이용한-자동-생성)
3. [생성된 파일 구조](#생성된-파일-구조)
4. [레이아웃 커스터마이징](#레이아웃-커스터마이징)
5. [카드 레이아웃 상세 가이드](#카드-레이아웃-상세-가이드)
5. [카드 레이아웃 상세 가이드](#카드-레이아웃-상세-가이드)
6. [고급 설정](#고급-설정)
7. [문제 해결](#문제-해결)
@ -846,6 +846,62 @@ const getColumnLabel = (columnName: string) => {
<span>{getColumnLabel(columnName)}:</span>
```
#### 9. 레이아웃 드롭 시 오류 발생 문제 (9월 11일 해결됨)
```typescript
// ❌ 문제: 복잡한 존별 드롭 로직으로 인한 오류
- Runtime TypeError: Cannot read properties of undefined (reading 'width')
- 다중선택 기능 중단
- 존별 드롭 이벤트 충돌
// ✅ 해결: 드롭 시스템 완전 단순화
- 모든 존별 드롭 로직 제거
- 일반 캔버스 드롭만 사용
- 레이아웃은 시각적 가이드 역할만
- z-index 기반 레이어 분리 (레이아웃=1, 컴포넌트=2+)
```
##### 드롭 시스템 단순화 후 장점
- ✅ **안정성**: 복잡한 이벤트 체인 제거로 오류 가능성 감소
- ✅ **일관성**: 모든 영역에서 동일한 드롭 동작
- ✅ **성능**: 불필요한 prop 전달 및 매핑 로직 제거
- ✅ **유지보수**: 단순한 구조로 디버깅 및 수정 용이
##### 새로운 레이아웃 개발 시 주의사항
```typescript
// ✅ 올바른 레이아웃 구현
export const YourLayoutLayout: React.FC<YourLayoutProps> = ({ layout, isDesignMode, ...props }) => {
// 🚫 존별 드롭 이벤트 구현 금지
// onDrop, onDragOver 등 드롭 관련 이벤트 추가하지 않음
return (
<div className="your-layout">
{layout.zones.map((zone) => (
<div
key={zone.id}
className="zone-area"
// 🚫 드롭 이벤트 추가 금지
// onDrop={...} ❌
// onDragOver={...} ❌
>
{/* 존 내용 */}
</div>
))}
<style jsx>{`
.your-layout {
/* z-index는 1로 고정 (레이아웃 레이어) */
z-index: 1;
height: 100% !important;
}
`}</style>
</div>
);
};
```
### 디버깅 도구
#### 브라우저 개발자 도구
@ -1025,6 +1081,53 @@ node scripts/create-layout.js form-layout --category=form --zones=3 --descriptio
- ✅ **시각적 피드백**: 모든 레이아웃에서 존 경계 명확히 표시
- ✅ **React 경고 해결**: Key prop, DOM prop 전달 등 모든 경고 해결
#### 🔧 드롭 시스템 대폭 단순화 (9월 11일 추가)
기존의 복잡한 존별 드롭 시스템을 완전히 제거하고 단순한 캔버스 드롭 방식으로 통일했습니다:
##### 변경 사항
- ✅ **복잡한 드롭 로직 제거**: `onZoneComponentDrop`, `handleZoneComponentDrop` 등 모든 존별 드롭 이벤트 제거
- ✅ **일반 캔버스 드롭만 사용**: 모든 드롭이 `handleComponentDrop`으로 통일
- ✅ **레이아웃은 시각적 가이드 역할**: 레이아웃 존은 배치 가이드라인 역할만 수행
- ✅ **z-index 기반 레이어링**: 레이아웃 z-index=1, 컴포넌트 z-index=2+로 설정
- ✅ **prop 전달 체인 단순화**: 불필요한 prop 매핑 및 전달 로직 제거
##### 새로운 동작 방식
**이전 (복잡한 방식)**:
```
컴포넌트 드래그 → 레이아웃 존 감지 → 존별 드롭 이벤트 → 복잡한 매핑 → 오류 발생
```
**현재 (단순한 방식)**:
```
컴포넌트 드래그 → 캔버스에 드롭 → 일반 handleComponentDrop만 실행 → 안정적 동작
```
##### 해결된 문제들
- ✅ **Runtime TypeError 해결**: `selectedComponent.size.width` undefined 오류 완전 해결
- ✅ **다중선택 복구**: 드래그로 다중선택 기능 정상화
- ✅ **안정적 드롭**: 레이아웃 위든 어디든 일관된 드롭 동작
- ✅ **코드 단순화**: 복잡한 존별 로직 제거로 유지보수성 향상
##### 기술적 변경점
1. **GridLayout, FlexboxLayout**: `onDragOver`, `onDrop` 이벤트 핸들러 제거
2. **ScreenDesigner**: `onZoneComponentDrop` prop 전달 제거
3. **DynamicComponentRenderer**: `onComponentDrop` 매핑 로직 제거
4. **DynamicLayoutRenderer**: 존별 prop 전달 제거
5. **RealtimePreviewDynamic**: z-index 기반 레이어링 적용
##### 개발자 가이드
새로운 시스템에서는:
- 🚫 **존별 드롭 로직 구현 금지**: 모든 드롭은 캔버스 레벨에서 처리
- ✅ **시각적 가이드만 제공**: 레이아웃은 배치 가이드라인 역할만
- ✅ **z-index로 레이어 관리**: 레이아웃=1, 컴포넌트=2+ 설정
- ✅ **단순한 이벤트 처리**: 복잡한 이벤트 체인 대신 직접적인 핸들링
#### 개발자 도구 강화
- ✅ **디버깅 로그**: 카드 설정 로드, 데이터 가져오기 등 상세 로깅
@ -1042,9 +1145,38 @@ node scripts/create-layout.js form-layout --category=form --zones=3 --descriptio
### 🔮 향후 계획
#### 새로운 레이아웃 타입
- **Table Layout**: 데이터 테이블 전용 레이아웃
- **Form Layout**: 폼 입력에 최적화된 레이아웃
- **Dashboard Layout**: 위젯 배치에 특화된 레이아웃
- **Mobile Responsive**: 모바일 대응 반응형 레이아웃
#### 시스템 개선
- **레이아웃 테마 시스템**: 다크/라이트 모드 지원
- **레이아웃 스타일 프리셋**: 미리 정의된 스타일 템플릿
- **레이아웃 애니메이션**: 전환 효과 및 인터랙션 개선
- **성능 최적화**: 가상화 및 지연 로딩 적용
#### 개발자 도구
- **레이아웃 빌더 GUI**: 코드 없이 레이아웃 생성 도구
- **실시간 프리뷰**: 레이아웃 편집 중 실시간 미리보기
- **레이아웃 디버거**: 시각적 디버깅 도구
- **성능 모니터링**: 레이아웃 렌더링 성능 분석
### 🎯 중요한 변화: 단순화된 드롭 시스템
**2025년 9월 11일**부터 모든 레이아웃에서 **복잡한 존별 드롭 로직이 완전히 제거**되었습니다.
새로운 시스템의 핵심 원칙:
- 🎯 **레이아웃 = 시각적 가이드**: 배치 참고용으로만 사용
- 🎯 **캔버스 = 실제 배치**: 모든 컴포넌트는 캔버스에 자유롭게 배치
- 🎯 **z-index = 레이어 분리**: 레이아웃(1) 위에 컴포넌트(2+) 배치
- 🎯 **단순함 = 안정성**: 복잡한 로직 제거로 오류 최소화
이 변화로 인해:
- ✅ 모든 드롭 관련 오류 해결
- ✅ 다중선택 기능 정상화
- ✅ 레이아웃 개발이 더욱 단순해짐
- ✅ 시스템 전체 안정성 크게 향상
더 자세한 정보가 필요하면 각 레이아웃의 `README.md` 파일을 참고하거나, 브라우저 개발자 도구에서 `window.__LAYOUT_REGISTRY__.help()`를 실행해보세요! 🚀

View File

@ -16,6 +16,8 @@ export interface ComponentRenderer {
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
children?: React.ReactNode;
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
onZoneClick?: (zoneId: string) => void;
[key: string]: any;
}): React.ReactElement;
}
@ -89,6 +91,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onUpdateLayout={props.onUpdateLayout}
// onComponentDrop 제거 - 일반 캔버스 드롭만 사용
onZoneClick={props.onZoneClick}
{...props}
/>
);

View File

@ -10,13 +10,14 @@ export interface DynamicLayoutRendererProps {
isDesignMode?: boolean;
isSelected?: boolean;
onClick?: (e: React.MouseEvent) => void;
onZoneClick?: (zoneId: string, e: React.MouseEvent) => void;
onComponentDrop?: (zoneId: string, component: ComponentData, e: React.DragEvent) => void;
onZoneClick?: (zoneId: string) => void;
onComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: (e: React.DragEvent) => void;
onUpdateLayout?: (updatedLayout: LayoutComponent) => void; // 레이아웃 업데이트 콜백
className?: string;
style?: React.CSSProperties;
[key: string]: any; // 추가 props 허용
}
export const DynamicLayoutRenderer: React.FC<DynamicLayoutRendererProps> = ({
@ -77,7 +78,7 @@ export const DynamicLayoutRenderer: React.FC<DynamicLayoutRendererProps> = ({
isSelected={isSelected}
onClick={onClick}
onZoneClick={onZoneClick}
onComponentDrop={onComponentDrop}
// onComponentDrop 제거
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onUpdateLayout={onUpdateLayout}

View File

@ -17,6 +17,8 @@ export const AccordionLayout: React.FC<AccordionLayoutProps> = ({
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.accordion) {
@ -57,7 +59,6 @@ export const AccordionLayout: React.FC<AccordionLayoutProps> = ({
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onZoneClick,
onComponentDrop,
onDragStart,
onDragEnd,
@ -205,7 +206,7 @@ const AccordionSection: React.FC<{
e.preventDefault();
e.stopPropagation();
if (onComponentDrop) {
onComponentDrop(e, zone.id); // 존 ID와 함께 드롭 이벤트 전달
onComponentDrop(e, zone.id, layout.id); // 존 ID와 레이아웃 ID를 함께 전달
}
}}
onClick={(e) => {

View File

@ -169,7 +169,7 @@ export const CardLayoutLayout: React.FC<CardLayoutProps> = ({
className="hover:border-blue-500 hover:shadow-md"
>
{/* 존 라벨 */}
<div className="absolute left-2 top-2 z-10">
<div className="absolute top-2 left-2 z-10">
<div className="rounded bg-blue-500 px-2 py-1 text-xs text-white">{zone.name}</div>
</div>
@ -247,7 +247,7 @@ export const CardLayoutLayout: React.FC<CardLayoutProps> = ({
return (
<div key={idx} className="flex justify-between text-xs">
<span className="capitalize text-gray-500">{getColumnLabel(columnName)}:</span>
<span className="text-gray-500 capitalize">{getColumnLabel(columnName)}:</span>
<span className="font-medium text-gray-700">{value}</span>
</div>
);

View File

@ -24,6 +24,7 @@ export const CardLayoutDefinition = createLayoutDefinition({
category: "dashboard",
icon: "grid-3x3",
component: CardLayoutWrapper,
defaultSize: { width: 800, height: 400 },
defaultConfig: {
cardLayout: {
columns: 3,

View File

@ -17,6 +17,8 @@ export const FlexboxLayout: React.FC<FlexboxLayoutProps> = ({
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.flexbox) {
@ -98,7 +100,6 @@ export const FlexboxLayout: React.FC<FlexboxLayoutProps> = ({
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onZoneClick,
onComponentDrop,
onDragStart,
onDragEnd,
@ -200,17 +201,7 @@ export const FlexboxLayout: React.FC<FlexboxLayoutProps> = ({
e.currentTarget.style.borderColor = "#e5e7eb";
e.currentTarget.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1)";
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
if (onComponentDrop) {
onComponentDrop(e, zone.id); // 존 ID와 함께 드롭 이벤트 전달
}
}}
// 드롭 이벤트 제거 - 일반 캔버스 드롭만 사용
onClick={(e) => {
e.stopPropagation();
if (onZoneClick) {

View File

@ -17,6 +17,8 @@ export const GridLayout: React.FC<GridLayoutProps> = ({
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.grid) {
@ -66,7 +68,6 @@ export const GridLayout: React.FC<GridLayoutProps> = ({
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onZoneClick,
onComponentDrop,
onDragStart,
onDragEnd,
@ -158,17 +159,7 @@ export const GridLayout: React.FC<GridLayoutProps> = ({
e.currentTarget.style.borderColor = "#e5e7eb";
e.currentTarget.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1)";
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
}}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
if (onComponentDrop) {
onComponentDrop(e, zone.id); // 존 ID와 함께 드롭 이벤트 전달
}
}}
// 드롭 이벤트 제거 - 일반 캔버스 드롭만 사용
onClick={(e) => {
e.stopPropagation();
if (onZoneClick) {

View File

@ -17,6 +17,8 @@ export const HeroSectionLayout: React.FC<HeroSectionLayoutProps> = ({
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.heroSection) {
@ -56,7 +58,6 @@ export const HeroSectionLayout: React.FC<HeroSectionLayoutProps> = ({
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onZoneClick,
onComponentDrop,
onDragStart,
onDragEnd,

View File

@ -17,6 +17,8 @@ export const SplitLayout: React.FC<SplitLayoutProps> = ({
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.split) {
@ -57,7 +59,6 @@ export const SplitLayout: React.FC<SplitLayoutProps> = ({
onSelectComponent,
isDesignMode: _isDesignMode,
allComponents,
onZoneClick,
onComponentDrop,
onDragStart,
onDragEnd,