Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
41404e021e
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { useState, useEffect, ChangeEvent } from "react";
|
import { useState, useEffect, ChangeEvent } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogDescription } from "@/components/ui/resizable-dialog";
|
import {
|
||||||
|
ResizableDialog,
|
||||||
|
ResizableDialogContent,
|
||||||
|
ResizableDialogHeader,
|
||||||
|
ResizableDialogTitle,
|
||||||
|
ResizableDialogDescription,
|
||||||
|
} from "@/components/ui/resizable-dialog";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
@ -119,21 +125,20 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
|
|
||||||
// SELECT 쿼리만 허용하는 검증
|
// SELECT 쿼리만 허용하는 검증
|
||||||
const trimmedQuery = query.trim().toUpperCase();
|
const trimmedQuery = query.trim().toUpperCase();
|
||||||
if (!trimmedQuery.startsWith('SELECT')) {
|
if (!trimmedQuery.startsWith("SELECT")) {
|
||||||
toast({
|
toast({
|
||||||
title: "보안 오류",
|
title: "보안 오류",
|
||||||
description: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다. INSERT, UPDATE, DELETE는 허용되지 않습니다.",
|
description:
|
||||||
|
"외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다. INSERT, UPDATE, DELETE는 허용되지 않습니다.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 위험한 키워드 검사
|
// 위험한 키워드 검사
|
||||||
const dangerousKeywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXEC', 'EXECUTE'];
|
const dangerousKeywords = ["INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE", "EXEC", "EXECUTE"];
|
||||||
const hasDangerousKeyword = dangerousKeywords.some(keyword =>
|
const hasDangerousKeyword = dangerousKeywords.some((keyword) => trimmedQuery.includes(keyword));
|
||||||
trimmedQuery.includes(keyword)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasDangerousKeyword) {
|
if (hasDangerousKeyword) {
|
||||||
toast({
|
toast({
|
||||||
title: "보안 오류",
|
title: "보안 오류",
|
||||||
|
|
@ -161,13 +166,13 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("쿼리 실행 오류:", error);
|
console.error("쿼리 실행 오류:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "오류",
|
title: "오류",
|
||||||
description: error instanceof Error ? error.message : "쿼리 실행 중 오류가 발생했습니다.",
|
description: error instanceof Error ? error.message : "쿼리 실행 중 오류가 발생했습니다.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +187,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
데이터베이스에 대해 SQL SELECT 쿼리를 실행하고 결과를 확인할 수 있습니다.
|
데이터베이스에 대해 SQL SELECT 쿼리를 실행하고 결과를 확인할 수 있습니다.
|
||||||
</ResizableDialogDescription>
|
</ResizableDialogDescription>
|
||||||
</ResizableDialogHeader>
|
</ResizableDialogHeader>
|
||||||
|
|
||||||
{/* 쿼리 입력 영역 */}
|
{/* 쿼리 입력 영역 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -220,18 +225,18 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 정보 */}
|
{/* 테이블 정보 */}
|
||||||
<div className="rounded-md border bg-muted/50 p-4 space-y-4">
|
<div className="bg-muted/50 space-y-4 rounded-md border p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-medium text-sm">사용 가능한 테이블</h3>
|
<h3 className="mb-2 text-sm font-medium">사용 가능한 테이블</h3>
|
||||||
<div className="max-h-[200px] overflow-y-auto">
|
<div className="max-h-[200px] overflow-y-auto">
|
||||||
<div className="space-y-2 pr-2">
|
<div className="space-y-2 pr-2">
|
||||||
{tables.map((table) => (
|
{tables.map((table) => (
|
||||||
<div key={table.table_name} className="rounded-lg border bg-card p-3 shadow-sm">
|
<div key={table.table_name} className="bg-card rounded-lg border p-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-mono font-bold text-sm">{table.table_name}</h4>
|
<h4 className="font-mono text-sm font-bold">{table.table_name}</h4>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedTable(table.table_name);
|
setSelectedTable(table.table_name);
|
||||||
loadTableColumns(table.table_name);
|
loadTableColumns(table.table_name);
|
||||||
|
|
@ -243,7 +248,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{table.description && (
|
{table.description && (
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{table.description}</p>
|
<p className="text-muted-foreground mt-1 text-sm">{table.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -254,12 +259,12 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
{/* 선택된 테이블의 컬럼 정보 */}
|
{/* 선택된 테이블의 컬럼 정보 */}
|
||||||
{selectedTable && (
|
{selectedTable && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-medium text-sm">테이블 컬럼 정보: {selectedTable}</h3>
|
<h3 className="mb-2 text-sm font-medium">테이블 컬럼 정보: {selectedTable}</h3>
|
||||||
{loadingColumns ? (
|
{loadingColumns ? (
|
||||||
<div className="text-sm text-muted-foreground">컬럼 정보 로딩 중...</div>
|
<div className="text-muted-foreground text-sm">컬럼 정보 로딩 중...</div>
|
||||||
) : selectedTableColumns.length > 0 ? (
|
) : selectedTableColumns.length > 0 ? (
|
||||||
<div className="max-h-[200px] overflow-y-auto">
|
<div className="max-h-[200px] overflow-y-auto">
|
||||||
<div className="rounded-lg border bg-card shadow-sm">
|
<div className="bg-card rounded-lg border shadow-sm">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -275,7 +280,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
<TableCell className="font-mono font-medium">{column.column_name}</TableCell>
|
<TableCell className="font-mono font-medium">{column.column_name}</TableCell>
|
||||||
<TableCell className="text-sm">{column.data_type}</TableCell>
|
<TableCell className="text-sm">{column.data_type}</TableCell>
|
||||||
<TableCell className="text-sm">{column.is_nullable}</TableCell>
|
<TableCell className="text-sm">{column.is_nullable}</TableCell>
|
||||||
<TableCell className="text-sm">{column.column_default || '-'}</TableCell>
|
<TableCell className="text-sm">{column.column_default || "-"}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
@ -283,7 +288,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-muted-foreground">컬럼 정보를 불러올 수 없습니다.</div>
|
<div className="text-muted-foreground text-sm">컬럼 정보를 불러올 수 없습니다.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -316,20 +321,24 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
{/* 결과 섹션 */}
|
{/* 결과 섹션 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-muted-foreground text-sm">
|
||||||
{loading ? "쿼리 실행 중..." : results.length > 0 ? `${results.length}개의 결과가 있습니다.` : "실행된 쿼리가 없습니다."}
|
{loading
|
||||||
|
? "쿼리 실행 중..."
|
||||||
|
: results.length > 0
|
||||||
|
? `${results.length}개의 결과가 있습니다.`
|
||||||
|
: "실행된 쿼리가 없습니다."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 결과 그리드 */}
|
{/* 결과 그리드 */}
|
||||||
<div className="rounded-md border bg-card">
|
<div className="bg-card rounded-md border">
|
||||||
<div className="max-h-[300px] overflow-y-auto">
|
<div className="max-h-[300px] overflow-y-auto">
|
||||||
<div className="inline-block min-w-full align-middle">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
{results.length > 0 ? (
|
{results.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<TableHeader className="sticky top-0 z-10 bg-card">
|
<TableHeader className="bg-card sticky top-0 z-10">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{Object.keys(results[0]).map((key) => (
|
{Object.keys(results[0]).map((key) => (
|
||||||
<TableHead key={key} className="font-mono font-bold">
|
<TableHead key={key} className="font-mono font-bold">
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,21 @@ import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSel
|
||||||
import { DashboardProvider } from "@/contexts/DashboardContext";
|
import { DashboardProvider } from "@/contexts/DashboardContext";
|
||||||
import { useMenu } from "@/contexts/MenuContext";
|
import { useMenu } from "@/contexts/MenuContext";
|
||||||
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import {
|
||||||
|
ResizableDialog,
|
||||||
|
ResizableDialogContent,
|
||||||
|
ResizableDialogDescription,
|
||||||
|
ResizableDialogHeader,
|
||||||
|
ResizableDialogTitle,
|
||||||
|
} from "@/components/ui/resizable-dialog";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertResizableDialogContent,
|
AlertDialogContent,
|
||||||
AlertResizableDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertResizableDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -610,21 +616,23 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 저장 성공 모달 */}
|
{/* 저장 성공 모달 */}
|
||||||
<Dialog
|
<ResizableDialog
|
||||||
open={successModalOpen}
|
open={successModalOpen}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
setSuccessModalOpen(false);
|
setSuccessModalOpen(false);
|
||||||
router.push("/admin/dashboard");
|
router.push("/admin/dashboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="sm:max-w-md">
|
<ResizableDialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<ResizableDialogHeader>
|
||||||
<div className="bg-success/10 mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
<div className="bg-success/10 mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<CheckCircle2 className="text-success h-6 w-6" />
|
<CheckCircle2 className="text-success h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<DialogTitle className="text-center">저장 완료</DialogTitle>
|
<ResizableDialogTitle className="text-center">저장 완료</ResizableDialogTitle>
|
||||||
<DialogDescription className="text-center">대시보드가 성공적으로 저장되었습니다.</DialogDescription>
|
<ResizableDialogDescription className="text-center">
|
||||||
</DialogHeader>
|
대시보드가 성공적으로 저장되었습니다.
|
||||||
|
</ResizableDialogDescription>
|
||||||
|
</ResizableDialogHeader>
|
||||||
<div className="flex justify-center pt-4">
|
<div className="flex justify-center pt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -635,8 +643,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
확인
|
확인
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</ResizableDialogContent>
|
||||||
</Dialog>
|
</ResizableDialog>
|
||||||
|
|
||||||
{/* 초기화 확인 모달 */}
|
{/* 초기화 확인 모달 */}
|
||||||
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
|
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue