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