From 76ad3d9c43c3f8193bf5377dbe9bd10fb597fad8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 17:42:31 +0900 Subject: [PATCH] =?UTF-8?q?=ED=82=A4=EB=B3=B4=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EB=B3=B5=EC=A0=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/DashboardDesigner.tsx | 68 +++++++++++- .../dashboard/KeyboardShortcutsGuide.tsx | 98 ++++++++++++++++ .../dashboard/hooks/useKeyboardShortcuts.ts | 105 ++++++++++++++++++ 3 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx create mode 100644 frontend/components/admin/dashboard/hooks/useKeyboardShortcuts.ts diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index c0d08083..4cb5f94d 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -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(""); const [successModalOpen, setSuccessModalOpen] = useState(false); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); + const [shortcutsGuideOpen, setShortcutsGuideOpen] = useState(false); + + // 클립보드 (복사/붙여넣기용) + const [clipboard, setClipboard] = useState(null); // 화면 해상도 자동 감지 const [screenResolution] = useState(() => 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 + + {/* 키보드 단축키 가이드 모달 */} + setShortcutsGuideOpen(false)} /> + + {/* 키보드 단축키 도움말 플로팅 버튼 */} +
+ +
); diff --git a/frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx b/frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx new file mode 100644 index 00000000..5de942b7 --- /dev/null +++ b/frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx @@ -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 }) => ( + + {keyName} + +); + +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, + ); + + // Mac OS 감지 + const isMac = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0; + + return ( + + + +
+ + 키보드 단축키 +
+ + 대시보드 편집을 더 빠르게 할 수 있는 단축키 목록입니다 + {isMac && " (Mac에서는 Ctrl 대신 Cmd 키를 사용하세요)"} + +
+ +
+ {Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => ( +
+

{category}

+
+ {categoryShortcuts.map((shortcut, index) => ( +
+ {shortcut.description} +
+ {shortcut.keys.map((key, keyIndex) => ( + + + {keyIndex < shortcut.keys.length - 1 && +} + + ))} +
+
+ ))} +
+
+ ))} +
+ +
+

💡 팁

+

입력 필드나 모달에서는 단축키가 자동으로 비활성화됩니다.

+
+
+
+ ); +} diff --git a/frontend/components/admin/dashboard/hooks/useKeyboardShortcuts.ts b/frontend/components/admin/dashboard/hooks/useKeyboardShortcuts.ts new file mode 100644 index 00000000..d0fab67f --- /dev/null +++ b/frontend/components/admin/dashboard/hooks/useKeyboardShortcuts.ts @@ -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]); +}