레이아웃 컴포넌트 단순화
This commit is contained in:
parent
4da06b2a56
commit
77a6b50761
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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()`를 실행해보세요! 🚀
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const CardLayoutDefinition = createLayoutDefinition({
|
|||
category: "dashboard",
|
||||
icon: "grid-3x3",
|
||||
component: CardLayoutWrapper,
|
||||
defaultSize: { width: 800, height: 400 },
|
||||
defaultConfig: {
|
||||
cardLayout: {
|
||||
columns: 3,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue