diff --git a/apps/wr/app/(with-layout)/jwttest/page.tsx b/apps/wr/app/(with-layout)/jwttest/page.tsx index c9df447ef..e7671bc25 100644 --- a/apps/wr/app/(with-layout)/jwttest/page.tsx +++ b/apps/wr/app/(with-layout)/jwttest/page.tsx @@ -1,220 +1,222 @@ /** - * JWT를 decoding하면 만료시간을 읽을 수 있다. - * JWT는 3부분으로 구성 : Header.Payload.Signature - * Payload 부분에 만료시간(exp)이 있다. + * 티베로(하수도정보시스템) 조회 콘솔 + * + * - reuse-service 의 티베로 데이터소스로 조회 SQL 한 문장(SELECT, WITH)을 실행하고 결과를 표로 보여준다. + * - 서버에서 조회 문장만 통과시키고, 읽기 전용 연결과 최대 행 수(기본 100, 최대 1000) 제한을 적용한다. + * - 서버 설정 tibero.sql-console.enabled=true 일 때만 동작한다. + * + * 🔗 사용 API + * 티베로 조회 SQL 실행 POST /ones/sql body: { sql, maxRows } + * + * 이전 JWT 만료시간 테스트 화면은 같은 폴더의 page.tsx.bak 에 보관되어 있다. */ "use client"; -import { useEffect, useRef, useState } from "react"; -import { authService } from "@/src/entities/auth"; -import { CLAIM_NAME } from "@/src/shared/config"; -import { getCookie } from "@/src/shared/lib"; +import { useCallback, useMemo, useState } from "react"; +import { fetchType, postFectch } from "@/components/lib/api"; -interface JwtPayload { - sub?: string; // 토큰 subject. 일반적으로 사용자 식별자나 주체 정보 - exp?: number; // 만료 시각(Unix timestamp, 초 단위) - iat?: number; // 토큰 발급 시각(Unix timestamp, 초 단위) - [key: string]: unknown; // 그 외 JWT payload에 포함될 수 있는 추가 클레임 -} - -interface JwtState { - token: string; // 쿠키에서 읽어온 JWT 원본 문자열 - payload: JwtPayload | null; // JWT를 파싱한 payload 객체 - exp: number | null; // payload에서 추출한 exp 값 - expDateLocal: string | null; // exp를 로컬 시간 문자열로 변환한 값 - remainingMs: number | null; // 현재 시각 기준 남은 시간(ms) - error: string | null; // 쿠키 없음, 파싱 실패 등 오류 메시지 -} - -const initialJwtState: JwtState = { - token: "", - payload: null, - exp: null, - expDateLocal: null, - remainingMs: null, - error: null, +type TiberoSqlResult = { + success: boolean; + message: string | null; + columns: string[]; + rows: (string | null)[][]; + rowCount: number; + truncated: boolean; + maxRows: number; + elapsedMs: number; }; -function parseJwt(token: string): JwtPayload { - const parts = token.split("."); - if (parts.length !== 3) { - throw new Error("유효한 JWT 형식이 아닙니다."); - } +type TiberoSqlResponse = { + status: number; + data: TiberoSqlResult | null; + count: number; + msg: string | null; +}; - const base64Url = parts[1] ?? ""; // Payload를 가져온다. - const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); - const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); - const jsonPayload = decodeURIComponent( - atob(padded) - .split("") - .map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`) - .join("") - ); +const DEFAULT_SQL = `SELECT BSC_FCT_CD, BSC_FCT_NM, RGN_CD, ADST_DSTR_CD, FCT_CPC +FROM ONSEMIRO_NEW.OM_MS_VW +WHERE ROWNUM <= 20`; - return JSON.parse(jsonPayload) as JwtPayload; -} +const MAX_ROWS_LIMIT = 1000; -function formatRemainingMs(remainingMs: number | null): string { - if (remainingMs === null) return "-"; - if (remainingMs <= 0) return "만료됨"; +const toTsv = (columns: string[], rows: (string | null)[][]) => + [columns, ...rows] + .map((row) => row.map((value) => (value ?? "").replace(/[\t\r\n]+/g, " ")).join("\t")) + .join("\n"); - const totalSeconds = Math.floor(remainingMs / 1000); - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; +export default function TiberoSqlConsolePage() { + const [sql, setSql] = useState(DEFAULT_SQL); + const [maxRows, setMaxRows] = useState(100); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); - return `${hours}시간 ${minutes}분 ${seconds}초`; -} - -function readJwtState(): JwtState { - const token = getCookie(CLAIM_NAME) ?? ""; - - if (!token) { - return { - token: "", - payload: null, - exp: null, - expDateLocal: null, - remainingMs: null, - error: `${CLAIM_NAME} 쿠키가 없습니다.`, - }; - } - - try { - const payload = parseJwt(token); - - if (typeof payload.exp !== "number") { - return { - token, - payload, - exp: null, - expDateLocal: null, - remainingMs: null, - error: "JWT payload에 exp 값이 없습니다.", - }; + const runQuery = useCallback(async () => { + if (loading) return; + if (!sql.trim()) { + setError("SQL을 입력해주세요."); + return; } - - const expMs = payload.exp * 1000; - - return { - token, - payload, - exp: payload.exp, - expDateLocal: new Date(expMs).toLocaleString(), - remainingMs: Math.max(expMs - Date.now(), 0), - error: null, - }; - } catch (error) { - return { - token, - payload: null, - exp: null, - expDateLocal: null, - remainingMs: null, - error: - error instanceof Error - ? error.message - : "JWT 파싱 중 오류가 발생했습니다.", - }; - } -} - -export default function Page() { - const [jwtState, setJwtState] = useState(initialJwtState); - const isRefreshingRef = useRef(false); - - const handleSilentRefresh = async () => { + setLoading(true); + setError(null); + setCopied(false); try { - await authService.silentRefresh(); - } catch (error) { - console.error("Manual silent refresh failed:", error); + const response = (await postFectch( + "/ones/sql", + { sql, maxRows }, + fetchType.WR + )) as TiberoSqlResponse; + const data = response?.data ?? null; + setResult(data); + if (!data || !data.success) { + setError(response?.msg || data?.message || "조회에 실패했습니다."); + } + } catch (e) { + setResult(null); + setError(e instanceof Error ? e.message : "요청 중 오류가 발생했습니다."); } finally { - setJwtState(readJwtState()); + setLoading(false); } - }; + }, [loading, sql, maxRows]); - useEffect(() => { - const syncJwtState = async () => { - const nextState = readJwtState(); + const copyTsv = useCallback(async () => { + if (!result?.success) return; + try { + await navigator.clipboard.writeText(toTsv(result.columns, result.rows)); + setCopied(true); + } catch { + setError("클립보드 복사에 실패했습니다."); + } + }, [result]); - if ( - nextState.remainingMs !== null && - nextState.remainingMs > 0 && - nextState.remainingMs < 5 * 60 * 1000 && - !isRefreshingRef.current - ) { - isRefreshingRef.current = true; - try { - await authService.silentRefresh(); - } catch (error) { - console.error("JWT test silent refresh failed:", error); - } - } - - setJwtState(readJwtState()); - - if ( - nextState.remainingMs === null || - nextState.remainingMs >= 5 * 60 * 1000 || - nextState.remainingMs <= 0 - ) { - isRefreshingRef.current = false; - } - }; - - void syncJwtState(); - const timer = window.setInterval(() => { - void syncJwtState(); - }, 1000); - - return () => window.clearInterval(timer); - }, []); + const summary = useMemo(() => { + if (!result?.success) return null; + return `${result.rowCount.toLocaleString()}행 · ${result.elapsedMs.toLocaleString()}ms · 최대 ${result.maxRows.toLocaleString()}행`; + }, [result]); return ( -
-

JWT Exp Test

+
+
+

티베로 조회 콘솔

+ + SELECT, WITH 한 문장만 실행됩니다 · Ctrl+Enter 실행 + +
-