From d005bb5ada5e84168697eb583d895a40bf8bf788 Mon Sep 17 00:00:00 2001 From: son0131 Date: Tue, 19 Aug 2025 20:45:16 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix/#52=20ERD=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DevTools/ERD/edge/ERDEdge.jsx | 34 +++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/components/DevTools/ERD/edge/ERDEdge.jsx b/src/components/DevTools/ERD/edge/ERDEdge.jsx index 1948745..aeaf29a 100644 --- a/src/components/DevTools/ERD/edge/ERDEdge.jsx +++ b/src/components/DevTools/ERD/edge/ERDEdge.jsx @@ -1,6 +1,6 @@ // src/components/DevTools/ERD/edge/ERDEdge.jsx import React from 'react'; -import { BaseEdge, EdgeLabelRenderer, getBezierPath, useStore } from 'reactflow'; +import { useStore } from 'reactflow'; import { O_Crow, O_Bar, @@ -24,8 +24,8 @@ const pickIcon = (card) => { }; const ICON_HALF = 12; -const PAD = -14; // 0~1px 미세조정 (경계 겹침/틈 방지) -const STUB_INSET = 0; // 왼쪽 짧은 막대가 아이콘 왼쪽 가장자리에서 안쪽으로 들어온 거리(px) +const PAD = -15; // 0~1px 미세조정 (경계 겹침/틈 방지) +const STUB_INSET = 2; // 왼쪽 짧은 막대가 아이콘 왼쪽 가장자리에서 안쪽으로 들어온 거리(px) // 각도(deg)만큼 회전한 아이콘 로컬 좌표를 월드 좌표로 변환 function rotateLocalToWorld(cx, cy, deg, lx, ly) { @@ -108,22 +108,6 @@ export default function ErdEdge(props) { const sPt = getIntersectionOnRect(sRect, sc, tc); const tPt = getIntersectionOnRect(tRect, tc, sc); - const dx = tPt.x - sPt.x; - const dy = tPt.y - sPt.y; - const horizontalDominant = Math.abs(dx) >= Math.abs(dy); - - let sourceIconDeg, targetIconDeg; - if (horizontalDominant) { - // 좌↔우 배치: 서로 마주보게 (← →) - sourceIconDeg = dx >= 0 ? 180 : 0; - targetIconDeg = dx >= 0 ? 0 : 180; - } else { - // 위↕아래 배치: 서로 마주보게 (↑ ↓) - // dy>0: 타깃이 아래 => 소스는 ↓(90°), 타깃은 ↑(-90°) - sourceIconDeg = dy >= 0 ? -90 : 90; - targetIconDeg = dy >= 0 ? 90 : -90; - } - function whichSide(rect, p) { const left = Math.abs(p.x - rect.x); const right = Math.abs(rect.x + rect.w - p.x); @@ -136,10 +120,10 @@ export default function ErdEdge(props) { return 'B'; } const sideInfo = { - L: { nx: -1, ny: 0, deg: 180 }, - R: { nx: 1, ny: 0, deg: 0 }, - T: { nx: 0, ny: -1, deg: -90 }, - B: { nx: 0, ny: 1, deg: 90 }, + L: { nx: -1, ny: 0, deg: 0 }, + R: { nx: 1, ny: 0, deg: 180 }, + T: { nx: 0, ny: -1, deg: 90 }, + B: { nx: 0, ny: 1, deg: -90 }, }; // 접점이 위치한 변 구하기 @@ -148,6 +132,10 @@ const sideInfo = { const sN = sideInfo[sSide]; const tN = sideInfo[tSide]; + // ✅ 아이콘 회전을 '붙은 변의 각도'로 고정 + const sourceIconDeg = sN.deg; + const targetIconDeg = tN.deg; + // 아이콘 중심 = 접점에서 바깥쪽(법선)으로 ICON_HALF + PAD 만큼 const sourceIconX = sPt.x + sN.nx * (ICON_HALF + PAD); const sourceIconY = sPt.y + sN.ny * (ICON_HALF + PAD); From 1885f4d55c3d3206b31e3f76dde6d9b6ac54d152 Mon Sep 17 00:00:00 2001 From: son0131 Date: Tue, 19 Aug 2025 21:17:35 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix/=20#52=20github=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Github/GithubManagerModal.jsx | 2 +- src/components/Github/GithubUpdateForm.jsx | 100 +++++++++++++------ 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/src/components/Github/GithubManagerModal.jsx b/src/components/Github/GithubManagerModal.jsx index 2c8a144..60239cc 100644 --- a/src/components/Github/GithubManagerModal.jsx +++ b/src/components/Github/GithubManagerModal.jsx @@ -114,7 +114,7 @@ export default function GithubManagerModal({ teamCode, onClose }) { )} {tab === 'delete' && (
-

현재 등록된 GitHub 설정을 삭제합니다. 이 작업은 되돌릴 수 없습니다.

+

현재 등록된 GitHub 설정을 삭제합니다. 이 작업은 되돌릴 수 없습니다.

