From 76ad3d9c43c3f8193bf5377dbe9bd10fb597fad8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 17:42:31 +0900 Subject: [PATCH 1/3] =?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]); +} From b62f2ffc10ae18b46a9fcc5a1d4337351fcda5dc Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 09:45:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?api=EA=B4=80=EB=A6=AC=20=EA=B5=AC=ED=98=84(?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=EC=AA=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/data-sources/ApiConfig.tsx | 135 ++++++++++++++++-- frontend/lib/api/externalDbConnection.ts | 94 ++++++++++-- 2 files changed, 208 insertions(+), 21 deletions(-) diff --git a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx index 09f45411..2e6616f9 100644 --- a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx @@ -1,12 +1,14 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { ChartDataSource, QueryResult, KeyValuePair } from "../types"; import { Card } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Plus, X, Play, AlertCircle } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; interface ApiConfigProps { dataSource: ChartDataSource; @@ -24,6 +26,106 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [testError, setTestError] = useState(null); + const [apiConnections, setApiConnections] = useState([]); + const [selectedConnectionId, setSelectedConnectionId] = useState(""); + + // 외부 API 커넥션 목록 로드 + useEffect(() => { + const loadApiConnections = async () => { + const connections = await ExternalDbConnectionAPI.getApiConnections({ is_active: "Y" }); + setApiConnections(connections); + }; + loadApiConnections(); + }, []); + + // 외부 커넥션 선택 핸들러 + const handleConnectionSelect = async (connectionId: string) => { + setSelectedConnectionId(connectionId); + + if (!connectionId || connectionId === "manual") return; + + const connection = await ExternalDbConnectionAPI.getApiConnectionById(Number(connectionId)); + if (!connection) { + console.error("커넥션을 찾을 수 없습니다:", connectionId); + return; + } + + console.log("불러온 커넥션:", connection); + + // 커넥션 설정을 API 설정에 자동 적용 + const updates: Partial = { + endpoint: connection.base_url, + }; + + const headers: KeyValuePair[] = []; + const queryParams: KeyValuePair[] = []; + + // 기본 헤더가 있으면 적용 + if (connection.default_headers && Object.keys(connection.default_headers).length > 0) { + Object.entries(connection.default_headers).forEach(([key, value]) => { + headers.push({ + id: `header_${Date.now()}_${Math.random()}`, + key, + value, + }); + }); + console.log("기본 헤더 적용:", headers); + } + + // 인증 설정이 있으면 헤더 또는 쿼리 파라미터에 추가 + if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) { + console.log("인증 설정:", connection.auth_type, connection.auth_config); + + if (connection.auth_type === "bearer" && connection.auth_config.token) { + headers.push({ + id: `header_${Date.now()}_auth`, + key: "Authorization", + value: `Bearer ${connection.auth_config.token}`, + }); + console.log("Bearer 토큰 추가"); + } else if (connection.auth_type === "api-key") { + console.log("API Key 설정:", connection.auth_config); + + if (connection.auth_config.keyName && connection.auth_config.keyValue) { + if (connection.auth_config.keyLocation === "header") { + headers.push({ + id: `header_${Date.now()}_apikey`, + key: connection.auth_config.keyName, + value: connection.auth_config.keyValue, + }); + console.log(`API Key 헤더 추가: ${connection.auth_config.keyName}=${connection.auth_config.keyValue}`); + } else if (connection.auth_config.keyLocation === "query") { + queryParams.push({ + id: `param_${Date.now()}_apikey`, + key: connection.auth_config.keyName, + value: connection.auth_config.keyValue, + }); + console.log( + `API Key 쿼리 파라미터 추가: ${connection.auth_config.keyName}=${connection.auth_config.keyValue}`, + ); + } + } + } else if ( + connection.auth_type === "basic" && + connection.auth_config.username && + connection.auth_config.password + ) { + const basicAuth = btoa(`${connection.auth_config.username}:${connection.auth_config.password}`); + headers.push({ + id: `header_${Date.now()}_basic`, + key: "Authorization", + value: `Basic ${basicAuth}`, + }); + console.log("Basic Auth 추가"); + } + } + + updates.headers = headers; + updates.queryParams = queryParams; + console.log("최종 업데이트:", updates); + + onChange(updates); + }; // 헤더를 배열로 정규화 (객체 형식 호환) const normalizeHeaders = (): KeyValuePair[] => { @@ -217,6 +319,30 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps

외부 API에서 데이터를 가져올 설정을 입력하세요

+ {/* 외부 커넥션 선택 */} + {apiConnections.length > 0 && ( + +
+ + +

외부 커넥션 관리에서 저장한 REST API 설정을 불러올 수 있습니다

+
+
+ )} + {/* API URL */}
@@ -230,13 +356,6 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps />

GET 요청을 보낼 API 엔드포인트

- - {/* HTTP 메서드 (고정) */} -
- -
GET (고정)
-

데이터 조회는 GET 메서드만 지원합니다

-
{/* 쿼리 파라미터 */} diff --git a/frontend/lib/api/externalDbConnection.ts b/frontend/lib/api/externalDbConnection.ts index aa161af7..257a7a3f 100644 --- a/frontend/lib/api/externalDbConnection.ts +++ b/frontend/lib/api/externalDbConnection.ts @@ -27,6 +27,36 @@ export interface ExternalDbConnection { updated_by?: string; } +export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2"; + +export interface ExternalApiConnection { + id?: number; + connection_name: string; + description?: string; + base_url: string; + default_headers: Record; + auth_type: AuthType; + auth_config?: { + keyLocation?: "header" | "query"; + keyName?: string; + keyValue?: string; + token?: string; + username?: string; + password?: string; + clientId?: string; + clientSecret?: string; + tokenUrl?: string; + accessToken?: string; + }; + timeout?: number; + company_code: string; + is_active: string; + created_date?: Date; + created_by?: string; + updated_date?: Date; + updated_by?: string; +} + export interface ExternalDbConnectionFilter { db_type?: string; is_active?: string; @@ -209,7 +239,7 @@ export class ExternalDbConnectionAPI { try { const response = await apiClient.post>( `${this.BASE_PATH}/${connectionId}/test`, - password ? { password } : undefined + password ? { password } : undefined, ); if (!response.data.success) { @@ -220,10 +250,12 @@ export class ExternalDbConnectionAPI { }; } - return response.data.data || { - success: true, - message: response.data.message || "연결 테스트가 완료되었습니다.", - }; + return ( + response.data.data || { + success: true, + message: response.data.message || "연결 테스트가 완료되었습니다.", + } + ); } catch (error) { console.error("연결 테스트 오류:", error); @@ -246,9 +278,7 @@ export class ExternalDbConnectionAPI { */ static async getTables(connectionId: number): Promise> { try { - const response = await apiClient.get>( - `${this.BASE_PATH}/${connectionId}/tables` - ); + const response = await apiClient.get>(`${this.BASE_PATH}/${connectionId}/tables`); return response.data; } catch (error) { console.error("테이블 목록 조회 오류:", error); @@ -260,7 +290,7 @@ export class ExternalDbConnectionAPI { try { console.log("컬럼 정보 API 요청:", `${this.BASE_PATH}/${connectionId}/tables/${tableName}/columns`); const response = await apiClient.get>( - `${this.BASE_PATH}/${connectionId}/tables/${tableName}/columns` + `${this.BASE_PATH}/${connectionId}/tables/${tableName}/columns`, ); console.log("컬럼 정보 API 응답:", response.data); return response.data; @@ -273,10 +303,7 @@ export class ExternalDbConnectionAPI { static async executeQuery(connectionId: number, query: string): Promise> { try { console.log("API 요청:", `${this.BASE_PATH}/${connectionId}/execute`, { query }); - const response = await apiClient.post>( - `${this.BASE_PATH}/${connectionId}/execute`, - { query } - ); + const response = await apiClient.post>(`${this.BASE_PATH}/${connectionId}/execute`, { query }); console.log("API 응답:", response.data); return response.data; } catch (error) { @@ -284,4 +311,45 @@ export class ExternalDbConnectionAPI { throw error; } } + + /** + * REST API 연결 목록 조회 (외부 커넥션에서) + */ + static async getApiConnections(filter: { is_active?: string } = {}): Promise { + try { + const params = new URLSearchParams(); + if (filter.is_active) params.append("is_active", filter.is_active); + + const response = await apiClient.get>( + `/external-rest-api-connections?${params.toString()}`, + ); + + if (!response.data.success) { + throw new Error(response.data.message || "API 연결 목록 조회에 실패했습니다."); + } + + return response.data.data || []; + } catch (error) { + console.error("API 연결 목록 조회 오류:", error); + return []; + } + } + + /** + * 특정 REST API 연결 조회 + */ + static async getApiConnectionById(id: number): Promise { + try { + const response = await apiClient.get>(`/external-rest-api-connections/${id}`); + + if (!response.data.success || !response.data.data) { + return null; + } + + return response.data.data; + } catch (error) { + console.error("API 연결 조회 오류:", error); + return null; + } + } } From 0823874ebc450bb41bd94fa81458142c2f826c37 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 10:07:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/DashboardDesigner.tsx | 21 +--- .../dashboard/KeyboardShortcutsGuide.tsx | 98 ------------------- 2 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 4cb5f94d..960583ce 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -8,7 +8,6 @@ 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"; @@ -27,7 +26,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; -import { CheckCircle2, Keyboard } from "lucide-react"; +import { CheckCircle2 } from "lucide-react"; interface DashboardDesignerProps { dashboardId?: string; @@ -58,7 +57,6 @@ 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); @@ -337,7 +335,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D onDelete: handleDeleteSelected, onCopy: handleCopyElement, onPaste: handlePasteElement, - enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen && !shortcutsGuideOpen, + enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen, }); // 전체 삭제 확인 모달 열기 @@ -653,21 +651,6 @@ 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 deleted file mode 100644 index 5de942b7..00000000 --- a/frontend/components/admin/dashboard/KeyboardShortcutsGuide.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"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 && +} - - ))} -
-
- ))} -
-
- ))} -
- -
-

💡 팁

-

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

-
-
-
- ); -}