Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
@@ -13,6 +13,7 @@ import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gr
|
||||
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,
|
||||
@@ -58,6 +59,9 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
|
||||
// 클립보드 (복사/붙여넣기용)
|
||||
const [clipboard, setClipboard] = useState<DashboardElement | null>(null);
|
||||
|
||||
// 화면 해상도 자동 감지
|
||||
const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
|
||||
const [resolution, setResolution] = useState<Resolution>(screenResolution);
|
||||
@@ -290,6 +294,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,
|
||||
});
|
||||
|
||||
// 전체 삭제 확인 모달 열기
|
||||
const clearCanvas = useCallback(() => {
|
||||
setClearConfirmOpen(true);
|
||||
|
||||
@@ -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<QueryResult | null>(null);
|
||||
const [testError, setTestError] = useState<string | null>(null);
|
||||
const [apiConnections, setApiConnections] = useState<ExternalApiConnection[]>([]);
|
||||
const [selectedConnectionId, setSelectedConnectionId] = useState<string>("");
|
||||
|
||||
// 외부 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<ChartDataSource> = {
|
||||
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
|
||||
<p className="mt-1 text-sm text-gray-600">외부 API에서 데이터를 가져올 설정을 입력하세요</p>
|
||||
</div>
|
||||
|
||||
{/* 외부 커넥션 선택 */}
|
||||
{apiConnections.length > 0 && (
|
||||
<Card className="space-y-4 p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700">외부 커넥션 (선택)</Label>
|
||||
<Select value={selectedConnectionId} onValueChange={handleConnectionSelect}>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="저장된 커넥션 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
<SelectItem value="manual">직접 입력</SelectItem>
|
||||
{apiConnections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||
{conn.connection_name}
|
||||
{conn.description && <span className="ml-2 text-xs text-gray-500">({conn.description})</span>}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-500">외부 커넥션 관리에서 저장한 REST API 설정을 불러올 수 있습니다</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* API URL */}
|
||||
<Card className="space-y-4 p-4">
|
||||
<div>
|
||||
@@ -230,13 +356,6 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">GET 요청을 보낼 API 엔드포인트</p>
|
||||
</div>
|
||||
|
||||
{/* HTTP 메서드 (고정) */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700">HTTP 메서드</Label>
|
||||
<div className="mt-2 rounded border border-gray-300 bg-gray-100 p-2 text-sm text-gray-700">GET (고정)</div>
|
||||
<p className="mt-1 text-xs text-gray-500">데이터 조회는 GET 메서드만 지원합니다</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 쿼리 파라미터 */}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user