레이어 관리 구현

This commit is contained in:
dohyeons 2025-10-01 16:17:41 +09:00
parent 722a413916
commit 172ecf34b3
2 changed files with 134 additions and 1 deletions

View File

@ -22,6 +22,10 @@ import {
RectangleHorizontal,
RectangleVertical,
Square,
ChevronDown,
ChevronsDown,
ChevronsUp,
ChevronUp,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
@ -70,14 +74,19 @@ export function ReportDesignerToolbar() {
makeSameWidth,
makeSameHeight,
makeSameSize,
bringToFront,
sendToBack,
bringForward,
sendBackward,
} = useReportDesigner();
const [showPreview, setShowPreview] = useState(false);
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
const { toast } = useToast();
// 정렬 버튼 활성화 조건
// 버튼 활성화 조건
const canAlign = selectedComponentIds && selectedComponentIds.length >= 2;
const canDistribute = selectedComponentIds && selectedComponentIds.length >= 3;
const hasSelection = selectedComponentIds && selectedComponentIds.length >= 1;
// 템플릿 저장 가능 여부: 컴포넌트가 있어야 함
const canSaveAsTemplate = components.length > 0;
@ -316,6 +325,38 @@ export function ReportDesignerToolbar() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* 레이어 드롭다운 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={!hasSelection}
className="gap-2"
title="레이어 순서 (1개 이상 선택 필요)"
>
<ChevronsUp className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={bringToFront}>
<ChevronsUp className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={bringForward}>
<ChevronUp className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={sendBackward}>
<ChevronDown className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={sendToBack}>
<ChevronsDown 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" />

View File

@ -345,6 +345,12 @@ interface ReportDesignerContextType {
makeSameWidth: () => void;
makeSameHeight: () => void;
makeSameSize: () => void;
// 레이어 관리
bringToFront: () => void;
sendToBack: () => void;
bringForward: () => void;
sendBackward: () => void;
}
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
@ -769,6 +775,87 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
toast({ title: "크기 조정 완료", description: "같은 크기로 조정되었습니다." });
}, [getSelectedComponents, toast]);
// 레이어 관리 함수들
const bringToFront = useCallback(() => {
if (!selectedComponentId && selectedComponentIds.length === 0) return;
const idsToUpdate =
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
const maxZIndex = Math.max(...components.map((c) => c.zIndex));
setComponents((prev) =>
prev.map((c) => {
if (idsToUpdate.includes(c.id)) {
return { ...c, zIndex: maxZIndex + 1 };
}
return c;
}),
);
toast({ title: "레이어 변경", description: "맨 앞으로 이동했습니다." });
}, [selectedComponentId, selectedComponentIds, components, toast]);
const sendToBack = useCallback(() => {
if (!selectedComponentId && selectedComponentIds.length === 0) return;
const idsToUpdate =
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
const minZIndex = Math.min(...components.map((c) => c.zIndex));
setComponents((prev) =>
prev.map((c) => {
if (idsToUpdate.includes(c.id)) {
return { ...c, zIndex: minZIndex - 1 };
}
return c;
}),
);
toast({ title: "레이어 변경", description: "맨 뒤로 이동했습니다." });
}, [selectedComponentId, selectedComponentIds, components, toast]);
const bringForward = useCallback(() => {
if (!selectedComponentId && selectedComponentIds.length === 0) return;
const idsToUpdate =
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
setComponents((prev) => {
const sorted = [...prev].sort((a, b) => a.zIndex - b.zIndex);
const updated = sorted.map((c, index) => ({ ...c, zIndex: index }));
return updated.map((c) => {
if (idsToUpdate.includes(c.id)) {
return { ...c, zIndex: Math.min(c.zIndex + 1, updated.length - 1) };
}
return c;
});
});
toast({ title: "레이어 변경", description: "한 단계 앞으로 이동했습니다." });
}, [selectedComponentId, selectedComponentIds, toast]);
const sendBackward = useCallback(() => {
if (!selectedComponentId && selectedComponentIds.length === 0) return;
const idsToUpdate =
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
setComponents((prev) => {
const sorted = [...prev].sort((a, b) => a.zIndex - b.zIndex);
const updated = sorted.map((c, index) => ({ ...c, zIndex: index }));
return updated.map((c) => {
if (idsToUpdate.includes(c.id)) {
return { ...c, zIndex: Math.max(c.zIndex - 1, 0) };
}
return c;
});
});
toast({ title: "레이어 변경", description: "한 단계 뒤로 이동했습니다." });
}, [selectedComponentId, selectedComponentIds, toast]);
// 캔버스 설정 (기본값)
const [canvasWidth, setCanvasWidth] = useState(210);
const [canvasHeight, setCanvasHeight] = useState(297);
@ -1180,6 +1267,11 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
makeSameWidth,
makeSameHeight,
makeSameSize,
// 레이어 관리
bringToFront,
sendToBack,
bringForward,
sendBackward,
};
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;