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..63e098a
--- /dev/null
+++ b/src/api/hooks/log.js
@@ -0,0 +1,149 @@
+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;
+
+ // ★ 계약: 쿼리스트링 방식으로만 연결
+ console.log('[WS URL]', WS_URL);
+ const ws = new WebSocket(WS_URL);
+
+ wsRef.current = ws;
+ setStatus(stateMap[ws.readyState] || 'connecting');
+
+ ws.onopen = () => {
+ console.log('[WS OPENED]');
+ backoffRef.current = 1000;
+ setStatus('open');
+
+ const frame = {
+ type: 'register',
+ 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;
+
+ // 1) 등록 ACK 등 제어 프레임은 무시
+ if (msg.type === 'ok' || (msg.type === 'ack' && msg.action === 'register')) return;
+ 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;
+ }
+
+ // 3) 배열 응답(과거 로그 묶음)도 지원
+ if (Array.isArray(msg.data)) {
+ const list = msg.data.map(r => {
+ 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]);
+ }
+ };;
+
+ 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', from, to }));
+ }, []);
+
+ const requestLevelContext = useCallback((level, context = 50) => {
+ if (!wsRef.current || wsRef.current.readyState !== 1) return;
+ wsRef.current.send(JSON.stringify({ type: 'levelFilter', 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 97d41e7..c5c38b9 100644
--- a/src/api/utils/axios.jsx
+++ b/src/api/utils/axios.jsx
@@ -88,4 +88,5 @@ clientAI.interceptors.response.use(
}
);
+export default client;
export { client, clientAI };
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);
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 설정을 삭제합니다. 이 작업은 되돌릴 수 없습니다.
-
+
- {data && !data.success && {data.error?.code} · {data.error?.message}
}
- {error && 네트워크 오류
}
-
);
}
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 63e6a66..5730886 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -117,7 +117,7 @@ const Home = () => {
{activeTab === "성능 테스트" && (
)}{" "}
- {activeTab === "로그 모니터링" && }
+ {activeTab === "로그 모니터링" && }
{/* ✅ 모달은 여기에서 관리 */}
diff --git a/src/pages/LogMonitoring.jsx b/src/pages/LogMonitoring.jsx
index 426ee6d..267c3a2 100644
--- a/src/pages/LogMonitoring.jsx
+++ b/src/pages/LogMonitoring.jsx
@@ -1,112 +1,281 @@
-import React, { useState, useEffect, useRef } from "react";
+
+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); // 일시정지/재시작
+
+ // ✅ WS 자동연결 방지: 최초엔 undefined로 두었다가, 수집 시작 후에만 세팅
+ const [wsTeamCode, setWsTeamCode] = useState(undefined);
- // 실제 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(() => {
+ // 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";
+ }
+
+ return (
+
+
+ {label}
+
+ );
+ };
+
+ // ✅ 여기서 teamCode 대신 wsTeamCode를 넘긴다 (undefined면 훅이 connect() 안 함)
+ const {
+ status,
+ rows,
+ lastError,
+ requestRange,
+ requestLevelContext,
+ reconnect,
+ disconnect,
+ } = useLogSocket({ teamCode: wsTeamCode, sourceType });
+
+ const isWsOpen = status === 'open';
+
+ // ⏸ 일시정지를 구현하기 위해 "스냅샷"을 따로 유지
+ const [snapshotRows, setSnapshotRows] = useState([]);
useEffect(() => {
- const interval = setInterval(() => {
- if (!isStreaming) return;
+ if (isStreaming) {
+ setSnapshotRows(rows);
+ }
+ }, [rows, isStreaming]);
- // 모의 로그 추가
- setLogs((prev) => {
- const nextLog = mockLogs[Math.floor(Math.random() * mockLogs.length)];
- return [...prev, nextLog];
- });
+ // 현재 렌더 대상: 스트리밍 중이면 rows, 일시정지면 snapshotRows
+ const activeRows = isStreaming ? rows : snapshotRows;
- // 실제 API 요청
- // fetchLogs();
- }, 2000);
+ // 레벨 필터
+ const filteredRows = useMemo(() => {
+ if (filterLevel === "ALL") return activeRows;
+ return activeRows.filter((r) => r.level === filterLevel);
+ }, [activeRows, filterLevel]);
- return () => clearInterval(interval);
- }, [isStreaming]);
+ // 문자열로 변환 (기존 렌더 구조 유지)
+ const filteredLogs = useMemo(
+ () => filteredRows.map(formatLine),
+ [filteredRows]
+ );
+ // 자동 스크롤
+ const logEndRef = useRef(null);
useEffect(() => {
- if (logEndRef.current) {
+ if (logEndRef.current && isStreaming) {
logEndRef.current.scrollIntoView({ behavior: "smooth" });
}
- }, [logs]);
+ }, [filteredLogs, isStreaming]);
+
+ // REST: 수집 시작 → 성공 시에만 WS 연결 허용
+ const onStartCollect = async () => {
+ if (!teamCode) return alert("팀이 선택되지 않았습니다.");
+ try {
+ await registerLog(sourceType, teamCode);
+ // 1) WS를 열 수 있도록 teamCode 주입
+ setWsTeamCode(teamCode);
+ // 2) 연결 시도
+ reconnect();
+ } catch (e) {
+ alert("수집 시작 실패: " + (e?.response?.data?.message || e.message));
+ }
+ };
- const filteredLogs = logs.filter((log) => {
- if (filterLevel === "ALL") return true;
- return log.includes(filterLevel);
- });
+ // WS 수동 연결/해제
+ const onManualReconnect = () => {
+ if (!wsTeamCode) setWsTeamCode(teamCode); // 혹시 비어있으면 채움
+ reconnect();
+ };
+ const onManualDisconnect = () => {
+ disconnect();
+ setWsTeamCode(undefined); // 🔒 자동 재연결 차단
+ };
return (
CLI 로그 모니터링
-
+
+
+
+
팀: {teamCode || "-"}
+
+
+ {/*
*/}
+
+ {/*
*/}
+
+
+
+ WS 상태: {status}
+ {lastError ? ` | 에러: [${lastError.code}] ${lastError.message}` : ""}
+
+
+ {status === 'open' ? '✅ 연결됨' :
+ status === 'connecting' ? '⏳ 연결 중...' :
+ status === 'closed' ? '❌ 끊김' :
+ status === 'closing' ? '🔒 닫는 중...' :
+ ''}
+
+
+
- {filteredLogs.map((log, i) => (
+ {filteredRows.map((row, i) => (
- {log}
+ {formatLine(row)}
))}
- {/* 자동 스크롤 대상 */}
+
+
+
+
+
+
+
+
);
}
-
-export default LogMonitoring;
diff --git a/src/styles/css/AddServerModal.css b/src/styles/css/AddServerModal.css
index 97bc191..6074fb5 100644
--- a/src/styles/css/AddServerModal.css
+++ b/src/styles/css/AddServerModal.css
@@ -106,4 +106,44 @@
.modal-actions button:last-of-type:disabled {
background: #cbd5e1;
cursor: not-allowed;
+}
+
+.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: -moz-fit-content;
+ 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: -moz-fit-content;
+ width: fit-content;
+ display: inline-flex;
+
}
\ No newline at end of file
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 de2736c..6bf802e 100644
--- a/src/styles/css/sideBar.css
+++ b/src/styles/css/sideBar.css
@@ -58,6 +58,7 @@
display: flex;
flex-direction: column;
}
+
.side-bar .github-container {
margin: 20px 0 0 8px;
}
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 5f3a43c..2e22278 100644
--- a/src/styles/scss/components/sideBar.scss
+++ b/src/styles/scss/components/sideBar.scss
@@ -60,6 +60,7 @@
display: flex;
flex-direction: column;
}
+
.github-container {
margin: 20px 0 0 8px;
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