revert: e27845a 커밋의 변경사항 되돌림 - 화면 레이아웃 문제 수정
This commit is contained in:
parent
02644f38ee
commit
15f21a1142
17
.cursorrules
17
.cursorrules
|
|
@ -1,22 +1,5 @@
|
||||||
# Cursor Rules for ERP-node Project
|
# Cursor Rules for ERP-node Project
|
||||||
|
|
||||||
## 🔥 필수 확인 규칙 (작업 시작 전 & 완료 후)
|
|
||||||
|
|
||||||
**AI 에이전트는 모든 작업을 시작하기 전과 완료한 후에 반드시 다음 파일을 확인해야 합니다:**
|
|
||||||
- [AI-개발자 협업 작업 수칙](.cursor/rules/ai-developer-collaboration-rules.mdc)
|
|
||||||
|
|
||||||
**핵심 3원칙:**
|
|
||||||
1. **확인 우선** 🔍 - 추측하지 말고, 항상 확인하고 작업
|
|
||||||
2. **한 번에 하나** 🎯 - 여러 문제를 동시에 해결하려 하지 말기
|
|
||||||
3. **철저한 마무리** ✨ - 로그 제거, 테스트, 명확한 설명
|
|
||||||
|
|
||||||
**절대 금지:**
|
|
||||||
- ❌ 확인 없이 "완료했습니다" 말하기
|
|
||||||
- ❌ 데이터베이스 컬럼명 추측하기 (반드시 MCP로 확인)
|
|
||||||
- ❌ 디버깅 로그를 남겨둔 채 작업 종료
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 최우선 보안 규칙: 멀티테넌시
|
## 🚨 최우선 보안 규칙: 멀티테넌시
|
||||||
|
|
||||||
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
|
**모든 코드 작성/수정 완료 후 반드시 다음 파일을 확인하세요:**
|
||||||
|
|
|
||||||
|
|
@ -286,5 +286,4 @@ uploads/
|
||||||
*.hwp
|
*.hwp
|
||||||
*.hwpx
|
*.hwpx
|
||||||
|
|
||||||
claude.md
|
claude.md
|
||||||
.cursor/rules/ai-developer-collaboration-rules.mdc
|
|
||||||
|
|
@ -57,7 +57,7 @@ interface RealtimePreviewProps {
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isDesignMode?: boolean;
|
isDesignMode?: boolean;
|
||||||
onClick?: (e?: React.MouseEvent) => void;
|
onClick?: (e?: React.MouseEvent) => void;
|
||||||
onDragStart?: (e: React.MouseEvent | React.DragEvent) => void; // MouseEvent도 허용
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
|
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
|
||||||
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
|
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
|
||||||
|
|
@ -247,13 +247,6 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { type, id, position, size, style = {} } = component;
|
const { type, id, position, size, style = {} } = component;
|
||||||
|
|
||||||
// 🔍 [디버깅] 렌더링 시 크기 로그
|
|
||||||
console.log("🎨 [RealtimePreview] 렌더링", {
|
|
||||||
componentId: id,
|
|
||||||
size,
|
|
||||||
position,
|
|
||||||
});
|
|
||||||
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
|
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
|
||||||
const [actualHeight, setActualHeight] = useState<number | null>(null);
|
const [actualHeight, setActualHeight] = useState<number | null>(null);
|
||||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -465,17 +458,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
|
||||||
// 디자인 모드에서만 드래그 시작 (캔버스 내 이동용)
|
|
||||||
if (isDesignMode && onDragStart) {
|
|
||||||
e.stopPropagation();
|
|
||||||
// MouseEvent를 그대로 전달
|
|
||||||
onDragStart(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent) => {
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
// HTML5 Drag API (팔레트에서 캔버스로 드래그용)
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDragStart?.(e);
|
onDragStart?.(e);
|
||||||
};
|
};
|
||||||
|
|
@ -490,9 +473,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
className="absolute cursor-pointer"
|
className="absolute cursor-pointer"
|
||||||
style={{ ...componentStyle, ...selectionStyle }}
|
style={{ ...componentStyle, ...selectionStyle }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseDown={isDesignMode ? handleMouseDown : undefined}
|
draggable
|
||||||
draggable={!isDesignMode} // 디자인 모드가 아닐 때만 draggable (팔레트용)
|
onDragStart={handleDragStart}
|
||||||
onDragStart={!isDesignMode ? handleDragStart : undefined}
|
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* 컴포넌트 타입별 렌더링 */}
|
{/* 컴포넌트 타입별 렌더링 */}
|
||||||
|
|
|
||||||
|
|
@ -264,9 +264,6 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
height: getHeight(),
|
height: getHeight(),
|
||||||
zIndex: component.type === "layout" ? 1 : position.z || 2,
|
zIndex: component.type === "layout" ? 1 : position.z || 2,
|
||||||
...componentStyle,
|
...componentStyle,
|
||||||
// 🔥 중요: componentStyle.width를 덮어쓰기 위해 다시 설정
|
|
||||||
width: getWidth(), // size.width 기반 픽셀 값으로 강제
|
|
||||||
height: getHeight(), // size.height 기반 픽셀 값으로 강제
|
|
||||||
right: undefined,
|
right: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,13 @@ import {
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
snapSizeToGrid,
|
snapSizeToGrid,
|
||||||
generateGridLines,
|
generateGridLines,
|
||||||
|
updateSizeFromGridColumns,
|
||||||
|
adjustGridColumnsFromSize,
|
||||||
alignGroupChildrenToGrid,
|
alignGroupChildrenToGrid,
|
||||||
calculateOptimalGroupSize,
|
calculateOptimalGroupSize,
|
||||||
normalizeGroupChildPositions,
|
normalizeGroupChildPositions,
|
||||||
|
calculateWidthFromColumns,
|
||||||
|
GridSettings as GridUtilSettings,
|
||||||
} from "@/lib/utils/gridUtils";
|
} from "@/lib/utils/gridUtils";
|
||||||
import { GroupingToolbar } from "./GroupingToolbar";
|
import { GroupingToolbar } from "./GroupingToolbar";
|
||||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||||
|
|
@ -103,8 +107,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const [layout, setLayout] = useState<LayoutData>({
|
const [layout, setLayout] = useState<LayoutData>({
|
||||||
components: [],
|
components: [],
|
||||||
gridSettings: {
|
gridSettings: {
|
||||||
snapToGrid: true, // 격자 스냅 ON
|
columns: 12,
|
||||||
showGrid: false, // 격자 표시 OFF
|
gap: 16,
|
||||||
|
padding: 0,
|
||||||
|
snapToGrid: true,
|
||||||
|
showGrid: false, // 기본값 false로 변경
|
||||||
gridColor: "#d1d5db",
|
gridColor: "#d1d5db",
|
||||||
gridOpacity: 0.5,
|
gridOpacity: 0.5,
|
||||||
},
|
},
|
||||||
|
|
@ -533,31 +540,107 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
gridInfo &&
|
gridInfo &&
|
||||||
newComp.type !== "group"
|
newComp.type !== "group"
|
||||||
) {
|
) {
|
||||||
// 🔥 10px 고정 격자로 스냅
|
// 현재 해상도에 맞는 격자 정보로 스냅 적용
|
||||||
const currentGridInfo = calculateGridInfo(
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
screenResolution.width,
|
columns: prevLayout.gridSettings.columns,
|
||||||
screenResolution.height,
|
gap: prevLayout.gridSettings.gap,
|
||||||
prevLayout.gridSettings,
|
padding: prevLayout.gridSettings.padding,
|
||||||
|
snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
||||||
|
});
|
||||||
|
const snappedSize = snapSizeToGrid(
|
||||||
|
newComp.size,
|
||||||
|
currentGridInfo,
|
||||||
|
prevLayout.gridSettings as GridUtilSettings,
|
||||||
);
|
);
|
||||||
const snappedSize = snapSizeToGrid(newComp.size, currentGridInfo, prevLayout.gridSettings);
|
|
||||||
newComp.size = snappedSize;
|
newComp.size = snappedSize;
|
||||||
|
|
||||||
|
// 크기 변경 시 gridColumns도 자동 조정
|
||||||
|
const adjustedColumns = adjustGridColumnsFromSize(
|
||||||
|
newComp,
|
||||||
|
currentGridInfo,
|
||||||
|
prevLayout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
if (newComp.gridColumns !== adjustedColumns) {
|
||||||
|
newComp.gridColumns = adjustedColumns;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ gridColumns 로직 제거: 10px 고정 격자에서는 불필요
|
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정
|
||||||
|
if (path === "gridColumns" && prevLayout.gridSettings?.snapToGrid && newComp.type !== "group") {
|
||||||
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
|
columns: prevLayout.gridSettings.columns,
|
||||||
|
gap: prevLayout.gridSettings.gap,
|
||||||
|
padding: prevLayout.gridSettings.padding,
|
||||||
|
snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// gridColumns에 맞는 정확한 너비 계산
|
||||||
|
const newWidth = calculateWidthFromColumns(
|
||||||
|
newComp.gridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
prevLayout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
newComp.size = {
|
||||||
|
...newComp.size,
|
||||||
|
width: newWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 위치 변경 시 격자 스냅 적용 (그룹 내부 컴포넌트 포함)
|
// 위치 변경 시 격자 스냅 적용 (그룹 내부 컴포넌트 포함)
|
||||||
if (
|
if (
|
||||||
(path === "position.x" || path === "position.y" || path === "position") &&
|
(path === "position.x" || path === "position.y" || path === "position") &&
|
||||||
layout.gridSettings?.snapToGrid
|
layout.gridSettings?.snapToGrid
|
||||||
) {
|
) {
|
||||||
// 🔥 10px 고정 격자
|
// 현재 해상도에 맞는 격자 정보 계산
|
||||||
const currentGridInfo = calculateGridInfo(
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
screenResolution.width,
|
columns: layout.gridSettings.columns,
|
||||||
screenResolution.height,
|
gap: layout.gridSettings.gap,
|
||||||
layout.gridSettings,
|
padding: layout.gridSettings.padding,
|
||||||
);
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
||||||
const snappedPosition = snapToGrid(newComp.position, currentGridInfo, layout.gridSettings);
|
});
|
||||||
newComp.position = snappedPosition;
|
|
||||||
|
// 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용
|
||||||
|
if (newComp.parentId && currentGridInfo) {
|
||||||
|
const { columnWidth } = currentGridInfo;
|
||||||
|
const { gap } = layout.gridSettings;
|
||||||
|
|
||||||
|
// 그룹 내부 패딩 고려한 격자 정렬
|
||||||
|
const padding = 16;
|
||||||
|
const effectiveX = newComp.position.x - padding;
|
||||||
|
const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16)));
|
||||||
|
const snappedX = padding + columnIndex * (columnWidth + (gap || 16));
|
||||||
|
|
||||||
|
// Y 좌표는 10px 단위로 스냅
|
||||||
|
const effectiveY = newComp.position.y - padding;
|
||||||
|
const rowIndex = Math.round(effectiveY / 10);
|
||||||
|
const snappedY = padding + rowIndex * 10;
|
||||||
|
|
||||||
|
// 크기도 외부 격자와 동일하게 스냅
|
||||||
|
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
||||||
|
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
|
||||||
|
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
||||||
|
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
|
||||||
|
const snappedHeight = Math.max(10, newComp.size.height);
|
||||||
|
|
||||||
|
newComp.position = {
|
||||||
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||||
|
y: Math.max(padding, snappedY),
|
||||||
|
z: newComp.position.z || 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
newComp.size = {
|
||||||
|
width: snappedWidth,
|
||||||
|
height: snappedHeight,
|
||||||
|
};
|
||||||
|
} else if (newComp.type !== "group") {
|
||||||
|
// 그룹이 아닌 일반 컴포넌트만 격자 스냅 적용
|
||||||
|
const snappedPosition = snapToGrid(
|
||||||
|
newComp.position,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
newComp.position = snappedPosition;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newComp;
|
return newComp;
|
||||||
|
|
@ -820,21 +903,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const { convertLayoutComponents } = await import("@/lib/utils/webTypeConfigConverter");
|
const { convertLayoutComponents } = await import("@/lib/utils/webTypeConfigConverter");
|
||||||
const convertedComponents = convertLayoutComponents(layoutToUse.components);
|
const convertedComponents = convertLayoutComponents(layoutToUse.components);
|
||||||
|
|
||||||
// 🔥 10px 고정 격자 시스템으로 자동 마이그레이션
|
// 기본 격자 설정 보장 (격자 표시와 스냅 기본 활성화)
|
||||||
// 이전 columns, gap, padding 설정을 제거하고 새 시스템으로 변환
|
|
||||||
const layoutWithDefaultGrid = {
|
const layoutWithDefaultGrid = {
|
||||||
...layoutToUse,
|
...layoutToUse,
|
||||||
components: convertedComponents, // 변환된 컴포넌트 사용
|
components: convertedComponents, // 변환된 컴포넌트 사용
|
||||||
gridSettings: {
|
gridSettings: {
|
||||||
// 🗑️ 제거: columns, gap, padding (더 이상 사용하지 않음)
|
columns: layoutToUse.gridSettings?.columns || 12, // DB 값 우선, 없으면 기본값 12
|
||||||
|
gap: layoutToUse.gridSettings?.gap ?? 16, // DB 값 우선, 없으면 기본값 16
|
||||||
|
padding: 0, // padding은 항상 0으로 강제
|
||||||
snapToGrid: layoutToUse.gridSettings?.snapToGrid ?? true, // DB 값 우선
|
snapToGrid: layoutToUse.gridSettings?.snapToGrid ?? true, // DB 값 우선
|
||||||
showGrid: layoutToUse.gridSettings?.showGrid ?? false, // DB 값 우선
|
showGrid: layoutToUse.gridSettings?.showGrid ?? false, // DB 값 우선
|
||||||
gridColor: layoutToUse.gridSettings?.gridColor || "#d1d5db",
|
gridColor: layoutToUse.gridSettings?.gridColor || "#d1d5db",
|
||||||
gridOpacity: layoutToUse.gridSettings?.gridOpacity ?? 0.5,
|
gridOpacity: layoutToUse.gridSettings?.gridOpacity ?? 0.5,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("✅ 격자 설정 로드 (10px 고정):", layoutWithDefaultGrid.gridSettings);
|
|
||||||
|
|
||||||
// 저장된 해상도 정보가 있으면 적용, 없으면 기본값 사용
|
// 저장된 해상도 정보가 있으면 적용, 없으면 기본값 사용
|
||||||
if (layoutToUse.screenResolution) {
|
if (layoutToUse.screenResolution) {
|
||||||
|
|
@ -992,12 +1074,51 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
};
|
};
|
||||||
}, [MIN_ZOOM, MAX_ZOOM]);
|
}, [MIN_ZOOM, MAX_ZOOM]);
|
||||||
|
|
||||||
// 격자 설정 업데이트 (10px 고정 격자 - 자동 스냅 제거)
|
// 격자 설정 업데이트 및 컴포넌트 자동 스냅
|
||||||
const updateGridSettings = useCallback(
|
const updateGridSettings = useCallback(
|
||||||
(newGridSettings: GridSettings) => {
|
(newGridSettings: GridSettings) => {
|
||||||
// 단순히 격자 설정만 업데이트 (컴포넌트 자동 이동 없음)
|
|
||||||
const newLayout = { ...layout, gridSettings: newGridSettings };
|
const newLayout = { ...layout, gridSettings: newGridSettings };
|
||||||
|
|
||||||
|
// 격자 스냅이 활성화된 경우, 모든 컴포넌트를 새로운 격자에 맞게 조정
|
||||||
|
if (newGridSettings.snapToGrid && screenResolution.width > 0) {
|
||||||
|
// 새로운 격자 설정으로 격자 정보 재계산 (해상도 기준)
|
||||||
|
const newGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
|
columns: newGridSettings.columns,
|
||||||
|
gap: newGridSettings.gap,
|
||||||
|
padding: newGridSettings.padding,
|
||||||
|
snapToGrid: newGridSettings.snapToGrid || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridUtilSettings = {
|
||||||
|
columns: newGridSettings.columns,
|
||||||
|
gap: newGridSettings.gap,
|
||||||
|
padding: newGridSettings.padding,
|
||||||
|
snapToGrid: newGridSettings.snapToGrid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustedComponents = layout.components.map((comp) => {
|
||||||
|
const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings);
|
||||||
|
const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings);
|
||||||
|
|
||||||
|
// gridColumns가 없거나 범위를 벗어나면 자동 조정
|
||||||
|
let adjustedGridColumns = comp.gridColumns;
|
||||||
|
if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > newGridSettings.columns) {
|
||||||
|
adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...comp,
|
||||||
|
position: snappedPosition,
|
||||||
|
size: snappedSize,
|
||||||
|
gridColumns: adjustedGridColumns, // gridColumns 속성 추가/조정
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
newLayout.components = adjustedComponents;
|
||||||
|
// console.log("격자 설정 변경으로 컴포넌트 위치 및 크기 자동 조정:", adjustedComponents.length, "개");
|
||||||
|
// console.log("새로운 격자 정보:", newGridInfo);
|
||||||
|
}
|
||||||
|
|
||||||
setLayout(newLayout);
|
setLayout(newLayout);
|
||||||
saveToHistory(newLayout);
|
saveToHistory(newLayout);
|
||||||
},
|
},
|
||||||
|
|
@ -1094,13 +1215,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings);
|
const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings);
|
||||||
const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings);
|
const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings);
|
||||||
|
|
||||||
|
// gridColumns 재계산
|
||||||
|
const adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...comp,
|
...comp,
|
||||||
position: snappedPosition,
|
position: snappedPosition,
|
||||||
size: snappedSize,
|
size: snappedSize,
|
||||||
|
gridColumns: adjustedGridColumns,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("🧲 격자 스냅 적용 완료");
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedLayout = {
|
const updatedLayout = {
|
||||||
|
|
@ -1159,10 +1285,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const snappedPosition = snapToGrid(comp.position, currentGridInfo, gridUtilSettings);
|
const snappedPosition = snapToGrid(comp.position, currentGridInfo, gridUtilSettings);
|
||||||
const snappedSize = snapSizeToGrid(comp.size, currentGridInfo, gridUtilSettings);
|
const snappedSize = snapSizeToGrid(comp.size, currentGridInfo, gridUtilSettings);
|
||||||
|
|
||||||
|
// gridColumns가 없거나 범위를 벗어나면 자동 조정
|
||||||
|
let adjustedGridColumns = comp.gridColumns;
|
||||||
|
if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > layout.gridSettings!.columns) {
|
||||||
|
adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, currentGridInfo, gridUtilSettings);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...comp,
|
...comp,
|
||||||
position: snappedPosition,
|
position: snappedPosition,
|
||||||
size: snappedSize,
|
size: snappedSize,
|
||||||
|
gridColumns: adjustedGridColumns,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1321,8 +1454,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
: { x: absoluteX, y: absoluteY, z: 1 };
|
: { x: absoluteX, y: absoluteY, z: 1 };
|
||||||
|
|
||||||
if (templateComp.type === "container") {
|
if (templateComp.type === "container") {
|
||||||
// 🔥 10px 고정 격자: 기본 너비 사용
|
// 그리드 컬럼 기반 크기 계산
|
||||||
const calculatedSize = { width: 400, height: templateComp.size.height };
|
const gridColumns =
|
||||||
|
typeof templateComp.size.width === "number" && templateComp.size.width <= 12 ? templateComp.size.width : 4; // 기본 4컬럼
|
||||||
|
|
||||||
|
const calculatedSize =
|
||||||
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
|
? (() => {
|
||||||
|
const newWidth = calculateWidthFromColumns(
|
||||||
|
gridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: newWidth,
|
||||||
|
height: templateComp.size.height,
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: { width: 400, height: templateComp.size.height }; // 폴백 크기
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: componentId,
|
id: componentId,
|
||||||
|
|
@ -1346,11 +1495,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 데이터 테이블 컴포넌트 생성
|
// 데이터 테이블 컴포넌트 생성
|
||||||
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
||||||
|
|
||||||
// 🔥 10px 고정 격자: 기본 크기 사용
|
// gridColumns에 맞는 크기 계산
|
||||||
const calculatedSize = {
|
const calculatedSize =
|
||||||
width: 800, // 데이터 테이블 기본 너비
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
height: templateComp.size.height,
|
? (() => {
|
||||||
};
|
const newWidth = calculateWidthFromColumns(
|
||||||
|
gridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: newWidth,
|
||||||
|
height: templateComp.size.height, // 높이는 템플릿 값 유지
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: templateComp.size;
|
||||||
|
|
||||||
console.log("📊 데이터 테이블 생성 시 크기 계산:", {
|
console.log("📊 데이터 테이블 생성 시 크기 계산:", {
|
||||||
gridColumns,
|
gridColumns,
|
||||||
|
|
@ -1415,11 +1574,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 파일 첨부 컴포넌트 생성
|
// 파일 첨부 컴포넌트 생성
|
||||||
const gridColumns = 6; // 기본값: 6컬럼
|
const gridColumns = 6; // 기본값: 6컬럼
|
||||||
|
|
||||||
// 🔥 10px 고정 격자
|
const calculatedSize =
|
||||||
const calculatedSize = {
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
width: 400,
|
? (() => {
|
||||||
height: templateComp.size.height,
|
const newWidth = calculateWidthFromColumns(
|
||||||
};
|
gridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: newWidth,
|
||||||
|
height: templateComp.size.height,
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: templateComp.size;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: componentId,
|
id: componentId,
|
||||||
|
|
@ -1457,11 +1625,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 영역 컴포넌트 생성
|
// 영역 컴포넌트 생성
|
||||||
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
||||||
|
|
||||||
// 🔥 10px 고정 격자
|
const calculatedSize =
|
||||||
const calculatedSize = {
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
width: 600, // 영역 기본 너비
|
? (() => {
|
||||||
height: templateComp.size.height,
|
const newWidth = calculateWidthFromColumns(
|
||||||
};
|
gridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: newWidth,
|
||||||
|
height: templateComp.size.height,
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: templateComp.size;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: componentId,
|
id: componentId,
|
||||||
|
|
@ -1583,7 +1760,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const widgetSize =
|
const widgetSize =
|
||||||
currentGridInfo && layout.gridSettings?.snapToGrid
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
? {
|
? {
|
||||||
width: 200,
|
width: calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings),
|
||||||
height: templateComp.size.height,
|
height: templateComp.size.height,
|
||||||
}
|
}
|
||||||
: templateComp.size;
|
: templateComp.size;
|
||||||
|
|
@ -1954,9 +2131,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ 10px 고정 격자: gridColumns 로직 제거
|
// 그리드 시스템이 활성화된 경우 gridColumns에 맞춰 너비 재계산
|
||||||
// 기본 크기만 사용
|
if (layout.gridSettings?.snapToGrid && gridInfo) {
|
||||||
componentSize = component.defaultSize;
|
// gridColumns에 맞는 정확한 너비 계산
|
||||||
|
const calculatedWidth = calculateWidthFromColumns(
|
||||||
|
gridColumns,
|
||||||
|
gridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 컴포넌트별 최소 크기 보장
|
||||||
|
const minWidth = isTableList ? 120 : isCardDisplay ? 400 : component.defaultSize.width;
|
||||||
|
|
||||||
|
componentSize = {
|
||||||
|
...component.defaultSize,
|
||||||
|
width: Math.max(calculatedWidth, minWidth),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
console.log("🎨 최종 컴포넌트 크기:", {
|
console.log("🎨 최종 컴포넌트 크기:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
|
|
@ -2056,12 +2247,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const dragData = e.dataTransfer.getData("application/json");
|
const dragData = e.dataTransfer.getData("application/json");
|
||||||
|
// console.log("🎯 드롭 이벤트:", { dragData });
|
||||||
if (!dragData) {
|
if (!dragData) {
|
||||||
|
// console.log("❌ 드래그 데이터가 없습니다");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(dragData);
|
const parsedData = JSON.parse(dragData);
|
||||||
|
// console.log("📋 파싱된 데이터:", parsedData);
|
||||||
|
|
||||||
// 템플릿 드래그인 경우
|
// 템플릿 드래그인 경우
|
||||||
if (parsedData.type === "template") {
|
if (parsedData.type === "template") {
|
||||||
|
|
@ -2115,8 +2309,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
};
|
};
|
||||||
} else if (type === "column") {
|
} else if (type === "column") {
|
||||||
// console.log("🔄 컬럼 드롭 처리:", { webType: column.widgetType, columnName: column.columnName });
|
// console.log("🔄 컬럼 드롭 처리:", { webType: column.widgetType, columnName: column.columnName });
|
||||||
// 🔥 10px 고정 격자 시스템: 간단한 기본 너비 사용
|
// 현재 해상도에 맞는 격자 정보로 기본 크기 계산
|
||||||
const defaultWidth = 200; // 기본 너비 200px
|
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 defaultWidth =
|
||||||
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
|
? calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings)
|
||||||
|
: 200;
|
||||||
|
|
||||||
|
console.log("🎯 컴포넌트 생성 시 크기 계산:", {
|
||||||
|
screenResolution: `${screenResolution.width}x${screenResolution.height}`,
|
||||||
|
gridSettings: layout.gridSettings,
|
||||||
|
currentGridInfo: currentGridInfo
|
||||||
|
? {
|
||||||
|
columnWidth: currentGridInfo.columnWidth.toFixed(2),
|
||||||
|
totalWidth: currentGridInfo.totalWidth,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
defaultWidth: defaultWidth.toFixed(2),
|
||||||
|
snapToGrid: layout.gridSettings?.snapToGrid,
|
||||||
|
});
|
||||||
|
|
||||||
// 웹타입별 기본 그리드 컬럼 수 계산
|
// 웹타입별 기본 그리드 컬럼 수 계산
|
||||||
const getDefaultGridColumns = (widgetType: string): number => {
|
const getDefaultGridColumns = (widgetType: string): number => {
|
||||||
|
|
@ -2155,6 +2375,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultColumns = widthMap[widgetType] || 3; // 기본값 3 (1/4, 25%)
|
const defaultColumns = widthMap[widgetType] || 3; // 기본값 3 (1/4, 25%)
|
||||||
|
console.log("🎯 [ScreenDesigner] getDefaultGridColumns:", { widgetType, defaultColumns });
|
||||||
return defaultColumns;
|
return defaultColumns;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2167,7 +2388,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
file: 240, // 파일 업로드 (40 * 6)
|
file: 240, // 파일 업로드 (40 * 6)
|
||||||
};
|
};
|
||||||
|
|
||||||
return heightMap[widgetType] || 30; // 기본값 30px로 변경
|
return heightMap[widgetType] || 40; // 기본값 40
|
||||||
};
|
};
|
||||||
|
|
||||||
// 웹타입별 기본 설정 생성
|
// 웹타입별 기본 설정 생성
|
||||||
|
|
@ -2326,9 +2547,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 웹타입별 적절한 gridColumns 계산
|
// 웹타입별 적절한 gridColumns 계산
|
||||||
const calculatedGridColumns = getDefaultGridColumns(column.widgetType);
|
const calculatedGridColumns = getDefaultGridColumns(column.widgetType);
|
||||||
|
|
||||||
// 🔥 10px 고정 격자: 간단한 너비 계산
|
// gridColumns에 맞는 실제 너비 계산
|
||||||
const componentWidth = defaultWidth;
|
const componentWidth =
|
||||||
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
|
? calculateWidthFromColumns(
|
||||||
|
calculatedGridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
)
|
||||||
|
: defaultWidth;
|
||||||
|
|
||||||
|
console.log("🎯 폼 컨테이너 컴포넌트 생성:", {
|
||||||
|
widgetType: column.widgetType,
|
||||||
|
calculatedGridColumns,
|
||||||
|
componentWidth,
|
||||||
|
defaultWidth,
|
||||||
|
});
|
||||||
|
|
||||||
newComponent = {
|
newComponent = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
|
|
@ -2349,7 +2583,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
codeCategory: column.codeCategory,
|
codeCategory: column.codeCategory,
|
||||||
}),
|
}),
|
||||||
style: {
|
style: {
|
||||||
labelDisplay: false, // 라벨 숨김 (placeholder 사용)
|
labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정
|
||||||
labelFontSize: "12px",
|
labelFontSize: "12px",
|
||||||
labelColor: "#212121",
|
labelColor: "#212121",
|
||||||
labelFontWeight: "500",
|
labelFontWeight: "500",
|
||||||
|
|
@ -2361,7 +2595,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
webType: column.widgetType, // 원본 웹타입 보존
|
webType: column.widgetType, // 원본 웹타입 보존
|
||||||
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
||||||
...getDefaultWebTypeConfig(column.widgetType),
|
...getDefaultWebTypeConfig(column.widgetType),
|
||||||
placeholder: column.columnLabel || column.columnName, // placeholder에 컬럼 라벨명 표시
|
|
||||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||||
...(column.widgetType === "code" &&
|
...(column.widgetType === "code" &&
|
||||||
column.codeCategory && {
|
column.codeCategory && {
|
||||||
|
|
@ -2380,9 +2613,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 웹타입별 적절한 gridColumns 계산
|
// 웹타입별 적절한 gridColumns 계산
|
||||||
const calculatedGridColumns = getDefaultGridColumns(column.widgetType);
|
const calculatedGridColumns = getDefaultGridColumns(column.widgetType);
|
||||||
|
|
||||||
// 🔥 10px 고정 격자: 간단한 너비 계산
|
// gridColumns에 맞는 실제 너비 계산
|
||||||
const componentWidth = defaultWidth;
|
const componentWidth =
|
||||||
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
||||||
|
? calculateWidthFromColumns(
|
||||||
|
calculatedGridColumns,
|
||||||
|
currentGridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
)
|
||||||
|
: defaultWidth;
|
||||||
|
|
||||||
|
console.log("🎯 캔버스 컴포넌트 생성:", {
|
||||||
|
widgetType: column.widgetType,
|
||||||
|
calculatedGridColumns,
|
||||||
|
componentWidth,
|
||||||
|
defaultWidth,
|
||||||
|
});
|
||||||
|
|
||||||
// 🔍 이미지 타입 드래그앤드롭 디버깅
|
// 🔍 이미지 타입 드래그앤드롭 디버깅
|
||||||
// if (column.widgetType === "image") {
|
// if (column.widgetType === "image") {
|
||||||
|
|
@ -2412,7 +2658,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
codeCategory: column.codeCategory,
|
codeCategory: column.codeCategory,
|
||||||
}),
|
}),
|
||||||
style: {
|
style: {
|
||||||
labelDisplay: false, // 라벨 숨김 (placeholder 사용)
|
labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시
|
||||||
labelFontSize: "14px",
|
labelFontSize: "14px",
|
||||||
labelColor: "#000000", // 순수한 검정
|
labelColor: "#000000", // 순수한 검정
|
||||||
labelFontWeight: "500",
|
labelFontWeight: "500",
|
||||||
|
|
@ -2424,7 +2670,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
webType: column.widgetType, // 원본 웹타입 보존
|
webType: column.widgetType, // 원본 웹타입 보존
|
||||||
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
||||||
...getDefaultWebTypeConfig(column.widgetType),
|
...getDefaultWebTypeConfig(column.widgetType),
|
||||||
placeholder: column.columnLabel || column.columnName, // placeholder에 컬럼 라벨명 표시
|
|
||||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||||
...(column.widgetType === "code" &&
|
...(column.widgetType === "code" &&
|
||||||
column.codeCategory && {
|
column.codeCategory && {
|
||||||
|
|
@ -2456,6 +2701,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
newComponent.position = snapToGrid(newComponent.position, currentGridInfo, gridUtilSettings);
|
newComponent.position = snapToGrid(newComponent.position, currentGridInfo, gridUtilSettings);
|
||||||
newComponent.size = snapSizeToGrid(newComponent.size, currentGridInfo, gridUtilSettings);
|
newComponent.size = snapSizeToGrid(newComponent.size, currentGridInfo, gridUtilSettings);
|
||||||
|
|
||||||
|
console.log("🧲 새 컴포넌트 격자 스냅 적용:", {
|
||||||
|
type: newComponent.type,
|
||||||
|
resolution: `${screenResolution.width}x${screenResolution.height}`,
|
||||||
|
snappedPosition: newComponent.position,
|
||||||
|
snappedSize: newComponent.size,
|
||||||
|
columnWidth: currentGridInfo.columnWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newComponent.type === "group") {
|
||||||
|
console.log("🔓 그룹 컴포넌트는 격자 스냅 제외:", {
|
||||||
|
type: newComponent.type,
|
||||||
|
position: newComponent.position,
|
||||||
|
size: newComponent.size,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLayout = {
|
const newLayout = {
|
||||||
|
|
@ -2629,11 +2889,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
componentsToMove = [...componentsToMove, ...additionalComponents];
|
componentsToMove = [...componentsToMove, ...additionalComponents];
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalGrabOffset = {
|
// console.log("드래그 시작:", component.id, "이동할 컴포넌트 수:", componentsToMove.length);
|
||||||
x: relativeMouseX - component.position.x,
|
console.log("마우스 위치 (줌 보정):", {
|
||||||
y: relativeMouseY - component.position.y,
|
zoomLevel,
|
||||||
};
|
clientX: event.clientX,
|
||||||
|
clientY: event.clientY,
|
||||||
|
rectLeft: rect.left,
|
||||||
|
rectTop: rect.top,
|
||||||
|
mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
||||||
|
mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY },
|
||||||
|
componentX: component.position.x,
|
||||||
|
componentY: component.position.y,
|
||||||
|
grabOffsetX: relativeMouseX - component.position.x,
|
||||||
|
grabOffsetY: relativeMouseY - component.position.y,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🚀 드래그 시작:", {
|
||||||
|
componentId: component.id,
|
||||||
|
componentType: component.type,
|
||||||
|
initialPosition: { x: component.position.x, y: component.position.y },
|
||||||
|
});
|
||||||
|
|
||||||
setDragState({
|
setDragState({
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
draggedComponent: component, // 주 드래그 컴포넌트 (마우스 위치 기준)
|
draggedComponent: component, // 주 드래그 컴포넌트 (마우스 위치 기준)
|
||||||
|
|
@ -2648,7 +2924,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
y: component.position.y,
|
y: component.position.y,
|
||||||
z: (component.position as Position).z || 1,
|
z: (component.position as Position).z || 1,
|
||||||
},
|
},
|
||||||
grabOffset: finalGrabOffset,
|
grabOffset: {
|
||||||
|
x: relativeMouseX - component.position.x,
|
||||||
|
y: relativeMouseY - component.position.y,
|
||||||
|
},
|
||||||
justFinishedDrag: false,
|
justFinishedDrag: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -2676,24 +2955,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const rawX = relativeMouseX - dragState.grabOffset.x;
|
const rawX = relativeMouseX - dragState.grabOffset.x;
|
||||||
const rawY = relativeMouseY - dragState.grabOffset.y;
|
const rawY = relativeMouseY - dragState.grabOffset.y;
|
||||||
|
|
||||||
// 🔥 경계 제한 로직 제거: 컴포넌트가 화면을 벗어나도 되게 함
|
|
||||||
// 이유:
|
|
||||||
// 1. 큰 컴포넌트(884px)를 작은 영역(16px)에만 제한하는 것은 사용성 문제
|
|
||||||
// 2. 사용자가 자유롭게 배치할 수 있어야 함
|
|
||||||
// 3. 최소 위치만 0 이상으로 제한 (음수 좌표 방지)
|
|
||||||
|
|
||||||
const newPosition = {
|
const newPosition = {
|
||||||
x: Math.max(0, rawX),
|
x: Math.max(0, Math.min(rawX, screenResolution.width - componentWidth)),
|
||||||
y: Math.max(0, rawY),
|
y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)),
|
||||||
z: (dragState.draggedComponent.position as Position).z || 1,
|
z: (dragState.draggedComponent.position as Position).z || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 드래그 상태 업데이트
|
// 드래그 상태 업데이트
|
||||||
|
console.log("🔥 ScreenDesigner updateDragPosition (줌 보정):", {
|
||||||
|
zoomLevel,
|
||||||
|
draggedComponentId: dragState.draggedComponent.id,
|
||||||
|
mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
||||||
|
mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY },
|
||||||
|
oldPosition: dragState.currentPosition,
|
||||||
|
newPosition: newPosition,
|
||||||
|
});
|
||||||
|
|
||||||
setDragState((prev) => {
|
setDragState((prev) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
...prev,
|
...prev,
|
||||||
currentPosition: { ...newPosition }, // 새로운 객체 생성
|
currentPosition: { ...newPosition }, // 새로운 객체 생성
|
||||||
};
|
};
|
||||||
|
console.log("🔄 ScreenDesigner dragState 업데이트:", {
|
||||||
|
prevPosition: prev.currentPosition,
|
||||||
|
newPosition: newState.currentPosition,
|
||||||
|
stateChanged:
|
||||||
|
prev.currentPosition.x !== newState.currentPosition.x ||
|
||||||
|
prev.currentPosition.y !== newState.currentPosition.y,
|
||||||
|
});
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2711,8 +3000,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent);
|
const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent);
|
||||||
let finalPosition = dragState.currentPosition;
|
let finalPosition = dragState.currentPosition;
|
||||||
|
|
||||||
// 🔥 10px 고정 격자 시스템: calculateGridInfo는 columns, gap, padding을 무시함
|
// 현재 해상도에 맞는 격자 정보 계산
|
||||||
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, layout.gridSettings);
|
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;
|
||||||
|
|
||||||
// 일반 컴포넌트 및 플로우 버튼 그룹에 격자 스냅 적용 (일반 그룹 제외)
|
// 일반 컴포넌트 및 플로우 버튼 그룹에 격자 스냅 적용 (일반 그룹 제외)
|
||||||
if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) {
|
if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) {
|
||||||
|
|
@ -2723,9 +3019,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
z: dragState.currentPosition.z ?? 1,
|
z: dragState.currentPosition.z ?? 1,
|
||||||
},
|
},
|
||||||
currentGridInfo,
|
currentGridInfo,
|
||||||
layout.gridSettings,
|
{
|
||||||
|
columns: layout.gridSettings.columns,
|
||||||
|
gap: layout.gridSettings.gap,
|
||||||
|
padding: layout.gridSettings.padding,
|
||||||
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("🎯 격자 스냅 적용됨:", {
|
||||||
|
componentType: draggedComponent?.type,
|
||||||
|
resolution: `${screenResolution.width}x${screenResolution.height}`,
|
||||||
|
originalPosition: dragState.currentPosition,
|
||||||
|
snappedPosition: finalPosition,
|
||||||
|
columnWidth: currentGridInfo.columnWidth,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스냅으로 인한 추가 이동 거리 계산
|
// 스냅으로 인한 추가 이동 거리 계산
|
||||||
|
|
@ -2790,6 +3098,28 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
height: snappedHeight,
|
height: snappedHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("🎯 드래그 종료 시 그룹 내부 컴포넌트 격자 스냅 (패딩 고려):", {
|
||||||
|
componentId: comp.id,
|
||||||
|
parentId: comp.parentId,
|
||||||
|
beforeSnap: {
|
||||||
|
x: originalComponent.position.x + totalDeltaX,
|
||||||
|
y: originalComponent.position.y + totalDeltaY,
|
||||||
|
},
|
||||||
|
calculation: {
|
||||||
|
effectiveX,
|
||||||
|
effectiveY,
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
columnWidth,
|
||||||
|
fullColumnWidth,
|
||||||
|
widthInColumns,
|
||||||
|
gap: gap || 16,
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
afterSnap: newPosition,
|
||||||
|
afterSizeSnap: newSize,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...comp,
|
...comp,
|
||||||
position: newPosition as Position,
|
position: newPosition as Position,
|
||||||
|
|
@ -2812,6 +3142,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) {
|
if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) {
|
||||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id);
|
const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id);
|
||||||
if (updatedSelectedComponent) {
|
if (updatedSelectedComponent) {
|
||||||
|
console.log("🔄 ScreenDesigner: 선택된 컴포넌트 위치 업데이트", {
|
||||||
|
componentId: selectedComponent.id,
|
||||||
|
oldPosition: selectedComponent.position,
|
||||||
|
newPosition: updatedSelectedComponent.position,
|
||||||
|
});
|
||||||
setSelectedComponent(updatedSelectedComponent);
|
setSelectedComponent(updatedSelectedComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,335 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Grid3X3 } from "lucide-react";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { GridSettings } from "@/types/screen-management";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Grid3X3, RotateCcw, Eye, EyeOff, Zap, RefreshCw } from "lucide-react";
|
||||||
|
import { GridSettings, ScreenResolution } from "@/types/screen";
|
||||||
|
import { calculateGridInfo } from "@/lib/utils/gridUtils";
|
||||||
|
|
||||||
interface GridPanelProps {
|
interface GridPanelProps {
|
||||||
gridSettings: GridSettings;
|
gridSettings: GridSettings;
|
||||||
onGridSettingsChange: (settings: GridSettings) => void;
|
onGridSettingsChange: (settings: GridSettings) => void;
|
||||||
|
onResetGrid: () => void;
|
||||||
|
onForceGridUpdate?: () => void; // 강제 격자 재조정 추가
|
||||||
|
screenResolution?: ScreenResolution; // 해상도 정보 추가
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
* 격자 설정 패널 (10px 고정 격자)
|
gridSettings,
|
||||||
*
|
onGridSettingsChange,
|
||||||
* 사용자 설정:
|
onResetGrid,
|
||||||
* - 격자 표시 ON/OFF
|
onForceGridUpdate,
|
||||||
* - 격자 스냅 ON/OFF
|
screenResolution,
|
||||||
*
|
}) => {
|
||||||
* 자동 설정 (변경 불가):
|
const updateSetting = (key: keyof GridSettings, value: any) => {
|
||||||
* - 격자 크기: 10px 고정
|
|
||||||
* - 격자 간격: 10px 고정
|
|
||||||
*/
|
|
||||||
export function GridPanel({ gridSettings, onGridSettingsChange }: GridPanelProps) {
|
|
||||||
const updateSetting = <K extends keyof GridSettings>(key: K, value: GridSettings[K]) => {
|
|
||||||
onGridSettingsChange({
|
onGridSettingsChange({
|
||||||
...gridSettings,
|
...gridSettings,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 최대 컬럼 수 계산 (최소 컬럼 너비 30px 기준)
|
||||||
|
const MIN_COLUMN_WIDTH = 30;
|
||||||
|
const maxColumns = screenResolution
|
||||||
|
? Math.floor((screenResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
|
||||||
|
: 24;
|
||||||
|
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
|
||||||
|
|
||||||
|
// 실제 격자 정보 계산
|
||||||
|
const actualGridInfo = screenResolution
|
||||||
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
|
columns: gridSettings.columns,
|
||||||
|
gap: gridSettings.gap,
|
||||||
|
padding: gridSettings.padding,
|
||||||
|
snapToGrid: gridSettings.snapToGrid || false,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 실제 표시되는 컬럼 수 계산 (항상 설정된 개수를 표시하되, 너비가 너무 작으면 경고)
|
||||||
|
const actualColumns = gridSettings.columns;
|
||||||
|
|
||||||
|
// 컬럼이 너무 작은지 확인
|
||||||
|
const isColumnsTooSmall =
|
||||||
|
screenResolution && actualGridInfo
|
||||||
|
? actualGridInfo.columnWidth < MIN_COLUMN_WIDTH
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<div className="flex h-full flex-col">
|
||||||
<CardHeader className="space-y-1 pb-3">
|
{/* 헤더 */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="border-b p-4">
|
||||||
<Grid3X3 className="h-4 w-4" />
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<CardTitle className="text-base">격자 설정</CardTitle>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Grid3X3 className="text-muted-foreground h-4 w-4" />
|
||||||
</CardHeader>
|
<h3 className="text-sm font-semibold">격자 설정</h3>
|
||||||
<CardContent className="space-y-4">
|
</div>
|
||||||
{/* 격자 표시 */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-1.5">
|
||||||
<Label htmlFor="showGrid" className="flex items-center gap-2 text-sm font-medium">
|
{onForceGridUpdate && (
|
||||||
<Grid3X3 className="h-4 w-4 text-muted-foreground" />
|
<Button
|
||||||
격자 표시
|
size="sm"
|
||||||
</Label>
|
variant="outline"
|
||||||
<Checkbox
|
onClick={onForceGridUpdate}
|
||||||
id="showGrid"
|
className="h-7 px-2 text-xs" style={{ fontSize: "12px" }}
|
||||||
checked={gridSettings.showGrid ?? false}
|
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
||||||
onCheckedChange={(checked) => updateSetting("showGrid", checked as boolean)}
|
>
|
||||||
/>
|
<RefreshCw className="mr-1 h-3 w-3" />
|
||||||
|
재정렬
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button size="sm" variant="outline" onClick={onResetGrid} className="h-7 px-2 text-xs">
|
||||||
|
<RotateCcw className="mr-1 h-3 w-3" />
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 격자 스냅 */}
|
{/* 주요 토글들 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-2.5">
|
||||||
<Label htmlFor="snapToGrid" className="flex items-center gap-2 text-sm font-medium">
|
<div className="flex items-center justify-between">
|
||||||
<Grid3X3 className="h-4 w-4 text-muted-foreground" />
|
<div className="flex items-center gap-2">
|
||||||
격자 스냅
|
{gridSettings.showGrid ? (
|
||||||
</Label>
|
<Eye className="text-primary h-3.5 w-3.5" />
|
||||||
<Checkbox
|
) : (
|
||||||
id="snapToGrid"
|
<EyeOff className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
checked={gridSettings.snapToGrid}
|
)}
|
||||||
onCheckedChange={(checked) => updateSetting("snapToGrid", checked as boolean)}
|
<Label htmlFor="showGrid" className="text-xs font-medium">
|
||||||
/>
|
격자 표시
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
id="showGrid"
|
||||||
|
checked={gridSettings.showGrid}
|
||||||
|
onCheckedChange={(checked) => updateSetting("showGrid", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap className="text-primary h-3.5 w-3.5" />
|
||||||
|
<Label htmlFor="snapToGrid" className="text-xs font-medium">
|
||||||
|
격자 스냅
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
id="snapToGrid"
|
||||||
|
checked={gridSettings.snapToGrid}
|
||||||
|
onCheckedChange={(checked) => updateSetting("snapToGrid", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 설정 영역 */}
|
||||||
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||||
|
{/* 격자 구조 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-xs font-semibold">격자 구조</h4>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="columns" className="text-xs font-medium">
|
||||||
|
컬럼 수
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="columns"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={safeMaxColumns}
|
||||||
|
value={gridSettings.columns}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) {
|
||||||
|
updateSetting("columns", value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs">/ {safeMaxColumns}</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
id="columns-slider"
|
||||||
|
min={1}
|
||||||
|
max={safeMaxColumns}
|
||||||
|
step={1}
|
||||||
|
value={[gridSettings.columns]}
|
||||||
|
onValueChange={([value]) => updateSetting("columns", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
|
<span>1열</span>
|
||||||
|
<span>{safeMaxColumns}열</span>
|
||||||
|
</div>
|
||||||
|
{isColumnsTooSmall && (
|
||||||
|
<p className="text-xs text-amber-600">
|
||||||
|
⚠️ 컬럼 너비가 너무 작습니다 (최소 {MIN_COLUMN_WIDTH}px 권장)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="gap" className="text-xs font-medium">
|
||||||
|
간격: <span className="text-primary">{gridSettings.gap}px</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id="gap"
|
||||||
|
min={0}
|
||||||
|
max={40}
|
||||||
|
step={2}
|
||||||
|
value={[gridSettings.gap]}
|
||||||
|
onValueChange={([value]) => updateSetting("gap", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
|
<span>0px</span>
|
||||||
|
<span>40px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="padding" className="text-xs font-medium">
|
||||||
|
여백: <span className="text-primary">{gridSettings.padding}px</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id="padding"
|
||||||
|
min={0}
|
||||||
|
max={60}
|
||||||
|
step={4}
|
||||||
|
value={[gridSettings.padding]}
|
||||||
|
onValueChange={([value]) => updateSetting("padding", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
|
<span>0px</span>
|
||||||
|
<span>60px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 격자 정보 (읽기 전용) */}
|
<Separator />
|
||||||
<div className="mt-4 rounded-md bg-muted p-3 text-xs text-muted-foreground">
|
|
||||||
<p className="font-medium mb-1">🔧 격자 시스템</p>
|
{/* 격자 스타일 */}
|
||||||
<ul className="space-y-0.5">
|
<div className="space-y-4">
|
||||||
<li>• 격자 크기: <span className="font-semibold">10px 고정</span></li>
|
<h4 className="font-medium text-gray-900">격자 스타일</h4>
|
||||||
<li>• 컴포넌트는 10px 단위로 배치됩니다</li>
|
|
||||||
<li>• 격자 스냅을 끄면 자유롭게 배치 가능</li>
|
<div>
|
||||||
</ul>
|
<Label htmlFor="gridColor" className="text-sm font-medium">
|
||||||
|
격자 색상
|
||||||
|
</Label>
|
||||||
|
<div className="mt-1 flex items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
id="gridColor"
|
||||||
|
type="color"
|
||||||
|
value={gridSettings.gridColor || "#d1d5db"}
|
||||||
|
onChange={(e) => updateSetting("gridColor", e.target.value)}
|
||||||
|
className="h-8 w-12 rounded border p-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={gridSettings.gridColor || "#d1d5db"}
|
||||||
|
onChange={(e) => updateSetting("gridColor", e.target.value)}
|
||||||
|
placeholder="#d1d5db"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="gridOpacity" className="mb-2 block text-sm font-medium">
|
||||||
|
격자 투명도: {Math.round((gridSettings.gridOpacity || 0.5) * 100)}%
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id="gridOpacity"
|
||||||
|
min={0.1}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
value={[gridSettings.gridOpacity || 0.5]}
|
||||||
|
onValueChange={([value]) => updateSetting("gridOpacity", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
||||||
|
<span>10%</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<Separator />
|
||||||
|
|
||||||
|
{/* 미리보기 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-medium text-gray-900">미리보기</h4>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="rounded-md border border-gray-200 bg-white p-4"
|
||||||
|
style={{
|
||||||
|
backgroundImage: gridSettings.showGrid
|
||||||
|
? `linear-gradient(to right, ${gridSettings.gridColor || "#d1d5db"} 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, ${gridSettings.gridColor || "#d1d5db"} 1px, transparent 1px)`
|
||||||
|
: "none",
|
||||||
|
backgroundSize: gridSettings.showGrid ? `${100 / gridSettings.columns}% 20px` : "none",
|
||||||
|
opacity: gridSettings.gridOpacity || 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="bg-primary/20 flex h-16 items-center justify-center rounded border-2 border-dashed border-blue-300">
|
||||||
|
<span className="text-primary text-xs">컴포넌트 예시</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||||
|
<div className="text-muted-foreground text-xs">💡 격자 설정은 실시간으로 캔버스에 반영됩니다 </div>
|
||||||
|
|
||||||
|
{/* 해상도 및 격자 정보 */}
|
||||||
|
{screenResolution && actualGridInfo && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-medium text-gray-900">격자 정보</h4>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">해상도:</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{screenResolution.width} × {screenResolution.height}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">컬럼 너비:</span>
|
||||||
|
<span className={`font-mono ${isColumnsTooSmall ? "text-destructive" : "text-gray-900"}`}>
|
||||||
|
{actualGridInfo.columnWidth.toFixed(1)}px
|
||||||
|
{isColumnsTooSmall && " (너무 작음)"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">사용 가능 너비:</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{(screenResolution.width - gridSettings.padding * 2).toLocaleString()}px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isColumnsTooSmall && (
|
||||||
|
<div className="rounded-md bg-yellow-50 p-2 text-xs text-yellow-800">
|
||||||
|
💡 컬럼이 너무 작습니다. 컬럼 수를 줄이거나 간격을 줄여보세요.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default GridPanel;
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,12 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
onDragStart,
|
onDragStart,
|
||||||
placedColumns = new Set(),
|
placedColumns = new Set(),
|
||||||
}) => {
|
}) => {
|
||||||
// 숨길 기본 컬럼 목록 (id, created_date, updated_date, writer, company_code)
|
// 이미 배치된 컬럼을 제외한 테이블 정보 생성
|
||||||
const hiddenColumns = new Set(['id', 'created_date', 'updated_date', 'writer', 'company_code']);
|
|
||||||
|
|
||||||
// 이미 배치된 컬럼 + 기본 컬럼을 제외한 테이블 정보 생성
|
|
||||||
const tablesWithAvailableColumns = tables.map((table) => ({
|
const tablesWithAvailableColumns = tables.map((table) => ({
|
||||||
...table,
|
...table,
|
||||||
columns: table.columns.filter((col) => {
|
columns: table.columns.filter((col) => {
|
||||||
const columnKey = `${table.tableName}.${col.columnName}`;
|
const columnKey = `${table.tableName}.${col.columnName}`;
|
||||||
// 기본 컬럼 또는 이미 배치된 컬럼은 제외
|
return !placedColumns.has(columnKey);
|
||||||
return !hiddenColumns.has(col.columnName) && !placedColumns.has(columnKey);
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,23 +125,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}, [selectedComponent?.size?.height, selectedComponent?.id]);
|
}, [selectedComponent?.size?.height, selectedComponent?.id]);
|
||||||
|
|
||||||
// 🔥 훅은 항상 최상단에 (early return 이전)
|
|
||||||
// 크기 입력 필드용 로컬 상태
|
|
||||||
const [localSize, setLocalSize] = useState({
|
|
||||||
width: selectedComponent?.size?.width || 100,
|
|
||||||
height: selectedComponent?.size?.height || 40,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 선택된 컴포넌트가 변경되면 로컬 상태 동기화
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedComponent) {
|
|
||||||
setLocalSize({
|
|
||||||
width: selectedComponent.size?.width || 100,
|
|
||||||
height: selectedComponent.size?.height || 40,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedComponent?.id, selectedComponent?.size?.width, selectedComponent?.size?.height]);
|
|
||||||
|
|
||||||
// 격자 설정 업데이트 함수 (early return 이전에 정의)
|
// 격자 설정 업데이트 함수 (early return 이전에 정의)
|
||||||
const updateGridSetting = (key: string, value: any) => {
|
const updateGridSetting = (key: string, value: any) => {
|
||||||
if (onGridSettingsChange && gridSettings) {
|
if (onGridSettingsChange && gridSettings) {
|
||||||
|
|
@ -152,10 +135,17 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 격자 설정 렌더링 (10px 고정 격자)
|
// 격자 설정 렌더링 (early return 이전에 정의)
|
||||||
const renderGridSettings = () => {
|
const renderGridSettings = () => {
|
||||||
if (!gridSettings || !onGridSettingsChange) return null;
|
if (!gridSettings || !onGridSettingsChange) return null;
|
||||||
|
|
||||||
|
// 최대 컬럼 수 계산
|
||||||
|
const MIN_COLUMN_WIDTH = 30;
|
||||||
|
const maxColumns = currentResolution
|
||||||
|
? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
|
||||||
|
: 24;
|
||||||
|
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|
@ -164,7 +154,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 격자 표시 */}
|
{/* 토글들 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{gridSettings.showGrid ? (
|
{gridSettings.showGrid ? (
|
||||||
|
|
@ -178,12 +168,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="showGrid"
|
id="showGrid"
|
||||||
checked={gridSettings.showGrid ?? false}
|
checked={gridSettings.showGrid}
|
||||||
onCheckedChange={(checked) => updateGridSetting("showGrid", checked)}
|
onCheckedChange={(checked) => updateGridSetting("showGrid", checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 격자 스냅 */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="text-primary h-3 w-3" />
|
<Zap className="text-primary h-3 w-3" />
|
||||||
|
|
@ -198,14 +187,65 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 격자 정보 (읽기 전용) */}
|
{/* 컬럼 수 */}
|
||||||
<div className="rounded-md bg-muted p-2 text-[10px] text-muted-foreground">
|
<div className="space-y-1">
|
||||||
<p className="font-medium mb-0.5">🔧 격자 시스템</p>
|
<Label htmlFor="columns" className="text-xs font-medium">
|
||||||
<ul className="space-y-0.5">
|
컬럼 수
|
||||||
<li>• 격자 크기: <span className="font-semibold">10px 고정</span></li>
|
</Label>
|
||||||
<li>• 컴포넌트는 10px 단위로 배치됩니다</li>
|
<div className="flex items-center gap-2">
|
||||||
<li>• 격자 스냅을 끄면 자유롭게 배치 가능</li>
|
<Input
|
||||||
</ul>
|
id="columns"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={safeMaxColumns}
|
||||||
|
step="1"
|
||||||
|
value={gridSettings.columns}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) {
|
||||||
|
updateGridSetting("columns", value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
placeholder={`1~${safeMaxColumns}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
최대 {safeMaxColumns}개까지 설정 가능 (최소 컬럼 너비 {MIN_COLUMN_WIDTH}px)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 간격 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="gap" className="text-xs font-medium">
|
||||||
|
간격: <span className="text-primary">{gridSettings.gap}px</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id="gap"
|
||||||
|
min={0}
|
||||||
|
max={40}
|
||||||
|
step={2}
|
||||||
|
value={[gridSettings.gap]}
|
||||||
|
onValueChange={([value]) => updateGridSetting("gap", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 여백 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="padding" className="text-xs font-medium">
|
||||||
|
여백: <span className="text-primary">{gridSettings.padding}px</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id="padding"
|
||||||
|
min={0}
|
||||||
|
max={60}
|
||||||
|
step={4}
|
||||||
|
value={[gridSettings.padding]}
|
||||||
|
onValueChange={([value]) => updateGridSetting("padding", value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -415,90 +455,47 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Z-Index */}
|
{/* Grid Columns + Z-Index (같은 행) */}
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Z-Index</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
value={currentPosition.z || 1}
|
|
||||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 크기 (너비/높이) */}
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{(selectedComponent as any).gridColumns !== undefined && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">차지 컬럼 수</Label>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={gridSettings?.columns || 12}
|
||||||
|
step="1"
|
||||||
|
value={(selectedComponent as any).gridColumns || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
const maxColumns = gridSettings?.columns || 12;
|
||||||
|
if (!isNaN(value) && value >= 1 && value <= maxColumns) {
|
||||||
|
handleUpdate("gridColumns", value);
|
||||||
|
|
||||||
|
// width를 퍼센트로 계산하여 업데이트
|
||||||
|
const widthPercent = (value / maxColumns) * 100;
|
||||||
|
handleUpdate("style.width", `${widthPercent}%`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-[10px] whitespace-nowrap">
|
||||||
|
/{gridSettings?.columns || 12}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">너비 (px)</Label>
|
<Label className="text-xs">Z-Index</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
step="1"
|
||||||
value={localSize.width}
|
value={currentPosition.z || 1}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||||
// 입력 중에는 로컬 상태만 업데이트
|
|
||||||
const value = e.target.value === "" ? "" : parseInt(e.target.value);
|
|
||||||
setLocalSize((prev) => ({ ...prev, width: value as number }));
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// 포커스 아웃 시 실제 컴포넌트 업데이트
|
|
||||||
const rawValue = e.target.value;
|
|
||||||
const parsedValue = parseInt(rawValue);
|
|
||||||
const newWidth = Math.max(10, parsedValue || 10);
|
|
||||||
|
|
||||||
// 로컬 상태도 최종값으로 업데이트
|
|
||||||
setLocalSize((prev) => ({ ...prev, width: newWidth }));
|
|
||||||
|
|
||||||
// size.width 경로로 업데이트 (격자 스냅 적용됨)
|
|
||||||
handleUpdate("size.width", newWidth);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Enter 키로도 즉시 적용
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
const newWidth = Math.max(10, parseInt((e.target as HTMLInputElement).value) || 10);
|
|
||||||
setLocalSize((prev) => ({ ...prev, width: newWidth }));
|
|
||||||
handleUpdate("size.width", newWidth);
|
|
||||||
(e.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">높이 (px)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
value={localSize.height}
|
|
||||||
onChange={(e) => {
|
|
||||||
// 입력 중에는 로컬 상태만 업데이트
|
|
||||||
const value = e.target.value === "" ? "" : parseInt(e.target.value);
|
|
||||||
setLocalSize((prev) => ({ ...prev, height: value as number }));
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// 포커스 아웃 시 실제 컴포넌트 업데이트
|
|
||||||
const rawValue = e.target.value;
|
|
||||||
const parsedValue = parseInt(rawValue);
|
|
||||||
const newHeight = Math.max(10, parsedValue || 10);
|
|
||||||
|
|
||||||
// 로컬 상태도 최종값으로 업데이트
|
|
||||||
setLocalSize((prev) => ({ ...prev, height: newHeight }));
|
|
||||||
|
|
||||||
// size.height 경로로 업데이트 (격자 스냅 적용됨)
|
|
||||||
handleUpdate("size.height", newHeight);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Enter 키로도 즉시 적용
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
const newHeight = Math.max(10, parseInt((e.target as HTMLInputElement).value) || 10);
|
|
||||||
setLocalSize((prev) => ({ ...prev, height: newHeight }));
|
|
||||||
handleUpdate("size.height", newHeight);
|
|
||||||
(e.target as HTMLInputElement).blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,205 @@
|
||||||
import { Position, Size } from "@/types/screen";
|
import { Position, Size } from "@/types/screen";
|
||||||
import { GridSettings } from "@/types/screen-management";
|
import { GridSettings } from "@/types/screen-management";
|
||||||
|
|
||||||
// 🎯 10px 고정 격자 시스템
|
|
||||||
const GRID_SIZE = 10; // 고정값
|
|
||||||
|
|
||||||
export interface GridInfo {
|
export interface GridInfo {
|
||||||
gridSize: number; // 항상 10px
|
columnWidth: number;
|
||||||
totalWidth: number;
|
totalWidth: number;
|
||||||
totalHeight: number;
|
totalHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 격자 정보 계산 (단순화)
|
* 격자 정보 계산
|
||||||
*/
|
*/
|
||||||
export function calculateGridInfo(
|
export function calculateGridInfo(
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
containerHeight: number,
|
containerHeight: number,
|
||||||
_gridSettings?: GridSettings, // 호환성 유지용 (사용 안 함)
|
gridSettings: GridSettings,
|
||||||
): GridInfo {
|
): GridInfo {
|
||||||
|
const { gap, padding } = gridSettings;
|
||||||
|
let { columns } = gridSettings;
|
||||||
|
|
||||||
|
// 🔥 최소 컬럼 너비를 보장하기 위한 최대 컬럼 수 계산
|
||||||
|
const MIN_COLUMN_WIDTH = 30; // 최소 컬럼 너비 30px
|
||||||
|
const availableWidth = containerWidth - padding * 2;
|
||||||
|
const maxPossibleColumns = Math.floor((availableWidth + gap) / (MIN_COLUMN_WIDTH + gap));
|
||||||
|
|
||||||
|
// 설정된 컬럼 수가 너무 많으면 자동으로 제한
|
||||||
|
if (columns > maxPossibleColumns) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ 격자 컬럼 수가 너무 많습니다. ${columns}개 → ${maxPossibleColumns}개로 자동 조정됨 (최소 컬럼 너비: ${MIN_COLUMN_WIDTH}px)`,
|
||||||
|
);
|
||||||
|
columns = Math.max(1, maxPossibleColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 격자 간격을 고려한 컬럼 너비 계산
|
||||||
|
const totalGaps = (columns - 1) * gap;
|
||||||
|
const columnWidth = (availableWidth - totalGaps) / columns;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gridSize: GRID_SIZE,
|
columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH),
|
||||||
totalWidth: containerWidth,
|
totalWidth: containerWidth,
|
||||||
totalHeight: containerHeight,
|
totalHeight: containerHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 위치를 10px 격자에 맞춤
|
* 위치를 격자에 맞춤
|
||||||
*/
|
*/
|
||||||
export function snapToGrid(position: Position, _gridInfo: GridInfo, gridSettings: GridSettings): Position {
|
export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position {
|
||||||
if (!gridSettings.snapToGrid) {
|
if (!gridSettings.snapToGrid) {
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
const { gap, padding } = gridSettings;
|
||||||
|
|
||||||
|
// 격자 셀 크기 (컬럼 너비 + 간격을 하나의 격자 단위로 계산)
|
||||||
|
const cellWidth = columnWidth + gap;
|
||||||
|
const cellHeight = 10; // 행 높이 10px 단위로 고정
|
||||||
|
|
||||||
|
// 패딩을 제외한 상대 위치
|
||||||
|
const relativeX = position.x - padding;
|
||||||
|
const relativeY = position.y - padding;
|
||||||
|
|
||||||
|
// 격자 기준으로 위치 계산 (가장 가까운 격자점으로 스냅)
|
||||||
|
const gridX = Math.round(relativeX / cellWidth);
|
||||||
|
const gridY = Math.round(relativeY / cellHeight);
|
||||||
|
|
||||||
|
// 실제 픽셀 위치로 변환
|
||||||
|
const snappedX = Math.max(padding, padding + gridX * cellWidth);
|
||||||
|
const snappedY = Math.max(padding, padding + gridY * cellHeight);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: Math.round(position.x / GRID_SIZE) * GRID_SIZE,
|
x: snappedX,
|
||||||
y: Math.round(position.y / GRID_SIZE) * GRID_SIZE,
|
y: snappedY,
|
||||||
z: position.z,
|
z: position.z,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 크기를 10px 격자에 맞춤
|
* 크기를 격자에 맞춤
|
||||||
*/
|
*/
|
||||||
export function snapSizeToGrid(size: Size, _gridInfo: GridInfo, gridSettings: GridSettings): Size {
|
export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size {
|
||||||
if (!gridSettings.snapToGrid) {
|
if (!gridSettings.snapToGrid) {
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
const { gap } = gridSettings;
|
||||||
|
|
||||||
|
// 격자 단위로 너비 계산
|
||||||
|
// 컴포넌트가 차지하는 컬럼 수를 올바르게 계산
|
||||||
|
let gridColumns = 1;
|
||||||
|
|
||||||
|
// 현재 너비에서 가장 가까운 격자 컬럼 수 찾기
|
||||||
|
for (let cols = 1; cols <= gridSettings.columns; cols++) {
|
||||||
|
const targetWidth = cols * columnWidth + (cols - 1) * gap;
|
||||||
|
if (size.width <= targetWidth + (columnWidth + gap) / 2) {
|
||||||
|
gridColumns = cols;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gridColumns = cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap;
|
||||||
|
|
||||||
|
// 높이는 10px 단위로 스냅
|
||||||
|
const rowHeight = 10;
|
||||||
|
const snappedHeight = Math.max(10, Math.round(size.height / rowHeight) * rowHeight);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`📏 크기 스냅: ${size.width}px → ${snappedWidth}px (${gridColumns}컬럼, 컬럼너비:${columnWidth}px, 간격:${gap}px)`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: Math.max(GRID_SIZE, Math.round(size.width / GRID_SIZE) * GRID_SIZE),
|
width: Math.max(columnWidth, snappedWidth),
|
||||||
height: Math.max(GRID_SIZE, Math.round(size.height / GRID_SIZE) * GRID_SIZE),
|
height: snappedHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 격자 가이드라인 생성 (10px 간격)
|
* 격자 컬럼 수로 너비 계산
|
||||||
|
*/
|
||||||
|
export function calculateWidthFromColumns(columns: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
const { gap } = gridSettings;
|
||||||
|
|
||||||
|
return columns * columnWidth + (columns - 1) * gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gridColumns 속성을 기반으로 컴포넌트 크기 업데이트
|
||||||
|
*/
|
||||||
|
export function updateSizeFromGridColumns(
|
||||||
|
component: { gridColumns?: number; size: Size },
|
||||||
|
gridInfo: GridInfo,
|
||||||
|
gridSettings: GridSettings,
|
||||||
|
): Size {
|
||||||
|
if (!component.gridColumns || component.gridColumns < 1) {
|
||||||
|
return component.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = calculateWidthFromColumns(component.gridColumns, gridInfo, gridSettings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: newWidth,
|
||||||
|
height: component.size.height, // 높이는 유지
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트의 gridColumns를 자동으로 크기에 맞게 조정
|
||||||
|
*/
|
||||||
|
export function adjustGridColumnsFromSize(
|
||||||
|
component: { size: Size },
|
||||||
|
gridInfo: GridInfo,
|
||||||
|
gridSettings: GridSettings,
|
||||||
|
): number {
|
||||||
|
const columns = calculateColumnsFromWidth(component.size.width, gridInfo, gridSettings);
|
||||||
|
return Math.min(Math.max(1, columns), gridSettings.columns); // 1-12 범위로 제한
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 너비에서 격자 컬럼 수 계산
|
||||||
|
*/
|
||||||
|
export function calculateColumnsFromWidth(width: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
const { gap } = gridSettings;
|
||||||
|
|
||||||
|
return Math.max(1, Math.round((width + gap) / (columnWidth + gap)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 격자 가이드라인 생성
|
||||||
*/
|
*/
|
||||||
export function generateGridLines(
|
export function generateGridLines(
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
containerHeight: number,
|
containerHeight: number,
|
||||||
_gridSettings?: GridSettings,
|
gridSettings: GridSettings,
|
||||||
): {
|
): {
|
||||||
verticalLines: number[];
|
verticalLines: number[];
|
||||||
horizontalLines: number[];
|
horizontalLines: number[];
|
||||||
} {
|
} {
|
||||||
|
const { columns, gap, padding } = gridSettings;
|
||||||
|
const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings);
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
|
||||||
|
// 격자 셀 크기 (스냅 로직과 동일하게)
|
||||||
|
const cellWidth = columnWidth + gap;
|
||||||
|
const cellHeight = 10; // 행 높이 10px 단위로 고정
|
||||||
|
|
||||||
|
// 세로 격자선
|
||||||
const verticalLines: number[] = [];
|
const verticalLines: number[] = [];
|
||||||
for (let x = 0; x <= containerWidth; x += GRID_SIZE) {
|
for (let i = 0; i <= columns; i++) {
|
||||||
verticalLines.push(x);
|
const x = padding + i * cellWidth;
|
||||||
|
if (x <= containerWidth) {
|
||||||
|
verticalLines.push(x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 가로 격자선
|
||||||
const horizontalLines: number[] = [];
|
const horizontalLines: number[] = [];
|
||||||
for (let y = 0; y <= containerHeight; y += GRID_SIZE) {
|
for (let y = padding; y < containerHeight; y += cellHeight) {
|
||||||
horizontalLines.push(y);
|
horizontalLines.push(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,21 +242,46 @@ export function alignGroupChildrenToGrid(
|
||||||
): any[] {
|
): any[] {
|
||||||
if (!gridSettings.snapToGrid || children.length === 0) return children;
|
if (!gridSettings.snapToGrid || children.length === 0) return children;
|
||||||
|
|
||||||
return children.map((child) => {
|
console.log("🔧 alignGroupChildrenToGrid 시작:", {
|
||||||
const padding = 16;
|
childrenCount: children.length,
|
||||||
|
groupPosition,
|
||||||
// 10px 단위로 스냅
|
gridInfo,
|
||||||
const snappedX = Math.max(padding, Math.round((child.position.x - padding) / GRID_SIZE) * GRID_SIZE + padding);
|
gridSettings,
|
||||||
const snappedY = Math.max(padding, Math.round((child.position.y - padding) / GRID_SIZE) * GRID_SIZE + padding);
|
});
|
||||||
|
|
||||||
const snappedWidth = Math.max(GRID_SIZE, Math.round(child.size.width / GRID_SIZE) * GRID_SIZE);
|
|
||||||
const snappedHeight = Math.max(GRID_SIZE, Math.round(child.size.height / GRID_SIZE) * GRID_SIZE);
|
|
||||||
|
|
||||||
return {
|
return children.map((child, index) => {
|
||||||
|
console.log(`📐 자식 ${index + 1} 처리 중:`, {
|
||||||
|
childId: child.id,
|
||||||
|
originalPosition: child.position,
|
||||||
|
originalSize: child.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { columnWidth } = gridInfo;
|
||||||
|
const { gap } = gridSettings;
|
||||||
|
|
||||||
|
// 그룹 내부 패딩 고려한 격자 정렬
|
||||||
|
const padding = 16;
|
||||||
|
const effectiveX = child.position.x - padding;
|
||||||
|
const columnIndex = Math.round(effectiveX / (columnWidth + gap));
|
||||||
|
const snappedX = padding + columnIndex * (columnWidth + gap);
|
||||||
|
|
||||||
|
// Y 좌표는 10px 단위로 스냅
|
||||||
|
const rowHeight = 10;
|
||||||
|
const effectiveY = child.position.y - padding;
|
||||||
|
const rowIndex = Math.round(effectiveY / rowHeight);
|
||||||
|
const snappedY = padding + rowIndex * rowHeight;
|
||||||
|
|
||||||
|
// 크기는 외부 격자와 동일하게 스냅 (columnWidth + gap 사용)
|
||||||
|
const fullColumnWidth = columnWidth + gap; // 외부 격자와 동일한 크기
|
||||||
|
const widthInColumns = Math.max(1, Math.round(child.size.width / fullColumnWidth));
|
||||||
|
const snappedWidth = widthInColumns * fullColumnWidth - gap; // gap 제거하여 실제 컴포넌트 크기
|
||||||
|
const snappedHeight = Math.max(10, Math.round(child.size.height / rowHeight) * rowHeight);
|
||||||
|
|
||||||
|
const snappedChild = {
|
||||||
...child,
|
...child,
|
||||||
position: {
|
position: {
|
||||||
x: snappedX,
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||||
y: snappedY,
|
y: Math.max(padding, snappedY),
|
||||||
z: child.position.z || 1,
|
z: child.position.z || 1,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|
@ -136,6 +289,26 @@ export function alignGroupChildrenToGrid(
|
||||||
height: snappedHeight,
|
height: snappedHeight,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(`✅ 자식 ${index + 1} 격자 정렬 완료:`, {
|
||||||
|
childId: child.id,
|
||||||
|
calculation: {
|
||||||
|
effectiveX,
|
||||||
|
effectiveY,
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
widthInColumns,
|
||||||
|
originalX: child.position.x,
|
||||||
|
snappedX: snappedChild.position.x,
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
snappedPosition: snappedChild.position,
|
||||||
|
snappedSize: snappedChild.size,
|
||||||
|
deltaX: snappedChild.position.x - child.position.x,
|
||||||
|
deltaY: snappedChild.position.y - child.position.y,
|
||||||
|
});
|
||||||
|
|
||||||
|
return snappedChild;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,13 +317,19 @@ export function alignGroupChildrenToGrid(
|
||||||
*/
|
*/
|
||||||
export function calculateOptimalGroupSize(
|
export function calculateOptimalGroupSize(
|
||||||
children: Array<{ position: Position; size: Size }>,
|
children: Array<{ position: Position; size: Size }>,
|
||||||
_gridInfo?: GridInfo,
|
gridInfo: GridInfo,
|
||||||
_gridSettings?: GridSettings,
|
gridSettings: GridSettings,
|
||||||
): Size {
|
): Size {
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
return { width: GRID_SIZE * 20, height: GRID_SIZE * 10 };
|
return { width: gridInfo.columnWidth * 2, height: 10 * 4 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("📏 calculateOptimalGroupSize 시작:", {
|
||||||
|
childrenCount: children.length,
|
||||||
|
children: children.map((c) => ({ pos: c.position, size: c.size })),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 자식 컴포넌트를 포함하는 최소 경계 계산
|
||||||
const bounds = children.reduce(
|
const bounds = children.reduce(
|
||||||
(acc, child) => ({
|
(acc, child) => ({
|
||||||
minX: Math.min(acc.minX, child.position.x),
|
minX: Math.min(acc.minX, child.position.x),
|
||||||
|
|
@ -161,38 +340,61 @@ export function calculateOptimalGroupSize(
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("📐 경계 계산:", bounds);
|
||||||
|
|
||||||
const contentWidth = bounds.maxX - bounds.minX;
|
const contentWidth = bounds.maxX - bounds.minX;
|
||||||
const contentHeight = bounds.maxY - bounds.minY;
|
const contentHeight = bounds.maxY - bounds.minY;
|
||||||
const padding = 16;
|
|
||||||
|
|
||||||
return {
|
// 그룹은 격자 스냅 없이 컨텐츠에 맞는 자연스러운 크기
|
||||||
|
const padding = 16; // 그룹 내부 여백
|
||||||
|
const groupSize = {
|
||||||
width: contentWidth + padding * 2,
|
width: contentWidth + padding * 2,
|
||||||
height: contentHeight + padding * 2,
|
height: contentHeight + padding * 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("✅ 자연스러운 그룹 크기:", {
|
||||||
|
contentSize: { width: contentWidth, height: contentHeight },
|
||||||
|
withPadding: groupSize,
|
||||||
|
strategy: "그룹은 격자 스냅 없이, 내부 컴포넌트만 격자에 맞춤",
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 그룹 내 상대 좌표를 격자 기준으로 정규화
|
* 그룹 내 상대 좌표를 격자 기준으로 정규화
|
||||||
*/
|
*/
|
||||||
export function normalizeGroupChildPositions(children: any[], _gridSettings?: GridSettings): any[] {
|
export function normalizeGroupChildPositions(children: any[], gridSettings: GridSettings): any[] {
|
||||||
if (children.length === 0) return children;
|
if (!gridSettings.snapToGrid || children.length === 0) return children;
|
||||||
|
|
||||||
|
console.log("🔄 normalizeGroupChildPositions 시작:", {
|
||||||
|
childrenCount: children.length,
|
||||||
|
originalPositions: children.map((c) => ({ id: c.id, pos: c.position })),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 자식의 최소 위치 찾기
|
||||||
const minX = Math.min(...children.map((child) => child.position.x));
|
const minX = Math.min(...children.map((child) => child.position.x));
|
||||||
const minY = Math.min(...children.map((child) => child.position.y));
|
const minY = Math.min(...children.map((child) => child.position.y));
|
||||||
const padding = 16;
|
|
||||||
|
|
||||||
return children.map((child) => ({
|
console.log("📍 최소 위치:", { minX, minY });
|
||||||
|
|
||||||
|
// 그룹 내에서 시작점을 패딩만큼 떨어뜨림 (자연스러운 여백)
|
||||||
|
const padding = 16;
|
||||||
|
const startX = padding;
|
||||||
|
const startY = padding;
|
||||||
|
|
||||||
|
const normalizedChildren = children.map((child) => ({
|
||||||
...child,
|
...child,
|
||||||
position: {
|
position: {
|
||||||
x: child.position.x - minX + padding,
|
x: child.position.x - minX + startX,
|
||||||
y: child.position.y - minY + padding,
|
y: child.position.y - minY + startY,
|
||||||
z: child.position.z || 1,
|
z: child.position.z || 1,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
// 🗑️ 제거된 함수들 (더 이상 필요 없음)
|
console.log("✅ 정규화 완료:", {
|
||||||
// - calculateWidthFromColumns
|
normalizedPositions: normalizedChildren.map((c) => ({ id: c.id, pos: c.position })),
|
||||||
// - updateSizeFromGridColumns
|
});
|
||||||
// - adjustGridColumnsFromSize
|
|
||||||
// - calculateColumnsFromWidth
|
return normalizedChildren;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -561,22 +561,21 @@ export interface LayoutData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 격자 설정 (10px 고정 격자)
|
* 격자 설정
|
||||||
*/
|
*/
|
||||||
export interface GridSettings {
|
export interface GridSettings {
|
||||||
snapToGrid: boolean; // 격자 스냅 ON/OFF
|
enabled: boolean;
|
||||||
showGrid?: boolean; // 격자 표시 여부
|
size: number;
|
||||||
gridColor?: string; // 격자 선 색상
|
color: string;
|
||||||
gridOpacity?: number; // 격자 선 투명도
|
opacity: number;
|
||||||
|
snapToGrid: boolean;
|
||||||
// 🗑️ 제거된 속성들 (10px 고정으로 더 이상 필요 없음)
|
// gridUtils에서 필요한 속성들 추가
|
||||||
// - columns: 자동 계산 (해상도 ÷ 10px)
|
columns: number;
|
||||||
// - gap: 10px 고정
|
gap: number;
|
||||||
// - padding: 0px 고정
|
padding: number;
|
||||||
// - size: 10px 고정
|
showGrid?: boolean;
|
||||||
// - enabled: showGrid로 대체
|
gridColor?: string;
|
||||||
// - color: gridColor로 대체
|
gridOpacity?: number;
|
||||||
// - opacity: gridOpacity로 대체
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue