컴포넌트 그룹화기능

This commit is contained in:
kjs 2025-09-01 15:57:49 +09:00
parent ad988f2951
commit e18c78f40d
3 changed files with 70 additions and 49 deletions

View File

@ -254,7 +254,7 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
<div className="flex items-center space-x-1">
<Group className="h-3 w-3 text-blue-600" />
<span className="text-xs font-medium">{label || "그룹"}</span>
<span className="text-xs text-gray-500">({children.length})</span>
<span className="text-xs text-gray-500">({children ? children.length : 0})</span>
</div>
{component.collapsible &&
(component.collapsed ? (

View File

@ -535,29 +535,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
// 경계 박스 계산
const boundingBox = calculateBoundingBox(selectedComponents);
// 그룹 컴포넌트 생성
// 그룹 컴포넌트 생성 (경계 박스 정보 전달)
const groupComponent = createGroupComponent(
componentIds,
title,
{ x: boundingBox.minX, y: boundingBox.minY },
{ width: boundingBox.width, height: boundingBox.height },
style,
);
// 자식 컴포넌트들의 상대 위치 계산
const relativeChildren = calculateRelativePositions(selectedComponents, {
x: boundingBox.minX,
y: boundingBox.minY,
});
const relativeChildren = calculateRelativePositions(
selectedComponents,
{
x: boundingBox.minX,
y: boundingBox.minY,
},
groupComponent.id,
);
// 새 레이아웃 생성
const newLayout = {
...layout,
components: [
// 그룹이 아닌 기존 컴포넌트들
...layout.components.filter((comp) => !componentIds.includes(comp.id) && comp.type !== "group"),
// 그룹 컴포넌트
// 그룹에 포함되지 않은 기존 컴포넌트들만 유지
...layout.components.filter((comp) => !componentIds.includes(comp.id)),
// 그룹 컴포넌트 추가
groupComponent,
// 상대 위치로 업데이트된 자식 컴포넌트들
// 자식 컴포넌트들도 유지 (parentId로 그룹과 연결)
...relativeChildren,
],
};
@ -585,8 +590,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const newLayout = {
...layout,
components: [
// 그룹이 아닌 기존 컴포넌트들
...layout.components.filter((comp) => comp.id !== groupId),
// 그룹과 그룹의 자식 컴포넌트들을 제외한 기존 컴포넌트들
...layout.components.filter((comp) => comp.id !== groupId && comp.parentId !== groupId),
// 절대 위치로 복원된 자식 컴포넌트들
...absoluteChildren,
],
@ -986,38 +991,45 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
</div>
{/* 컴포넌트들 - 실시간 미리보기 */}
{layout.components.map((component) => {
// 그룹 컴포넌트인 경우 자식 컴포넌트들 가져오기
const children =
component.type === "group"
? layout.components.filter((child) => child.parentId === component.id)
: [];
{layout.components
.filter((component) => !component.parentId) // 최상위 컴포넌트만 렌더링
.map((component) => {
// 그룹 컴포넌트인 경우 자식 컴포넌트들 가져오기
const children =
component.type === "group"
? layout.components.filter((child) => child.parentId === component.id)
: [];
return (
<RealtimePreview
key={component.id}
component={component}
isSelected={
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
}
onClick={() => handleComponentClick(component)}
onDragStart={(e) => startComponentDrag(component, e)}
onDragEnd={endDrag}
onGroupToggle={(groupId) => {
// 그룹 접기/펼치기 토글
const groupComp = component as GroupComponent;
updateComponentProperty(groupId, "collapsed", !groupComp.collapsed);
}}
>
{children.map((child) => (
<div key={child.id} className="rounded border bg-white p-2 text-xs text-gray-600">
<div className="font-medium">{child.label || (child as any).columnName || child.id}</div>
<div className="text-gray-500">{child.type}</div>
</div>
))}
</RealtimePreview>
);
})}
return (
<RealtimePreview
key={component.id}
component={component}
isSelected={
selectedComponent?.id === component.id ||
groupState.selectedComponents.includes(component.id)
}
onClick={() => handleComponentClick(component)}
onDragStart={(e) => startComponentDrag(component, e)}
onDragEnd={endDrag}
onGroupToggle={(groupId) => {
// 그룹 접기/펼치기 토글
const groupComp = component as GroupComponent;
updateComponentProperty(groupId, "collapsed", !groupComp.collapsed);
}}
>
{children.map((child) => (
<RealtimePreview
key={child.id}
component={child}
isSelected={groupState.selectedComponents.includes(child.id)}
onClick={() => handleComponentClick(child)}
onDragStart={(e) => startComponentDrag(child, e)}
onDragEnd={endDrag}
/>
))}
</RealtimePreview>
);
})}
</div>
)}
</div>

View File

@ -12,14 +12,19 @@ export function createGroupComponent(
componentIds: string[],
title: string = "새 그룹",
position: Position = { x: 0, y: 0 },
boundingBox?: { width: number; height: number },
style?: any,
): GroupComponent {
// 격자 기반 크기 계산
const gridWidth = Math.max(6, Math.ceil(boundingBox?.width / 80) + 2); // 최소 6 그리드, 여백 2
const gridHeight = Math.max(100, (boundingBox?.height || 200) + 40); // 최소 100px, 여백 40px
return {
id: generateComponentId(),
type: "group",
position,
size: { width: 12, height: 200 }, // 기본 크기
title,
size: { width: gridWidth, height: gridHeight },
label: title, // title 대신 label 사용
backgroundColor: "#f8f9fa",
border: "1px solid #dee2e6",
borderRadius: 8,
@ -34,7 +39,7 @@ export function createGroupComponent(
};
}
// 선택된 컴포넌트들의 경계 박스 계산
// 선택된 컴포넌트들의 경계 박스 계산 (격자 기반)
export function calculateBoundingBox(components: ComponentData[]): {
minX: number;
minY: number;
@ -49,7 +54,7 @@ export function calculateBoundingBox(components: ComponentData[]): {
const minX = Math.min(...components.map((c) => c.position.x));
const minY = Math.min(...components.map((c) => c.position.y));
const maxX = Math.max(...components.map((c) => c.position.x + (c.size.width * 80 - 16)));
const maxX = Math.max(...components.map((c) => c.position.x + c.size.width * 80));
const maxY = Math.max(...components.map((c) => c.position.y + c.size.height));
return {
@ -63,14 +68,18 @@ export function calculateBoundingBox(components: ComponentData[]): {
}
// 그룹 내 컴포넌트들의 상대 위치 계산
export function calculateRelativePositions(components: ComponentData[], groupPosition: Position): ComponentData[] {
export function calculateRelativePositions(
components: ComponentData[],
groupPosition: Position,
groupId: string,
): ComponentData[] {
return components.map((component) => ({
...component,
position: {
x: component.position.x - groupPosition.x,
y: component.position.y - groupPosition.y,
},
parentId: components[0]?.id, // 임시로 첫 번째 컴포넌트 ID 사용
parentId: groupId, // 그룹 ID를 부모로 설정
}));
}