키보드를 사용한 복제 및 삭제 구현
This commit is contained in:
parent
8c18555305
commit
76ad3d9c43
|
|
@ -8,11 +8,13 @@ import { ElementConfigModal } from "./ElementConfigModal";
|
|||
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
|
||||
import { YardWidgetConfigModal } from "./widgets/YardWidgetConfigModal";
|
||||
import { DashboardSaveModal } from "./DashboardSaveModal";
|
||||
import { KeyboardShortcutsGuide } from "./KeyboardShortcutsGuide";
|
||||
import { DashboardElement, ElementType, ElementSubtype } from "./types";
|
||||
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils";
|
||||
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
|
||||
import { DashboardProvider } from "@/contexts/DashboardContext";
|
||||
import { useMenu } from "@/contexts/MenuContext";
|
||||
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -25,7 +27,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { CheckCircle2, Keyboard } from "lucide-react";
|
||||
|
||||
interface DashboardDesignerProps {
|
||||
dashboardId?: string;
|
||||
|
|
@ -56,6 +58,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
const [dashboardDescription, setDashboardDescription] = useState<string>("");
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [shortcutsGuideOpen, setShortcutsGuideOpen] = useState(false);
|
||||
|
||||
// 클립보드 (복사/붙여넣기용)
|
||||
const [clipboard, setClipboard] = useState<DashboardElement | null>(null);
|
||||
|
||||
// 화면 해상도 자동 감지
|
||||
const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
|
||||
|
|
@ -289,6 +295,51 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
[selectedElement],
|
||||
);
|
||||
|
||||
// 키보드 단축키 핸들러들
|
||||
const handleCopyElement = useCallback(() => {
|
||||
if (!selectedElement) return;
|
||||
const element = elements.find((el) => el.id === selectedElement);
|
||||
if (element) {
|
||||
setClipboard(element);
|
||||
}
|
||||
}, [selectedElement, elements]);
|
||||
|
||||
const handlePasteElement = useCallback(() => {
|
||||
if (!clipboard) return;
|
||||
|
||||
// 새 ID 생성
|
||||
const newId = `element-${elementCounter + 1}`;
|
||||
setElementCounter((prev) => prev + 1);
|
||||
|
||||
// 위치를 약간 오프셋 (오른쪽 아래로 20px씩)
|
||||
const newElement: DashboardElement = {
|
||||
...clipboard,
|
||||
id: newId,
|
||||
position: {
|
||||
x: clipboard.position.x + 20,
|
||||
y: clipboard.position.y + 20,
|
||||
},
|
||||
};
|
||||
|
||||
setElements((prev) => [...prev, newElement]);
|
||||
setSelectedElement(newId);
|
||||
}, [clipboard, elementCounter]);
|
||||
|
||||
const handleDeleteSelected = useCallback(() => {
|
||||
if (selectedElement) {
|
||||
removeElement(selectedElement);
|
||||
}
|
||||
}, [selectedElement, removeElement]);
|
||||
|
||||
// 키보드 단축키 활성화
|
||||
useKeyboardShortcuts({
|
||||
selectedElementId: selectedElement,
|
||||
onDelete: handleDeleteSelected,
|
||||
onCopy: handleCopyElement,
|
||||
onPaste: handlePasteElement,
|
||||
enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen && !shortcutsGuideOpen,
|
||||
});
|
||||
|
||||
// 전체 삭제 확인 모달 열기
|
||||
const clearCanvas = useCallback(() => {
|
||||
setClearConfirmOpen(true);
|
||||
|
|
@ -602,6 +653,21 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 키보드 단축키 가이드 모달 */}
|
||||
<KeyboardShortcutsGuide isOpen={shortcutsGuideOpen} onClose={() => setShortcutsGuideOpen(false)} />
|
||||
|
||||
{/* 키보드 단축키 도움말 플로팅 버튼 */}
|
||||
<div className="fixed right-8 bottom-8 z-50">
|
||||
<Button
|
||||
onClick={() => setShortcutsGuideOpen(true)}
|
||||
size="lg"
|
||||
className="h-12 w-12 rounded-full p-0 shadow-lg"
|
||||
title="키보드 단축키 (?"
|
||||
>
|
||||
<Keyboard className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Keyboard } from "lucide-react";
|
||||
|
||||
interface KeyboardShortcutsGuideProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface ShortcutItem {
|
||||
keys: string[];
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const shortcuts: ShortcutItem[] = [
|
||||
// 기본 작업
|
||||
{ keys: ["Delete"], description: "선택한 요소 삭제", category: "기본 작업" },
|
||||
{ keys: ["Ctrl", "C"], description: "요소 복사", category: "기본 작업" },
|
||||
{ keys: ["Ctrl", "V"], description: "요소 붙여넣기", category: "기본 작업" },
|
||||
|
||||
// 실행 취소/재실행 (구현 예정)
|
||||
{ keys: ["Ctrl", "Z"], description: "실행 취소 (구현 예정)", category: "편집" },
|
||||
{ keys: ["Ctrl", "Shift", "Z"], description: "재실행 (구현 예정)", category: "편집" },
|
||||
];
|
||||
|
||||
const KeyBadge = ({ keyName }: { keyName: string }) => (
|
||||
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-gray-300 bg-gray-100 px-2 text-xs font-semibold text-gray-800 shadow-sm">
|
||||
{keyName}
|
||||
</kbd>
|
||||
);
|
||||
|
||||
export function KeyboardShortcutsGuide({ isOpen, onClose }: KeyboardShortcutsGuideProps) {
|
||||
// 카테고리별로 그룹화
|
||||
const groupedShortcuts = shortcuts.reduce(
|
||||
(acc, shortcut) => {
|
||||
if (!acc[shortcut.category]) {
|
||||
acc[shortcut.category] = [];
|
||||
}
|
||||
acc[shortcut.category].push(shortcut);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ShortcutItem[]>,
|
||||
);
|
||||
|
||||
// Mac OS 감지
|
||||
const isMac = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Keyboard className="h-5 w-5" />
|
||||
<DialogTitle className="text-base sm:text-lg">키보드 단축키</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
대시보드 편집을 더 빠르게 할 수 있는 단축키 목록입니다
|
||||
{isMac && " (Mac에서는 Ctrl 대신 Cmd 키를 사용하세요)"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-[60vh] space-y-6 overflow-y-auto pr-2">
|
||||
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
|
||||
<div key={category}>
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-900">{category}</h3>
|
||||
<div className="space-y-2">
|
||||
{categoryShortcuts.map((shortcut, index) => (
|
||||
<div
|
||||
key={`${category}-${index}`}
|
||||
className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-3"
|
||||
>
|
||||
<span className="text-sm text-gray-700">{shortcut.description}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{shortcut.keys.map((key, keyIndex) => (
|
||||
<React.Fragment key={keyIndex}>
|
||||
<KeyBadge keyName={isMac && key === "Ctrl" ? "⌘" : key} />
|
||||
{keyIndex < shortcut.keys.length - 1 && <span className="text-xs text-gray-400">+</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-lg bg-blue-50 p-3 text-xs text-blue-800">
|
||||
<p className="font-medium">💡 팁</p>
|
||||
<p className="mt-1">입력 필드나 모달에서는 단축키가 자동으로 비활성화됩니다.</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { useEffect, useCallback } from "react";
|
||||
|
||||
interface KeyboardShortcutsProps {
|
||||
selectedElementId: string | null;
|
||||
onDelete: () => void;
|
||||
onCopy: () => void;
|
||||
onPaste: () => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 키보드 단축키 훅
|
||||
*
|
||||
* 지원 단축키:
|
||||
* - Delete: 선택한 요소 삭제
|
||||
* - Ctrl+C: 요소 복사
|
||||
* - Ctrl+V: 요소 붙여넣기
|
||||
* - Ctrl+Z: 실행 취소 (구현 예정)
|
||||
* - Ctrl+Shift+Z: 재실행 (구현 예정)
|
||||
*/
|
||||
export function useKeyboardShortcuts({
|
||||
selectedElementId,
|
||||
onDelete,
|
||||
onCopy,
|
||||
onPaste,
|
||||
onUndo,
|
||||
onRedo,
|
||||
enabled = true,
|
||||
}: KeyboardShortcutsProps) {
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!enabled) return;
|
||||
|
||||
// 입력 필드에서는 단축키 비활성화
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.contentEditable === "true" ||
|
||||
target.closest('[role="dialog"]') ||
|
||||
target.closest('[role="alertdialog"]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const ctrlKey = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
// Delete: 선택한 요소 삭제
|
||||
if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (selectedElementId) {
|
||||
e.preventDefault();
|
||||
onDelete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+C: 복사
|
||||
if (ctrlKey && e.key === "c") {
|
||||
if (selectedElementId) {
|
||||
e.preventDefault();
|
||||
onCopy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+V: 붙여넣기
|
||||
if (ctrlKey && e.key === "v") {
|
||||
e.preventDefault();
|
||||
onPaste();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Z: 실행 취소
|
||||
if (ctrlKey && e.key === "z" && !e.shiftKey) {
|
||||
if (onUndo) {
|
||||
e.preventDefault();
|
||||
onUndo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Shift+Z 또는 Ctrl+Y: 재실행
|
||||
if ((ctrlKey && e.shiftKey && e.key === "z") || (ctrlKey && e.key === "y")) {
|
||||
if (onRedo) {
|
||||
e.preventDefault();
|
||||
onRedo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
[enabled, selectedElementId, onDelete, onCopy, onPaste, onUndo, onRedo],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown, enabled]);
|
||||
}
|
||||
Loading…
Reference in New Issue