Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/api/apis/log.js
Original file line number Diff line number Diff line change
@@ -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 } });
149 changes: 149 additions & 0 deletions src/api/hooks/log.js
Original file line number Diff line number Diff line change
@@ -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 };
}
1 change: 1 addition & 0 deletions src/api/utils/axios.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ clientAI.interceptors.response.use(
}
);

export default client;
export { client, clientAI };
34 changes: 11 additions & 23 deletions src/components/DevTools/ERD/edge/ERDEdge.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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 },
};

// 접점이 위치한 변 구하기
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Github/GithubManagerModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default function GithubManagerModal({ teamCode, onClose }) {
)}
{tab === 'delete' && (
<div className="flex flex-col gap-3">
<p>현재 등록된 GitHub 설정을 삭제합니다. 이 작업은 되돌릴 수 없습니다.</p>
<p><strong>현재 등록된 GitHub 설정을 삭제합니다. 이 작업은 되돌릴 수 없습니다.</strong></p>
<GithubDelete
teamCode={teamCode}
className="modal-delete-button"
Expand Down
100 changes: 69 additions & 31 deletions src/components/Github/GithubUpdateForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,42 +45,80 @@ export default function GithubUpdateForm({ teamCode, onDone }) {
};

return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3 max-w-xl">
<label className="flex flex-col gap-1">
<span><b>저장소 URL</b></span>
<input value={repoUrl} onChange={(e) => setRepoUrl(e.target.value)} placeholder="https://github.com/owner/repo" />
</label>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 max-w-xl">
{/* 저장소 URL */}
<label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-gray-700">Repo URL</span>
<input
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/owner/repo"
className="px-3 py-2 border-2 border-gray-300 rounded-md focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 font-medium"
/>
</label>

<label className="flex flex-col gap-1">
<span><b>Default Branch</b></span>
<input value={defaultBranch} onChange={(e) => setDefaultBranch(e.target.value)} placeholder="main" />
</label>
{/* 기본 브랜치 */}
<label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-gray-700">Default Branch</span>
<input
value={defaultBranch}
onChange={(e) => setDefaultBranch(e.target.value)}
placeholder="main"
className="px-3 py-2 border-2 border-gray-300 rounded-md focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 font-medium"
/>
</label>

<label className="inline-flex items-center">
<input
type="checkbox"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
className="mr-2" // ✅ 체크박스 오른쪽 간격
/>
<span><b>Private</b></span>
</label>
{/* Private 체크박스 */}
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
className="w-4 h-4 accent-blue-500"
/>
<span className="text-sm font-semibold text-gray-700">Private</span>
</label>

{/* 토큰 */}
<label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-gray-700">
Personal Access Token (변경 시에만 입력)
</span>
<input
type="password"
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder="ghp_xxx..."
className="px-3 py-2 border-2 border-gray-300 rounded-md focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 font-medium"
/>
</label>

<label className="flex flex-col gap-1">
<span><b>Personal Access Token (변경 시에만 입력)</b></span>
<input type="password" value={accessToken} onChange={(e) => setAccessToken(e.target.value)} placeholder="ghp_xxx..." />
</label>
{/* 액션 버튼 */}
<div className="modal-actions mt-4 flex justify-end gap-3">
<button
type="button"
onClick={onDone}
className="px-4 py-2 rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50"
>
취소
</button>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 rounded-md bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-300"
>
{isPending ? '업데이트 중...' : '업데이트'}
</button>
</div>

<div className="modal-actions">
<button type="button" onClick={onDone}>취소</button>
<button type="submit" disabled={isPending}>
{isPending ? '업데이트 중...' : '업데이트'}
</button>
</div>
{/* 에러 표시 */}
{data && !data.success && (
<p className="text-sm text-red-600">
{data.error?.code} · {data.error?.message}
</p>
)}
{error && <p className="text-sm text-red-600">네트워크 오류</p>}
</form>

{data && !data.success && <p style={{ color: 'crimson' }}>{data.error?.code} · {data.error?.message}</p>}
{error && <p style={{ color: 'crimson' }}>네트워크 오류</p>}
</form>
);
}
2 changes: 1 addition & 1 deletion src/pages/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const Home = () => {
{activeTab === "성능 테스트" && (
<PerformanceTest specData={specData} trafficData={trafficData} />
)}{" "}
{activeTab === "로그 모니터링" && <LogMonitoring />}
{activeTab === "로그 모니터링" && <LogMonitoring teamCode={teamCode} defaultSource="BACKEND"/>}
</main>

{/* ✅ 모달은 여기에서 관리 */}
Expand Down
Loading