- +
+ {/* 저장소 URL */} + - + {/* 기본 브랜치 */} + - + {/* Private 체크박스 */} + + {/* 토큰 */} + - + {/* 액션 버튼 */} +
+ + +
-
- - -
+ {/* 에러 표시 */} + {data && !data.success && ( +

+ {data.error?.code} · {data.error?.message} +

+ )} + {error &&

네트워크 오류

} +
- {data && !data.success &&

{data.error?.code} · {data.error?.message}

} - {error &&

네트워크 오류

} - ); } From 8135ebae91506a83e2cba572edc9f36063ea78e9 Mon Sep 17 00:00:00 2001 From: son0131 Date: Wed, 20 Aug 2025 20:31:02 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix=20/#52=20=EB=A1=9C=EA=B7=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/apis/log.js | 19 + src/api/hooks/log.js | 132 ++++++ src/api/utils/axios.jsx | 1 + src/pages/Home.jsx | 2 +- src/pages/LogMonitoring.jsx | 412 +++++++++++++++--- src/styles/css/AddServerModal.css | 10 +- src/styles/css/CreateTableModal.css | 14 - src/styles/css/Dashboard.css | 1 + src/styles/css/LogMonitoring.css | 20 + src/styles/css/login.css | 2 +- src/styles/css/sideBar.css | 1 - src/styles/scss/auth/login.scss | 2 +- .../ServerManagement/AddServerModal.scss | 37 ++ src/styles/scss/components/sideBar.scss | 8 + src/styles/scss/pages/LogMonitoring.scss | 22 +- 15 files changed, 607 insertions(+), 76 deletions(-) create mode 100644 src/api/apis/log.js create mode 100644 src/api/hooks/log.js diff --git a/src/api/apis/log.js b/src/api/apis/log.js new file mode 100644 index 0000000..5eee9a2 --- /dev/null +++ b/src/api/apis/log.js @@ -0,0 +1,19 @@ +import api from "../utils/axios.jsx"; + +// log.js +// REST용 +const toRestPath = (t) => + ({ BACKEND: "backend", FRONTEND: "frontend", RDS: "rds" }[t] || + String(t || "").toLowerCase()); + +export const registerLog = (sourceType, teamCode) => + api.post(`/api/log/register/${toRestPath(sourceType)}/${teamCode}`); + +export const queryRange = (sourceType, teamCode, from, to) => + api.get('/api/log/query/range', { params: { sourceType, teamCode, from, to } }); + +export const queryContext = (sourceType, teamCode, level, context = 50) => + api.get('/api/log/query/context', { params: { sourceType, teamCode, level, context } }); + +export const queryStatus = (sourceType, teamCode) => + api.get('/api/log/query/status', { params: { sourceType, teamCode } }); diff --git a/src/api/hooks/log.js b/src/api/hooks/log.js new file mode 100644 index 0000000..b862de3 --- /dev/null +++ b/src/api/hooks/log.js @@ -0,0 +1,132 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +const WS_URL = + import.meta.env.VITE_WS_URL || + ((import.meta.env.VITE_API_URL || '') + .replace(/^http(s?):\/\//, (_, s) => (s ? 'wss://' : 'ws://')) + '/ws/log'); + +const stateMap = { 0: 'connecting', 1: 'open', 2: 'closing', 3: 'closed' }; + +// ✅ WS 쿼리스트링은 서버 계약 그대로 "대문자" 유지 +const toWsParam = (t) => + ({ backend: "BACKEND", frontend: "FRONTEND", rds: "RDS" }[String(t || "").toLowerCase()] || + String(t || "").toUpperCase()); + +// ✅ 훅 함수명은 반드시 use로 시작 +export default function useLogSocket({ teamCode, sourceType }) { + const wsRef = useRef(null); + const [status, setStatus] = useState('closed'); + const [rows, setRows] = useState([]); + const [lastError, setLastError] = useState(null); + + const backoffRef = useRef(1000); + const manualCloseRef = useRef(false); + const reconnectTimerRef = useRef(null); + + const clearReconnectTimer = () => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + }; + + const scheduleReconnect = useCallback(() => { + if (manualCloseRef.current) return; + if (reconnectTimerRef.current) return; + const wait = backoffRef.current; + reconnectTimerRef.current = setTimeout(() => { + reconnectTimerRef.current = null; + connect(); + backoffRef.current = Math.min(backoffRef.current * 1.8, 30000); + }, wait); + }, []); + + const connect = useCallback(() => { + clearReconnectTimer(); + if (!teamCode || !sourceType) return; + + try { wsRef.current?.close(); } catch {} + wsRef.current = null; + + // ★ 계약: 쿼리스트링 방식으로만 연결 + const url = `${WS_URL}?teamCode=${encodeURIComponent(teamCode)}&sourceType=${encodeURIComponent(toWsParam(sourceType))}`; + console.log('[WS URL]', url); + const ws = new WebSocket(url); + + wsRef.current = ws; + setStatus(stateMap[ws.readyState] || 'connecting'); + + ws.onopen = () => { + backoffRef.current = 1000; + setStatus('open'); + // ★ 더 이상 별도 register 메시지 보내지 않음 (쿼리스트링으로 라우팅) + }; + + ws.onmessage = (e) => { + let msg; try { msg = JSON.parse(e.data); } catch { msg = e.data; } + if (!msg || typeof msg !== 'object') return; + + if (msg.type === 'ok' || (msg.type === 'ack' && msg.action === 'register')) return; + + if (msg.type === 'log' && msg.data) { + const r = msg.data; + const local = new Date((r.timestamp || '') + 'Z'); + setRows(prev => [ + { time: isNaN(local) ? r.timestamp : local.toLocaleString(), level: r.level, service: r.service || '-', message: r.message }, + ...prev, + ]); + return; + } + + if (msg.type === 'filter_between_result' && Array.isArray(msg.data)) { + const list = msg.data.map(r => { + const local = new Date((r.timestamp || '') + 'Z'); + return { time: isNaN(local) ? r.timestamp : local.toLocaleString(), level: r.level, service: r.service || '-', message: r.message }; + }); + setRows(prev => [...list.reverse(), ...prev]); + return; + } + + if (msg.type === 'error') { + setLastError({ code: msg.code, message: msg.message }); + } + }; + + ws.onclose = (e) => { + setStatus("closed"); + console.warn("[WS CLOSE]", { code: e.code, reason: e.reason, wasClean: e.wasClean }); + if (!manualCloseRef.current) scheduleReconnect(); + }; + + ws.onerror = (err) => { + setLastError({ code: "WS", message: String(err?.message || "WebSocket error") }); + console.error("[WS ERROR]", err); + }; + }, [teamCode, sourceType, scheduleReconnect]); + + const disconnect = useCallback(() => { + manualCloseRef.current = true; + clearReconnectTimer(); + try { wsRef.current?.close(1000, 'client close'); } catch {} + wsRef.current = null; + setStatus('closed'); + }, []); + + useEffect(() => { + manualCloseRef.current = false; + connect(); + return () => disconnect(); + }, [connect, disconnect]); + + const requestRange = useCallback((from, to) => { + if (!wsRef.current || wsRef.current.readyState !== 1) return; + wsRef.current.send(JSON.stringify({ type: 'filter_between', from, to })); + }, []); + + const requestLevelContext = useCallback((level, context = 50) => { + if (!wsRef.current || wsRef.current.readyState !== 1) return; + wsRef.current.send(JSON.stringify({ type: 'level_context', level, context })); + }, []); + + return { status, rows, lastError, requestRange, requestLevelContext, reconnect: connect, disconnect }; +} diff --git a/src/api/utils/axios.jsx b/src/api/utils/axios.jsx index fa7b02a..3952969 100644 --- a/src/api/utils/axios.jsx +++ b/src/api/utils/axios.jsx @@ -96,4 +96,5 @@ clientAI.interceptors.response.use( } ); +export default client; export { client, clientAI }; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 40f89d9..200c610 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -113,7 +113,7 @@ const Home = () => { {activeTab === "성능 테스트" && ( )}{" "} - {activeTab === "로그 모니터링" && } + {activeTab === "로그 모니터링" && } {/* ✅ 모달은 여기에서 관리 */} diff --git a/src/pages/LogMonitoring.jsx b/src/pages/LogMonitoring.jsx index 426ee6d..5fbedb1 100644 --- a/src/pages/LogMonitoring.jsx +++ b/src/pages/LogMonitoring.jsx @@ -1,68 +1,338 @@ -import React, { useState, useEffect, useRef } from "react"; +// import React, { useState, useEffect, useRef } from "react"; +// import "../styles/css/LogMonitoring.css"; +// import { Pause, Play } from "lucide-react"; + +// const mockLogs = [ +// "[INFO] 서버가 시작되었습니다.", +// "[WARN] 메모리 사용량 증가 감지", +// "[ERROR] 데이터베이스 연결 실패", +// "[INFO] 요청이 정상 처리되었습니다.", +// ]; + +// function LogMonitoring() { +// const [logs, setLogs] = useState([]); +// const [isStreaming, setIsStreaming] = useState(true); +// const [filterLevel, setFilterLevel] = useState("ALL"); +// const logEndRef = useRef(null); + +// // 실제 API 요청 +// // const fetchLogs = async () => { +// // try { +// // const res = await fetch("/logging/live"); +// // if (!res.ok) throw new Error("로그 로딩 실패"); +// // const data = await res.json(); +// // setLogs(data); +// // } catch (err) { +// // console.error("로그 가져오기 실패:", err); +// // } +// // }; + +// useEffect(() => { +// const interval = setInterval(() => { +// if (!isStreaming) return; + +// // 모의 로그 추가 +// setLogs((prev) => { +// const nextLog = mockLogs[Math.floor(Math.random() * mockLogs.length)]; +// return [...prev, nextLog]; +// }); + +// // 실제 API 요청 +// // fetchLogs(); +// }, 2000); + +// return () => clearInterval(interval); +// }, [isStreaming]); + +// useEffect(() => { +// if (logEndRef.current) { +// logEndRef.current.scrollIntoView({ behavior: "smooth" }); +// } +// }, [logs]); + +// const filteredLogs = logs.filter((log) => { +// if (filterLevel === "ALL") return true; +// return log.includes(filterLevel); +// }); + +// return ( +//
+//

