컴포넌트 잠금기능 구현
This commit is contained in:
parent
172ecf34b3
commit
a1ddf4678d
|
|
@ -27,6 +27,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
|
||||
const isSelected = selectedComponentId === component.id;
|
||||
const isMultiSelected = selectedComponentIds.includes(component.id);
|
||||
const isLocked = component.locked === true;
|
||||
|
||||
// 드래그 시작
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
|
|
@ -34,6 +35,15 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 잠긴 컴포넌트는 드래그 불가
|
||||
if (isLocked) {
|
||||
e.stopPropagation();
|
||||
// Ctrl/Cmd 키 감지 (다중 선택)
|
||||
const isMultiSelect = e.ctrlKey || e.metaKey;
|
||||
selectComponent(component.id, isMultiSelect);
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
// Ctrl/Cmd 키 감지 (다중 선택)
|
||||
|
|
@ -49,6 +59,12 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
|
||||
// 리사이즈 시작
|
||||
const handleResizeStart = (e: React.MouseEvent) => {
|
||||
// 잠긴 컴포넌트는 리사이즈 불가
|
||||
if (isLocked) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
setResizeStart({
|
||||
|
|
@ -277,8 +293,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
return (
|
||||
<div
|
||||
ref={componentRef}
|
||||
className={`absolute cursor-move p-2 shadow-sm ${
|
||||
isSelected ? "ring-2 ring-blue-500" : isMultiSelected ? "ring-2 ring-blue-300" : ""
|
||||
className={`absolute p-2 shadow-sm ${isLocked ? "cursor-not-allowed opacity-80" : "cursor-move"} ${
|
||||
isSelected
|
||||
? isLocked
|
||||
? "ring-2 ring-red-500"
|
||||
: "ring-2 ring-blue-500"
|
||||
: isMultiSelected
|
||||
? isLocked
|
||||
? "ring-2 ring-red-300"
|
||||
: "ring-2 ring-blue-300"
|
||||
: ""
|
||||
}`}
|
||||
style={{
|
||||
left: `${component.x}px`,
|
||||
|
|
@ -295,8 +319,13 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
>
|
||||
{renderContent()}
|
||||
|
||||
{/* 리사이즈 핸들 (선택된 경우만) */}
|
||||
{isSelected && (
|
||||
{/* 잠금 표시 */}
|
||||
{isLocked && (
|
||||
<div className="absolute top-1 right-1 rounded bg-red-500 px-1 py-0.5 text-[10px] text-white">🔒</div>
|
||||
)}
|
||||
|
||||
{/* 리사이즈 핸들 (선택된 경우만, 잠금 안 된 경우만) */}
|
||||
{isSelected && !isLocked && (
|
||||
<div
|
||||
className="resize-handle absolute right-0 bottom-0 h-3 w-3 cursor-se-resize rounded-full bg-blue-500"
|
||||
style={{ transform: "translate(50%, 50%)" }}
|
||||
|
|
|
|||
|
|
@ -103,10 +103,10 @@ export function ReportDesignerCanvas() {
|
|||
const idsToMove =
|
||||
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
|
||||
|
||||
// 각 컴포넌트 이동
|
||||
// 각 컴포넌트 이동 (잠긴 컴포넌트는 제외)
|
||||
idsToMove.forEach((id) => {
|
||||
const component = components.find((c) => c.id === id);
|
||||
if (!component) return;
|
||||
if (!component || component.locked) return;
|
||||
|
||||
let newX = component.x;
|
||||
let newY = component.y;
|
||||
|
|
@ -132,12 +132,20 @@ export function ReportDesignerCanvas() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Delete 키: 삭제
|
||||
// Delete 키: 삭제 (잠긴 컴포넌트는 제외)
|
||||
if (e.key === "Delete") {
|
||||
if (selectedComponentIds.length > 0) {
|
||||
selectedComponentIds.forEach((id) => removeComponent(id));
|
||||
selectedComponentIds.forEach((id) => {
|
||||
const component = components.find((c) => c.id === id);
|
||||
if (component && !component.locked) {
|
||||
removeComponent(id);
|
||||
}
|
||||
});
|
||||
} else if (selectedComponentId) {
|
||||
removeComponent(selectedComponentId);
|
||||
const component = components.find((c) => c.id === selectedComponentId);
|
||||
if (component && !component.locked) {
|
||||
removeComponent(selectedComponentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
ChevronsDown,
|
||||
ChevronsUp,
|
||||
ChevronUp,
|
||||
Lock,
|
||||
Unlock,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
|
|
@ -78,6 +80,9 @@ export function ReportDesignerToolbar() {
|
|||
sendToBack,
|
||||
bringForward,
|
||||
sendBackward,
|
||||
toggleLock,
|
||||
lockComponents,
|
||||
unlockComponents,
|
||||
} = useReportDesigner();
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
||||
|
|
@ -357,6 +362,37 @@ export function ReportDesignerToolbar() {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 잠금 드롭다운 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!hasSelection}
|
||||
className="gap-2"
|
||||
title="컴포넌트 잠금/해제 (1개 이상 선택 필요)"
|
||||
>
|
||||
<Lock className="h-4 w-4" />
|
||||
잠금
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={toggleLock}>
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
토글 (잠금/해제)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={lockComponents}>
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
잠금 설정
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={unlockComponents}>
|
||||
<Unlock 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" />
|
||||
초기화
|
||||
|
|
|
|||
|
|
@ -351,6 +351,11 @@ interface ReportDesignerContextType {
|
|||
sendToBack: () => void;
|
||||
bringForward: () => void;
|
||||
sendBackward: () => void;
|
||||
|
||||
// 잠금 관리
|
||||
toggleLock: () => void;
|
||||
lockComponents: () => void;
|
||||
unlockComponents: () => void;
|
||||
}
|
||||
|
||||
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
|
||||
|
|
@ -856,6 +861,65 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
toast({ title: "레이어 변경", description: "한 단계 뒤로 이동했습니다." });
|
||||
}, [selectedComponentId, selectedComponentIds, toast]);
|
||||
|
||||
// 잠금 관리 함수들
|
||||
const toggleLock = useCallback(() => {
|
||||
if (!selectedComponentId && selectedComponentIds.length === 0) return;
|
||||
|
||||
const idsToUpdate =
|
||||
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
|
||||
|
||||
setComponents((prev) =>
|
||||
prev.map((c) => {
|
||||
if (idsToUpdate.includes(c.id)) {
|
||||
return { ...c, locked: !c.locked };
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
);
|
||||
|
||||
const isLocking = components.find((c) => idsToUpdate.includes(c.id))?.locked === false;
|
||||
toast({
|
||||
title: isLocking ? "잠금 설정" : "잠금 해제",
|
||||
description: isLocking ? "선택된 컴포넌트가 잠겼습니다." : "선택된 컴포넌트의 잠금이 해제되었습니다.",
|
||||
});
|
||||
}, [selectedComponentId, selectedComponentIds, components, toast]);
|
||||
|
||||
const lockComponents = useCallback(() => {
|
||||
if (!selectedComponentId && selectedComponentIds.length === 0) return;
|
||||
|
||||
const idsToUpdate =
|
||||
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
|
||||
|
||||
setComponents((prev) =>
|
||||
prev.map((c) => {
|
||||
if (idsToUpdate.includes(c.id)) {
|
||||
return { ...c, locked: true };
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
);
|
||||
|
||||
toast({ title: "잠금 설정", description: "선택된 컴포넌트가 잠겼습니다." });
|
||||
}, [selectedComponentId, selectedComponentIds, toast]);
|
||||
|
||||
const unlockComponents = useCallback(() => {
|
||||
if (!selectedComponentId && selectedComponentIds.length === 0) return;
|
||||
|
||||
const idsToUpdate =
|
||||
selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]);
|
||||
|
||||
setComponents((prev) =>
|
||||
prev.map((c) => {
|
||||
if (idsToUpdate.includes(c.id)) {
|
||||
return { ...c, locked: false };
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
);
|
||||
|
||||
toast({ title: "잠금 해제", description: "선택된 컴포넌트의 잠금이 해제되었습니다." });
|
||||
}, [selectedComponentId, selectedComponentIds, toast]);
|
||||
|
||||
// 캔버스 설정 (기본값)
|
||||
const [canvasWidth, setCanvasWidth] = useState(210);
|
||||
const [canvasHeight, setCanvasHeight] = useState(297);
|
||||
|
|
@ -1272,6 +1336,10 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
sendToBack,
|
||||
bringForward,
|
||||
sendBackward,
|
||||
// 잠금 관리
|
||||
toggleLock,
|
||||
lockComponents,
|
||||
unlockComponents,
|
||||
};
|
||||
|
||||
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export interface ComponentConfig {
|
|||
visible?: boolean;
|
||||
printable?: boolean;
|
||||
conditional?: string;
|
||||
locked?: boolean; // 잠금 여부 (편집/이동/삭제 방지)
|
||||
}
|
||||
|
||||
// 리포트 상세
|
||||
|
|
|
|||
Loading…
Reference in New Issue