From a7ef3fe4f7f3702d8c8db4b78e5de91f4f38e03b Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 10 Jul 2025 09:52:56 +0000 Subject: (김준회) 나준규 프로 DB조회 우회 요청사항 (임시) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/admin/temp-db-viewer/actions.ts | 65 ++++++++++++++ app/[lng]/admin/temp-db-viewer/page.tsx | 141 ++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 app/[lng]/admin/temp-db-viewer/actions.ts create mode 100644 app/[lng]/admin/temp-db-viewer/page.tsx (limited to 'app') diff --git a/app/[lng]/admin/temp-db-viewer/actions.ts b/app/[lng]/admin/temp-db-viewer/actions.ts new file mode 100644 index 00000000..753c1b28 --- /dev/null +++ b/app/[lng]/admin/temp-db-viewer/actions.ts @@ -0,0 +1,65 @@ +"use server" + +/** + * 사적이고 우회적인 요청사항... + +-- readonly 사용자 생성 (비밀번호 설정) +CREATE USER readonly WITH PASSWORD 'tempReadOnly_123'; + + +-- evcp 데이터베이스에 연결할 수 있는 권한 부여 +GRANT CONNECT ON DATABASE evcp TO readonly; + +-- 조회할 스키마 사용 권한 부여 +GRANT USAGE ON SCHEMA public TO readonly; +GRANT USAGE ON SCHEMA soap TO readonly; +GRANT USAGE ON SCHEMA nonsap TO readonly; +GRANT USAGE ON SCHEMA mdg TO readonly; + +-- 기존 모든 테이블에 대한 SELECT 권한 부여 +GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly; +GRANT SELECT ON ALL TABLES IN SCHEMA soap TO readonly; +GRANT SELECT ON ALL TABLES IN SCHEMA nonsap TO readonly; +GRANT SELECT ON ALL TABLES IN SCHEMA mdg TO readonly; + + */ + +import { Pool } from "pg" + +export interface QueryResultState { + columns: string[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rows: Record[] + error?: string +} + +export async function executeSqlAction( + prevState: QueryResultState, + formData: FormData +): Promise { + const query = (formData.get("query") as string | null) ?? "" + + if (!query.trim()) { + return { ...prevState, error: "쿼리를 입력해주세요." } + } + + try { + const connectionString = process.env.READONLY_DB_URL + + if (!connectionString) { + return { ...prevState, error: "READONLY_DB_URL 환경변수가 설정되지 않았습니다." } + } + + const pool = new Pool({ connectionString }) + const result = await pool.query(query) + await pool.end() + + return { + columns: result.fields.map((f) => f.name), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rows: result.rows as Record[], + } + } catch (err) { + return { ...prevState, error: (err as Error).message } + } +} \ No newline at end of file diff --git a/app/[lng]/admin/temp-db-viewer/page.tsx b/app/[lng]/admin/temp-db-viewer/page.tsx new file mode 100644 index 00000000..6692e63e --- /dev/null +++ b/app/[lng]/admin/temp-db-viewer/page.tsx @@ -0,0 +1,141 @@ +"use client"; + +import * as React from "react"; +import { useActionState, useState } from "react"; +import { executeSqlAction, type QueryResultState } from "./actions"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; + +// CSV 변환 유틸 +function convertToCSV(columns: string[], rows: Record[]): string { + const escape = (value: any) => { + if (value === null || value === undefined) return ""; + const str = String(value).replace(/"/g, '""'); + return `"${str}"`; + }; + + const header = columns.map(escape).join(","); + const lines = rows.map((row) => + columns.map((col) => escape(row[col])).join(",") + ); + return [header, ...lines].join("\r\n"); +} +// ──────────────────────────────────────────────────────────────────────────────── +// Main page component +// ──────────────────────────────────────────────────────────────────────────────── + +export default function SqlEditorPage() { + const [query, setQuery] = useState(""); + + const initialState: QueryResultState = { + columns: [], + rows: [], + }; + + // useActionState: 서버 액션과 클라이언트 상태 연결 + const [state, formAction, isPending] = useActionState< + QueryResultState, + FormData + >(executeSqlAction, initialState); + + // CSV 내보내기 핸들러 + const handleExportCSV = React.useCallback(() => { + if (state.rows.length === 0) { + toast.info("내보낼 결과가 없습니다."); + return; + } + + const csv = convertToCSV(state.columns, state.rows); + const blob = new Blob([csv], { type: "text/csv;charset=euc-kr;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "query_result.csv"; + link.click(); + URL.revokeObjectURL(url); + }, [state.columns, state.rows]); + + // 오류 toast 표시 + React.useEffect(() => { + if (state.error) { + toast.error(state.error); + } + }, [state.error]); + + return ( +
+ {/* 상단: 쿼리 입력 영역 */} +
+