Compare commits
No commits in common. "f046493960616ca57fab412340047662cab3a81e" and "fef2f4a132e019d4facf703f68b1bf95582b875e" have entirely different histories.
f046493960
...
fef2f4a132
|
|
@ -2,13 +2,7 @@
|
||||||
|
|
||||||
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 {
|
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogDescription } from "@/components/ui/resizable-dialog";
|
||||||
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";
|
||||||
|
|
@ -125,19 +119,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:
|
description: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다. INSERT, UPDATE, DELETE는 허용되지 않습니다.",
|
||||||
"외부 데이터베이스에서는 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) => trimmedQuery.includes(keyword));
|
const hasDangerousKeyword = dangerousKeywords.some(keyword =>
|
||||||
|
trimmedQuery.includes(keyword)
|
||||||
|
);
|
||||||
|
|
||||||
if (hasDangerousKeyword) {
|
if (hasDangerousKeyword) {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -225,15 +220,15 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 정보 */}
|
{/* 테이블 정보 */}
|
||||||
<div className="bg-muted/50 space-y-4 rounded-md border p-4">
|
<div className="rounded-md border bg-muted/50 p-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 text-sm font-medium">사용 가능한 테이블</h3>
|
<h3 className="mb-2 font-medium text-sm">사용 가능한 테이블</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="bg-card rounded-lg border p-3 shadow-sm">
|
<div key={table.table_name} className="rounded-lg border bg-card p-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-mono text-sm font-bold">{table.table_name}</h4>
|
<h4 className="font-mono font-bold text-sm">{table.table_name}</h4>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -248,7 +243,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{table.description && (
|
{table.description && (
|
||||||
<p className="text-muted-foreground mt-1 text-sm">{table.description}</p>
|
<p className="mt-1 text-sm text-muted-foreground">{table.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -259,12 +254,12 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
{/* 선택된 테이블의 컬럼 정보 */}
|
{/* 선택된 테이블의 컬럼 정보 */}
|
||||||
{selectedTable && (
|
{selectedTable && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 text-sm font-medium">테이블 컬럼 정보: {selectedTable}</h3>
|
<h3 className="mb-2 font-medium text-sm">테이블 컬럼 정보: {selectedTable}</h3>
|
||||||
{loadingColumns ? (
|
{loadingColumns ? (
|
||||||
<div className="text-muted-foreground text-sm">컬럼 정보 로딩 중...</div>
|
<div className="text-sm text-muted-foreground">컬럼 정보 로딩 중...</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="bg-card rounded-lg border shadow-sm">
|
<div className="rounded-lg border bg-card shadow-sm">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -280,7 +275,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>
|
||||||
|
|
@ -288,7 +283,7 @@ export const SqlQueryModal: React.FC<SqlQueryModalProps> = ({ isOpen, onClose, c
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground text-sm">컬럼 정보를 불러올 수 없습니다.</div>
|
<div className="text-sm text-muted-foreground">컬럼 정보를 불러올 수 없습니다.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -321,24 +316,20 @@ 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-muted-foreground text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
{loading
|
{loading ? "쿼리 실행 중..." : results.length > 0 ? `${results.length}개의 결과가 있습니다.` : "실행된 쿼리가 없습니다."}
|
||||||
? "쿼리 실행 중..."
|
|
||||||
: results.length > 0
|
|
||||||
? `${results.length}개의 결과가 있습니다.`
|
|
||||||
: "실행된 쿼리가 없습니다."}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 결과 그리드 */}
|
{/* 결과 그리드 */}
|
||||||
<div className="bg-card rounded-md border">
|
<div className="rounded-md border bg-card">
|
||||||
<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="bg-card sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10 bg-card">
|
||||||
<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,21 +12,15 @@ 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 {
|
import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
ResizableDialog,
|
|
||||||
ResizableDialogContent,
|
|
||||||
ResizableDialogDescription,
|
|
||||||
ResizableDialogHeader,
|
|
||||||
ResizableDialogTitle,
|
|
||||||
} from "@/components/ui/resizable-dialog";
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertResizableDialogContent,
|
||||||
AlertDialogDescription,
|
AlertResizableDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertResizableDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -616,23 +610,21 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 저장 성공 모달 */}
|
{/* 저장 성공 모달 */}
|
||||||
<ResizableDialog
|
<Dialog
|
||||||
open={successModalOpen}
|
open={successModalOpen}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
setSuccessModalOpen(false);
|
setSuccessModalOpen(false);
|
||||||
router.push("/admin/dashboard");
|
router.push("/admin/dashboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResizableDialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<ResizableDialogHeader>
|
<DialogHeader>
|
||||||
<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>
|
||||||
<ResizableDialogTitle className="text-center">저장 완료</ResizableDialogTitle>
|
<DialogTitle className="text-center">저장 완료</DialogTitle>
|
||||||
<ResizableDialogDescription className="text-center">
|
<DialogDescription className="text-center">대시보드가 성공적으로 저장되었습니다.</DialogDescription>
|
||||||
대시보드가 성공적으로 저장되었습니다.
|
</DialogHeader>
|
||||||
</ResizableDialogDescription>
|
|
||||||
</ResizableDialogHeader>
|
|
||||||
<div className="flex justify-center pt-4">
|
<div className="flex justify-center pt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -643,8 +635,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
확인
|
확인
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ResizableDialogContent>
|
</DialogContent>
|
||||||
</ResizableDialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 초기화 확인 모달 */}
|
{/* 초기화 확인 모달 */}
|
||||||
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
|
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue