컴포넌트 그룹화(Grouping) 기능 구현
This commit is contained in:
@@ -10,6 +10,7 @@ interface CanvasComponentProps {
|
||||
|
||||
export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
const {
|
||||
components,
|
||||
selectedComponentId,
|
||||
selectedComponentIds,
|
||||
selectComponent,
|
||||
@@ -28,6 +29,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
const isSelected = selectedComponentId === component.id;
|
||||
const isMultiSelected = selectedComponentIds.includes(component.id);
|
||||
const isLocked = component.locked === true;
|
||||
const isGrouped = !!component.groupId;
|
||||
|
||||
// 드래그 시작
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
@@ -48,7 +50,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
|
||||
// Ctrl/Cmd 키 감지 (다중 선택)
|
||||
const isMultiSelect = e.ctrlKey || e.metaKey;
|
||||
selectComponent(component.id, isMultiSelect);
|
||||
|
||||
// 그룹화된 컴포넌트 클릭 시: 같은 그룹의 모든 컴포넌트 선택
|
||||
if (isGrouped && !isMultiSelect) {
|
||||
const groupMembers = components.filter((c) => c.groupId === component.groupId);
|
||||
const groupMemberIds = groupMembers.map((c) => c.id);
|
||||
// 첫 번째 컴포넌트를 선택하고, 나머지를 다중 선택에 추가
|
||||
selectComponent(groupMemberIds[0], false);
|
||||
groupMemberIds.slice(1).forEach((id) => selectComponent(id, true));
|
||||
} else {
|
||||
selectComponent(component.id, isMultiSelect);
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
setDragStart({
|
||||
@@ -89,11 +101,27 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
// 정렬 가이드라인 계산
|
||||
calculateAlignmentGuides(component.id, snappedX, snappedY, component.width, component.height);
|
||||
|
||||
// Grid Snap 적용
|
||||
// 이동 거리 계산
|
||||
const deltaX = snappedX - component.x;
|
||||
const deltaY = snappedY - component.y;
|
||||
|
||||
// 현재 컴포넌트 이동
|
||||
updateComponent(component.id, {
|
||||
x: snappedX,
|
||||
y: snappedY,
|
||||
});
|
||||
|
||||
// 그룹화된 경우: 같은 그룹의 다른 컴포넌트도 함께 이동
|
||||
if (isGrouped) {
|
||||
components.forEach((c) => {
|
||||
if (c.groupId === component.groupId && c.id !== component.id) {
|
||||
updateComponent(c.id, {
|
||||
x: c.x + deltaX,
|
||||
y: c.y + deltaY,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (isResizing) {
|
||||
const deltaX = e.clientX - resizeStart.x;
|
||||
const deltaY = e.clientY - resizeStart.y;
|
||||
@@ -131,8 +159,13 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
resizeStart.width,
|
||||
resizeStart.height,
|
||||
component.id,
|
||||
component.x,
|
||||
component.y,
|
||||
component.width,
|
||||
component.height,
|
||||
component.groupId,
|
||||
isGrouped,
|
||||
components,
|
||||
updateComponent,
|
||||
snapValueToGrid,
|
||||
calculateAlignmentGuides,
|
||||
@@ -324,6 +357,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||
<div className="absolute top-1 right-1 rounded bg-red-500 px-1 py-0.5 text-[10px] text-white">🔒</div>
|
||||
)}
|
||||
|
||||
{/* 그룹화 표시 */}
|
||||
{isGrouped && !isLocked && (
|
||||
<div className="absolute top-1 left-1 rounded bg-purple-500 px-1 py-0.5 text-[10px] text-white">👥</div>
|
||||
)}
|
||||
|
||||
{/* 리사이즈 핸들 (선택된 경우만, 잠금 안 된 경우만) */}
|
||||
{isSelected && !isLocked && (
|
||||
<div
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
Lock,
|
||||
Unlock,
|
||||
Ruler as RulerIcon,
|
||||
Group,
|
||||
Ungroup,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
@@ -86,6 +88,8 @@ export function ReportDesignerToolbar() {
|
||||
unlockComponents,
|
||||
showRuler,
|
||||
setShowRuler,
|
||||
groupComponents,
|
||||
ungroupComponents,
|
||||
} = useReportDesigner();
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
||||
@@ -95,6 +99,7 @@ export function ReportDesignerToolbar() {
|
||||
const canAlign = selectedComponentIds && selectedComponentIds.length >= 2;
|
||||
const canDistribute = selectedComponentIds && selectedComponentIds.length >= 3;
|
||||
const hasSelection = selectedComponentIds && selectedComponentIds.length >= 1;
|
||||
const canGroup = selectedComponentIds && selectedComponentIds.length >= 2;
|
||||
|
||||
// 템플릿 저장 가능 여부: 컴포넌트가 있어야 함
|
||||
const canSaveAsTemplate = components.length > 0;
|
||||
@@ -406,6 +411,32 @@ export function ReportDesignerToolbar() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 그룹화 드롭다운 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!hasSelection}
|
||||
className="gap-2"
|
||||
title="컴포넌트 그룹화/해제"
|
||||
>
|
||||
<Group className="h-4 w-4" />
|
||||
그룹
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={groupComponents} disabled={!canGroup}>
|
||||
<Group className="mr-2 h-4 w-4" />
|
||||
그룹화 (2개 이상)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={ungroupComponents} disabled={!hasSelection}>
|
||||
<Ungroup className="mr-2 h-4 w-4" />
|
||||
그룹 해제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={handleReset} className="gap-2">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
초기화
|
||||
|
||||
Reference in New Issue
Block a user