Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs 2025-11-12 15:12:15 +09:00
commit 41404e021e
2 changed files with 63 additions and 46 deletions

View File

@ -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">

View File

@ -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}>