CLI 로그 모니터링

+ +//
+// + +// +//
+ +//
+// {filteredLogs.map((log, i) => ( +//
+// {log} +//
+// ))} +// {/* 자동 스크롤 대상 */} +//
+//
+//
+// ); +// } + +// export default LogMonitoring; +// src/pages/LogMonitoring.jsx (또는 현재 경로 유지) +// 기존 파일 전체 교체본 +import React, { useEffect, useMemo, useRef, useState } from "react"; import "../styles/css/LogMonitoring.css"; import { Pause, Play } from "lucide-react"; -const mockLogs = [ - "[INFO] 서버가 시작되었습니다.", - "[WARN] 메모리 사용량 증가 감지", - "[ERROR] 데이터베이스 연결 실패", - "[INFO] 요청이 정상 처리되었습니다.", -]; +// ✅ 네가 만든 훅/REST 래퍼 +import useLogSocket from "@/api/hooks/log"; +import { registerLog, queryStatus } from "@/api/apis/log"; + +// 레벨 옵션 +const LEVELS = ["ALL", "INFO", "WARN", "ERROR"]; + +// 화면에 표시할 한 줄을 문자열로 포맷 (기존 스타일 유지) +function formatLine(row) { + // row: { time, level, service, message } + // 기존 컴포넌트는 문자열 배열을 썼으므로 같은 스타일로 출력 + return `[${row.level}] ${row.service || "-"} — ${row.message}`; +} + +export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { + // 🔧 필요하면 상단 상태/입력으로 컨트롤 + const [sourceType, setSourceType] = useState(defaultSource); // FRONTEND | RDS + + const [rangeStart, setRangeStart] = useState(""); + const [rangeEnd, setRangeEnd] = useState(""); + + const canConnect = !!teamCode; -function LogMonitoring() { - const [logs, setLogs] = useState([]); - const [isStreaming, setIsStreaming] = useState(true); const [filterLevel, setFilterLevel] = useState("ALL"); - const logEndRef = useRef(null); + const [isStreaming, setIsStreaming] = useState(true); // 일시정지/재시작 - // 실제 API 요청 - // const fetchLogs = async () => { - // try { - // const res = await fetch("/logging/live"); - // if (!res.ok) throw new Error("로그 로딩 실패"); - // const data = await res.json(); - // setLogs(data); - // } catch (err) { - // console.error("로그 가져오기 실패:", err); - // } - // }; + // 컴포넌트 내부 + const [statusActive, setStatusActive] = useState(null); // true | false | null + const [statusLoading, setStatusLoading] = useState(false); + const [statusError, setStatusError] = useState(null); useEffect(() => { - const interval = setInterval(() => { - if (!isStreaming) return; + if (!teamCode || !sourceType) { + setStatusActive(null); + setStatusError(null); + setStatusLoading(false); + return; + } + + let stop = false; + const tick = async () => { + // 첫 로딩에만 로딩 표시 + setStatusLoading((prev) => (statusActive === null ? true : prev)); + try { + const { data } = await queryStatus(sourceType, teamCode); + if (stop) return; + setStatusActive(!!data?.active); // true면 "수집 중", false면 "대기" + setStatusError(null); + } catch (err) { + if (stop) return; + const code = err?.response?.data?.code; + if (code === "40710") { + // 서버 약속: 수집 미시작 + setStatusActive(false); + setStatusError(null); + } else { + setStatusError("error"); + } + } finally { + if (!stop) setStatusLoading(false); + } + }; + + tick(); // 즉시 한 번 + const id = setInterval(tick, 15000); // 15초마다 + return () => { + stop = true; + clearInterval(id); + }; + }, [teamCode, sourceType]); + + const StatusBadge = ({ active, loading, error }) => { + let label = "대기"; + let cls = "bg-zinc-600"; + if (loading) { + label = "확인중"; + cls = "bg-gray-500"; + } else if (error) { + label = "오류"; + cls = "bg-red-600"; + } else if (active === true) { + label = "수집 중"; + cls = "bg-green-600"; + } - // 모의 로그 추가 - setLogs((prev) => { - const nextLog = mockLogs[Math.floor(Math.random() * mockLogs.length)]; - return [...prev, nextLog]; - }); + return ( + + + {label} + + ); + }; - // 실제 API 요청 - // fetchLogs(); - }, 2000); + // WS 훅: rows는 [{time, level, service, message}, ...] 최신이 앞 + const { + status, + rows, + lastError, + requestRange, + requestLevelContext, + reconnect, + disconnect, + } = useLogSocket({ teamCode: canConnect ? teamCode : undefined, sourceType }); - return () => clearInterval(interval); - }, [isStreaming]); + const isWsOpen = status === 'open'; + // ⏸ 일시정지를 구현하기 위해 "스냅샷"을 따로 유지 + const [snapshotRows, setSnapshotRows] = useState([]); useEffect(() => { - if (logEndRef.current) { + if (isStreaming) { + setSnapshotRows(rows); + } + }, [rows, isStreaming]); + + // 현재 렌더 대상: 스트리밍 중이면 rows, 일시정지면 snapshotRows + const activeRows = isStreaming ? rows : snapshotRows; + + // 레벨 필터 + const filteredRows = useMemo(() => { + if (filterLevel === "ALL") return activeRows; + return activeRows.filter((r) => r.level === filterLevel); + }, [activeRows, filterLevel]); + + // 문자열로 변환 (기존 렌더 구조 유지) + const filteredLogs = useMemo( + () => filteredRows.map(formatLine), + [filteredRows] + ); + + // 자동 스크롤 + const logEndRef = useRef(null); + useEffect(() => { + if (logEndRef.current && isStreaming) { logEndRef.current.scrollIntoView({ behavior: "smooth" }); } - }, [logs]); + }, [filteredLogs, isStreaming]); + + // REST: 수집 시작 트리거 + const onStartCollect = async () => { + try { + await registerLog(sourceType, teamCode); // 1) HTTP 등록 + reconnect(); // 2) WS 연결 + } catch (e) { + alert('수집 시작 실패: ' + (e?.response?.data?.message || e.message)); + } + }; - const filteredLogs = logs.filter((log) => { - if (filterLevel === "ALL") return true; - return log.includes(filterLevel); - }); + // REST: 상태 + const onCheckStatus = async () => { + try { + const res = await queryStatus(sourceType, teamCode); + alert("active: " + (res?.data?.active ? "true" : "false")); + } catch { + alert("상태 조회 실패"); + } + }; return (

