diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 50234c37..4a839817 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -254,7 +254,7 @@ export const RealtimePreview: React.FC = ({
{label || "그룹"} - ({children.length}개) + ({children ? children.length : 0}개)
{component.collapsible && (component.collapsed ? ( diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index b3aaaf9b..1e17a203 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -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 {/* 컴포넌트들 - 실시간 미리보기 */} - {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 ( - handleComponentClick(component)} - onDragStart={(e) => startComponentDrag(component, e)} - onDragEnd={endDrag} - onGroupToggle={(groupId) => { - // 그룹 접기/펼치기 토글 - const groupComp = component as GroupComponent; - updateComponentProperty(groupId, "collapsed", !groupComp.collapsed); - }} - > - {children.map((child) => ( -
-
{child.label || (child as any).columnName || child.id}
-
{child.type}
-
- ))} -
- ); - })} + return ( + handleComponentClick(component)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} + onGroupToggle={(groupId) => { + // 그룹 접기/펼치기 토글 + const groupComp = component as GroupComponent; + updateComponentProperty(groupId, "collapsed", !groupComp.collapsed); + }} + > + {children.map((child) => ( + handleComponentClick(child)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + /> + ))} + + ); + })} )} diff --git a/frontend/lib/utils/groupingUtils.ts b/frontend/lib/utils/groupingUtils.ts index e10794b9..16108fae 100644 --- a/frontend/lib/utils/groupingUtils.ts +++ b/frontend/lib/utils/groupingUtils.ts @@ -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를 부모로 설정 })); }