CLI 로그 모니터링

-
+ {/* 상단 컨트롤 */} +
+ + + + 팀: {teamCode || "-"} + + + + + + + +
+ {/* 상태/에러 표시 */} +
+ WS 상태: {status} + {lastError ? ` | 에러: [${lastError.code}] ${lastError.message}` : ""} +
+ + {/* 로그 뷰어 */}
- {filteredLogs.map((log, i) => ( + {filteredRows.map((row, i) => (
- {log} + {formatLine(row)}
))} - {/* 자동 스크롤 대상 */}
+ + {/* (선택) 하단 빠른 조회 버튼 - WS 쿼리 */} +
+ + + + +
); } - -export default LogMonitoring; diff --git a/src/styles/css/AddServerModal.css b/src/styles/css/AddServerModal.css index 66bcd98..742fe79 100644 --- a/src/styles/css/AddServerModal.css +++ b/src/styles/css/AddServerModal.css @@ -108,14 +108,12 @@ cursor: not-allowed; } -/* AddServerModal.css */ - .modal-github-button { padding: 0.5rem 1rem; border-radius: 6px; border: none; cursor: pointer; - background-color: white; + background-color: white; color: black; gap: 10px; border: 1px solid #D9D9D9; @@ -126,10 +124,11 @@ border-radius: 6px; border: none; cursor: pointer; - background-color: red; + background-color: red; color: white; gap: 10px; border: 1px solid #D9D9D9; + width: -moz-fit-content; width: fit-content; display: inline-flex; } @@ -143,6 +142,7 @@ color: white; gap: 10px; border: 1px solid #D9D9D9; + width: -moz-fit-content; width: fit-content; display: inline-flex; -} +} \ No newline at end of file diff --git a/src/styles/css/CreateTableModal.css b/src/styles/css/CreateTableModal.css index ce83f5a..ec859c8 100644 --- a/src/styles/css/CreateTableModal.css +++ b/src/styles/css/CreateTableModal.css @@ -19,20 +19,6 @@ border-radius: 8px; width: 700px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); - max-height: 80vh; - display: flex; - flex-direction: column; -} -.modal-body { - flex: 1 1 auto; - overflow: auto; - -webkit-overflow-scrolling: touch; - cursor: grab; -} - -.modal-body:active -{ - cursor: grabbing; } .modal-title { diff --git a/src/styles/css/Dashboard.css b/src/styles/css/Dashboard.css index 3a8eb8c..262e603 100644 --- a/src/styles/css/Dashboard.css +++ b/src/styles/css/Dashboard.css @@ -47,6 +47,7 @@ background-color: #f1f1f1; padding: 8px 12px; border-radius: 16px; + width: -moz-fit-content; width: fit-content; max-width: 100%; } diff --git a/src/styles/css/LogMonitoring.css b/src/styles/css/LogMonitoring.css index ce8d68a..700b65b 100644 --- a/src/styles/css/LogMonitoring.css +++ b/src/styles/css/LogMonitoring.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; .log-monitoring-container { height: 100%; display: flex; @@ -70,4 +71,23 @@ .log-entry.error { color: #f87171; +} + +input[type=datetime-local] { + background-color: #ffffff; /* 흰색 배경 */ + color: #000000; /* 검정 텍스트 */ + border: 1px solid #ccc; + border-radius: 6px; + padding: 4px 6px; + /* 다크 모드에서도 달력 아이콘 안 사라지게 */ +} +input[type=datetime-local]::-webkit-calendar-picker-indicator { + filter: invert(0); +} + +input[type=datetime-local]:focus { + background-color: #ffffff; + color: #000000; + border-color: #3b82f6; /* 파란색 테두리 */ + outline: none; } \ No newline at end of file diff --git a/src/styles/css/login.css b/src/styles/css/login.css index 8ea0c50..d455703 100644 --- a/src/styles/css/login.css +++ b/src/styles/css/login.css @@ -112,7 +112,7 @@ justify-content: center; font-size: 0.85rem; color: #666; - gap: 60px; + gap: 30px; } .login-page .login-card .login-links a { font-size: 14px; diff --git a/src/styles/css/sideBar.css b/src/styles/css/sideBar.css index 688580f..99cb236 100644 --- a/src/styles/css/sideBar.css +++ b/src/styles/css/sideBar.css @@ -50,7 +50,6 @@ display: flex; flex-direction: column; } - .side-bar .connected-github-header { color: #666666; font-size: 16px; diff --git a/src/styles/scss/auth/login.scss b/src/styles/scss/auth/login.scss index 8b50464..d024d17 100644 --- a/src/styles/scss/auth/login.scss +++ b/src/styles/scss/auth/login.scss @@ -116,7 +116,7 @@ justify-content: center; font-size: 0.85rem; color: #666; - gap: 60px; + gap: 30px; a { @include font-style("body2"); diff --git a/src/styles/scss/components/ServerManagement/AddServerModal.scss b/src/styles/scss/components/ServerManagement/AddServerModal.scss index 7b0b35c..fbcab9c 100644 --- a/src/styles/scss/components/ServerManagement/AddServerModal.scss +++ b/src/styles/scss/components/ServerManagement/AddServerModal.scss @@ -103,4 +103,41 @@ } } } +} + +.modal-github-button { + padding: 0.5rem 1rem; + border-radius: 6px; + border: none; + cursor: pointer; + background-color: white; + color: black; + gap: 10px; + border: 1px solid #D9D9D9; +} + +.github-modal-body .modal-delete-button { + padding: 0.5rem 1rem; + border-radius: 6px; + border: none; + cursor: pointer; + background-color: red; + color: white; + gap: 10px; + border: 1px solid #D9D9D9; + width: fit-content; + display: inline-flex; +} + +.modal-refresh-button { + padding: 0.5rem 1rem; + border-radius: 6px; + border: none; + cursor: pointer; + background-color: #3b82f6; + color: white; + gap: 10px; + border: 1px solid #D9D9D9; + width: fit-content; + display: inline-flex; } \ No newline at end of file diff --git a/src/styles/scss/components/sideBar.scss b/src/styles/scss/components/sideBar.scss index 38f9667..f61f3bd 100644 --- a/src/styles/scss/components/sideBar.scss +++ b/src/styles/scss/components/sideBar.scss @@ -51,6 +51,14 @@ flex-direction: column; } + .connected-github-header{ + color: #666666; + font-size: 16px; + line-height: 24px; + font-weight: 500; + letter-spacing: -2.5%; + margin: 28px 0 -9px 8px; + } } \ No newline at end of file diff --git a/src/styles/scss/pages/LogMonitoring.scss b/src/styles/scss/pages/LogMonitoring.scss index 8355cd9..ada0af0 100644 --- a/src/styles/scss/pages/LogMonitoring.scss +++ b/src/styles/scss/pages/LogMonitoring.scss @@ -71,4 +71,24 @@ .log-entry.error { color: #f87171; - } \ No newline at end of file + } + + input[type='datetime-local'] { + background-color: #ffffff; /* 흰색 배경 */ + color: #000000; /* 검정 텍스트 */ + border: 1px solid #ccc; + border-radius: 6px; + padding: 4px 6px; + + /* 다크 모드에서도 달력 아이콘 안 사라지게 */ + &::-webkit-calendar-picker-indicator { + filter: invert(0); + } +} + +input[type='datetime-local']:focus { + background-color: #ffffff; + color: #000000; + border-color: #3b82f6; /* 파란색 테두리 */ + outline: none; +} \ No newline at end of file From 125d6946d908e296b7991863d830f9218b7cc479 Mon Sep 17 00:00:00 2001 From: son0131 Date: Wed, 20 Aug 2025 22:04:16 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix/#52=20=EB=A1=9C=EA=B7=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=EC=88=98=EC=A0=95(1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/log.js | 60 +++++++++++------- src/pages/LogMonitoring.jsx | 123 +++++++++++++----------------------- 2 files changed, 81 insertions(+), 102 deletions(-) diff --git a/src/api/hooks/log.js b/src/api/hooks/log.js index b862de3..5a83072 100644 --- a/src/api/hooks/log.js +++ b/src/api/hooks/log.js @@ -49,9 +49,8 @@ export default function useLogSocket({ teamCode, sourceType }) { wsRef.current = null; // ★ 계약: 쿼리스트링 방식으로만 연결 - const url = `${WS_URL}?teamCode=${encodeURIComponent(teamCode)}&sourceType=${encodeURIComponent(toWsParam(sourceType))}`; - console.log('[WS URL]', url); - const ws = new WebSocket(url); + console.log('[WS URL]', WS_URL); + const ws = new WebSocket(WS_URL); wsRef.current = ws; setStatus(stateMap[ws.readyState] || 'connecting'); @@ -59,38 +58,51 @@ export default function useLogSocket({ teamCode, sourceType }) { ws.onopen = () => { backoffRef.current = 1000; setStatus('open'); - // ★ 더 이상 별도 register 메시지 보내지 않음 (쿼리스트링으로 라우팅) + // ★ 계약: 연결 직후 register 프레임 전송 + ws.send(JSON.stringify({ + type: 'register', + sourceType: toWsParam(sourceType), // BACKEND/FRONTEND/RDS 대문자 + code: teamCode // ← 키 이름은 code + })); }; ws.onmessage = (e) => { - let msg; try { msg = JSON.parse(e.data); } catch { msg = e.data; } + let msg; try { msg = JSON.parse(e.data); } catch { return; } if (!msg || typeof msg !== 'object') return; + // 1) 등록 ACK 등 제어 프레임은 무시 if (msg.type === 'ok' || (msg.type === 'ack' && msg.action === 'register')) return; - - if (msg.type === 'log' && msg.data) { - const r = msg.data; - const local = new Date((r.timestamp || '') + 'Z'); - setRows(prev => [ - { time: isNaN(local) ? r.timestamp : local.toLocaleString(), level: r.level, service: r.service || '-', message: r.message }, - ...prev, - ]); + if (msg.type === 'error') { setLastError({ code: msg.code, message: msg.message }); return; } + + // 2) 단일 DTO (ts/level/service/message) 수신 + if ('ts' in msg || 'timestamp' in msg || 'time' in msg) { + const ts = msg.ts ?? msg.timestamp ?? msg.time ?? ''; + const local = new Date((ts || '') + 'Z'); + const row = { + time: isNaN(local) ? ts : local.toLocaleString(), + level: msg.level || 'INFO', + service: msg.service || '-', + message: msg.message || '', + }; + setRows(prev => [row, ...prev]); return; } - if (msg.type === 'filter_between_result' && Array.isArray(msg.data)) { + // 3) 배열 응답(과거 로그 묶음)도 지원 + if (Array.isArray(msg.data)) { const list = msg.data.map(r => { - const local = new Date((r.timestamp || '') + 'Z'); - return { time: isNaN(local) ? r.timestamp : local.toLocaleString(), level: r.level, service: r.service || '-', message: r.message }; + const ts = r.ts ?? r.timestamp ?? r.time ?? ''; + const local = new Date((ts || '') + 'Z'); + return { + time: isNaN(local) ? ts : local.toLocaleString(), + level: r.level || 'INFO', + service: r.service || '-', + message: r.message || '', + }; }); setRows(prev => [...list.reverse(), ...prev]); - return; } - - if (msg.type === 'error') { - setLastError({ code: msg.code, message: msg.message }); - } - }; + };; ws.onclose = (e) => { setStatus("closed"); @@ -120,12 +132,12 @@ export default function useLogSocket({ teamCode, sourceType }) { const requestRange = useCallback((from, to) => { if (!wsRef.current || wsRef.current.readyState !== 1) return; - wsRef.current.send(JSON.stringify({ type: 'filter_between', from, to })); + wsRef.current.send(JSON.stringify({ type: 'filter', from, to })); }, []); const requestLevelContext = useCallback((level, context = 50) => { if (!wsRef.current || wsRef.current.readyState !== 1) return; - wsRef.current.send(JSON.stringify({ type: 'level_context', level, context })); + wsRef.current.send(JSON.stringify({ type: 'levelFilter', level, context })); }, []); return { status, rows, lastError, requestRange, requestLevelContext, reconnect: connect, disconnect }; diff --git a/src/pages/LogMonitoring.jsx b/src/pages/LogMonitoring.jsx index 5fbedb1..95e7f0d 100644 --- a/src/pages/LogMonitoring.jsx +++ b/src/pages/LogMonitoring.jsx @@ -142,6 +142,9 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { const [filterLevel, setFilterLevel] = useState("ALL"); const [isStreaming, setIsStreaming] = useState(true); // 일시정지/재시작 + // ✅ WS 자동연결 방지: 최초엔 undefined로 두었다가, 수집 시작 후에만 세팅 + const [wsTeamCode, setWsTeamCode] = useState(undefined); + // 컴포넌트 내부 const [statusActive, setStatusActive] = useState(null); // true | false | null const [statusLoading, setStatusLoading] = useState(false); @@ -220,7 +223,7 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { ); }; - // WS 훅: rows는 [{time, level, service, message}, ...] 최신이 앞 + // ✅ 여기서 teamCode 대신 wsTeamCode를 넘긴다 (undefined면 훅이 connect() 안 함) const { status, rows, @@ -229,7 +232,7 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { requestLevelContext, reconnect, disconnect, - } = useLogSocket({ teamCode: canConnect ? teamCode : undefined, sourceType }); + } = useLogSocket({ teamCode: wsTeamCode, sourceType }); const isWsOpen = status === 'open'; @@ -264,31 +267,34 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { } }, [filteredLogs, isStreaming]); - // REST: 수집 시작 트리거 + // REST: 수집 시작 → 성공 시에만 WS 연결 허용 const onStartCollect = async () => { + if (!teamCode) return alert("팀이 선택되지 않았습니다."); try { - await registerLog(sourceType, teamCode); // 1) HTTP 등록 - reconnect(); // 2) WS 연결 + await registerLog(sourceType, teamCode); + // 1) WS를 열 수 있도록 teamCode 주입 + setWsTeamCode(teamCode); + // 2) 연결 시도 + reconnect(); } catch (e) { - alert('수집 시작 실패: ' + (e?.response?.data?.message || e.message)); + alert("수집 시작 실패: " + (e?.response?.data?.message || e.message)); } }; - // REST: 상태 - const onCheckStatus = async () => { - try { - const res = await queryStatus(sourceType, teamCode); - alert("active: " + (res?.data?.active ? "true" : "false")); - } catch { - alert("상태 조회 실패"); - } + // WS 수동 연결/해제 + const onManualReconnect = () => { + if (!wsTeamCode) setWsTeamCode(teamCode); // 혹시 비어있으면 채움 + reconnect(); + }; + const onManualDisconnect = () => { + disconnect(); + setWsTeamCode(undefined); // 🔒 자동 재연결 차단 }; return (

CLI 로그 모니터링

- {/* 상단 컨트롤 */}
- - 팀: {teamCode || "-"} - + 팀: {teamCode || "-"} - - + - -
- {/* 상태/에러 표시 */}
WS 상태: {status} {lastError ? ` | 에러: [${lastError.code}] ${lastError.message}` : ""} + {!wsTeamCode && " | (수집 시작 전이므로 자동 연결 안 함)"}
- {/* 로그 뷰어 */}
{filteredRows.map((row, i) => (
{formatLine(row)}
@@ -385,32 +358,26 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) {
- {/* (선택) 하단 빠른 조회 버튼 - WS 쿼리 */} -
+
- + From db53de54547270cb19be735ad09499409ce53514 Mon Sep 17 00:00:00 2001 From: son0131 Date: Wed, 20 Aug 2025 22:33:45 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix/#52=20=EB=A1=9C=EA=B7=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=88=ED=84=B0=EB=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/log.js | 15 ++++-- src/pages/LogMonitoring.jsx | 105 +++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/api/hooks/log.js b/src/api/hooks/log.js index 5a83072..63e098a 100644 --- a/src/api/hooks/log.js +++ b/src/api/hooks/log.js @@ -56,16 +56,21 @@ export default function useLogSocket({ teamCode, sourceType }) { setStatus(stateMap[ws.readyState] || 'connecting'); ws.onopen = () => { + console.log('[WS OPENED]'); backoffRef.current = 1000; setStatus('open'); - // ★ 계약: 연결 직후 register 프레임 전송 - ws.send(JSON.stringify({ + + const frame = { type: 'register', - sourceType: toWsParam(sourceType), // BACKEND/FRONTEND/RDS 대문자 - code: teamCode // ← 키 이름은 code - })); + sourceType: toWsParam(sourceType), // ex: "BACKEND" + code: teamCode, // ex: "28a00517" + }; + + console.log('[WS SEND REGISTER]', frame); + ws.send(JSON.stringify(frame)); }; + ws.onmessage = (e) => { let msg; try { msg = JSON.parse(e.data); } catch { return; } if (!msg || typeof msg !== 'object') return; diff --git a/src/pages/LogMonitoring.jsx b/src/pages/LogMonitoring.jsx index 95e7f0d..52227d3 100644 --- a/src/pages/LogMonitoring.jsx +++ b/src/pages/LogMonitoring.jsx @@ -145,50 +145,50 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { // ✅ WS 자동연결 방지: 최초엔 undefined로 두었다가, 수집 시작 후에만 세팅 const [wsTeamCode, setWsTeamCode] = useState(undefined); - // 컴포넌트 내부 - const [statusActive, setStatusActive] = useState(null); // true | false | null - const [statusLoading, setStatusLoading] = useState(false); - const [statusError, setStatusError] = useState(null); - - useEffect(() => { - if (!teamCode || !sourceType) { - setStatusActive(null); - setStatusError(null); - setStatusLoading(false); - return; - } - - let stop = false; - const tick = async () => { - // 첫 로딩에만 로딩 표시 - setStatusLoading((prev) => (statusActive === null ? true : prev)); - try { - const { data } = await queryStatus(sourceType, teamCode); - if (stop) return; - setStatusActive(!!data?.active); // true면 "수집 중", false면 "대기" - setStatusError(null); - } catch (err) { - if (stop) return; - const code = err?.response?.data?.code; - if (code === "40710") { - // 서버 약속: 수집 미시작 - setStatusActive(false); - setStatusError(null); - } else { - setStatusError("error"); - } - } finally { - if (!stop) setStatusLoading(false); - } - }; - - tick(); // 즉시 한 번 - const id = setInterval(tick, 15000); // 15초마다 - return () => { - stop = true; - clearInterval(id); - }; - }, [teamCode, sourceType]); + // // 컴포넌트 내부 + // const [statusActive, setStatusActive] = useState(null); // true | false | null + // const [statusLoading, setStatusLoading] = useState(false); + // const [statusError, setStatusError] = useState(null); + + // useEffect(() => { + // if (!teamCode || !sourceType) { + // setStatusActive(null); + // setStatusError(null); + // setStatusLoading(false); + // return; + // } + + // let stop = false; + // const tick = async () => { + // // 첫 로딩에만 로딩 표시 + // setStatusLoading((prev) => (statusActive === null ? true : prev)); + // try { + // const { data } = await queryStatus(sourceType, teamCode); + // if (stop) return; + // setStatusActive(!!data?.active); // true면 "수집 중", false면 "대기" + // setStatusError(null); + // } catch (err) { + // if (stop) return; + // const code = err?.response?.data?.code; + // if (code === "40710") { + // // 서버 약속: 수집 미시작 + // setStatusActive(false); + // setStatusError(null); + // } else { + // setStatusError("error"); + // } + // } finally { + // if (!stop) setStatusLoading(false); + // } + // }; + + // tick(); // 즉시 한 번 + // const id = setInterval(tick, 15000); // 15초마다 + // return () => { + // stop = true; + // clearInterval(id); + // }; + // }, [teamCode, sourceType]); const StatusBadge = ({ active, loading, error }) => { let label = "대기"; @@ -310,13 +310,13 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { 팀: {teamCode || "-"} - + {/* */} - + */} @@ -342,7 +342,14 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) {
WS 상태: {status} {lastError ? ` | 에러: [${lastError.code}] ${lastError.message}` : ""} - {!wsTeamCode && " | (수집 시작 전이므로 자동 연결 안 함)"} + + + {status === 'open' ? '✅ 연결됨' : + status === 'connecting' ? '⏳ 연결 중...' : + status === 'closed' ? '❌ 끊김' : + status === 'closing' ? '🔒 닫는 중...' : + ''} +
From 852161e6f49208fec0f3e34f91d3a75aeef13ead Mon Sep 17 00:00:00 2001 From: son0131 Date: Wed, 20 Aug 2025 22:46:03 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix/#52=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/LogMonitoring.jsx | 117 +----------------------------------- 1 file changed, 2 insertions(+), 115 deletions(-) diff --git a/src/pages/LogMonitoring.jsx b/src/pages/LogMonitoring.jsx index 52227d3..267c3a2 100644 --- a/src/pages/LogMonitoring.jsx +++ b/src/pages/LogMonitoring.jsx @@ -1,117 +1,4 @@ -// import React, { useState, useEffect, useRef } from "react"; -// import "../styles/css/LogMonitoring.css"; -// import { Pause, Play } from "lucide-react"; - -// const mockLogs = [ -// "[INFO] 서버가 시작되었습니다.", -// "[WARN] 메모리 사용량 증가 감지", -// "[ERROR] 데이터베이스 연결 실패", -// "[INFO] 요청이 정상 처리되었습니다.", -// ]; - -// function LogMonitoring() { -// const [logs, setLogs] = useState([]); -// const [isStreaming, setIsStreaming] = useState(true); -// const [filterLevel, setFilterLevel] = useState("ALL"); -// const logEndRef = useRef(null); - -// // 실제 API 요청 -// // const fetchLogs = async () => { -// // try { -// // const res = await fetch("/logging/live"); -// // if (!res.ok) throw new Error("로그 로딩 실패"); -// // const data = await res.json(); -// // setLogs(data); -// // } catch (err) { -// // console.error("로그 가져오기 실패:", err); -// // } -// // }; - -// useEffect(() => { -// const interval = setInterval(() => { -// if (!isStreaming) return; - -// // 모의 로그 추가 -// setLogs((prev) => { -// const nextLog = mockLogs[Math.floor(Math.random() * mockLogs.length)]; -// return [...prev, nextLog]; -// }); - -// // 실제 API 요청 -// // fetchLogs(); -// }, 2000); - -// return () => clearInterval(interval); -// }, [isStreaming]); - -// useEffect(() => { -// if (logEndRef.current) { -// logEndRef.current.scrollIntoView({ behavior: "smooth" }); -// } -// }, [logs]); - -// const filteredLogs = logs.filter((log) => { -// if (filterLevel === "ALL") return true; -// return log.includes(filterLevel); -// }); - -// return ( -//
-//

CLI 로그 모니터링

- -//
-// - -// -//
- -//
-// {filteredLogs.map((log, i) => ( -//
-// {log} -//
-// ))} -// {/* 자동 스크롤 대상 */} -//
-//
-//
-// ); -// } - -// export default LogMonitoring; -// src/pages/LogMonitoring.jsx (또는 현재 경로 유지) -// 기존 파일 전체 교체본 + import React, { useEffect, useMemo, useRef, useState } from "react"; import "../styles/css/LogMonitoring.css"; import { Pause, Play } from "lucide-react"; @@ -310,7 +197,7 @@ export default function LogMonitoring({ teamCode, defaultSource = "BACKEND" }) { 팀: {teamCode || "-"} {/* */}