Files
hanarang-dashboard/frontend/lib/useSocket.ts
narang-ai 914903466c feat(sprint-005): cost monitor + WebSocket realtime
Sprint 004 non-blocking:
- null byte 방지 (harness file write)

Sprint 005 본문:
- CostLog Prisma 모델 추가
- TASK-016: GET /api/admin/costs + POST /api/admin/costs/record/:name
  자매별/모델별/일별 토큰 + 예상 비용 (USD), 기간 필터(day/week/month)
- TASK-018: WebSocket Gateway (@WebSocketGateway /ws namespace)
  sisters:update, activity:new 브로드캐스트
  EventsScheduler: 30초 주기 자매 상태 체크 + 브로드캐스트
  ActivityService: 새 로그 생성 시 실시간 브로드캐스트
- TASK-017: /admin/costs 비용 대시보드 (BarChart + 요약 카드)
- 메인 대시보드 useSocket 훅 + 실시간 연결 상태 표시
- 테스트 22/22 pass, FE 12 routes build 성공
- NEXT_PUBLIC_WS_URL env (.env.local.example 업데이트)
2026-04-04 12:28:25 +09:00

56 lines
1.5 KiB
TypeScript

'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
interface UseSocketOptions {
onSistersUpdate?: (sisters: any[]) => void;
onActivityNew?: (item: any) => void;
}
export function useSocket(options: UseSocketOptions = {}) {
const socketRef = useRef<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
// next.config.ts rewrites가 없는 경우를 위해 빈 origin 처리
// 브라우저에서 WebSocket은 rewrites 대상이 아니므로 직접 BE URL 필요
const wsUrl = process.env.NEXT_PUBLIC_WS_URL ?? window.location.origin;
const socket = io(`${wsUrl}/ws`, {
path: '/socket.io',
transports: ['websocket', 'polling'],
reconnectionAttempts: 5,
reconnectionDelay: 3000,
});
socket.on('connect', () => {
setConnected(true);
});
socket.on('disconnect', () => {
setConnected(false);
});
socket.on('sisters:update', (data: { sisters: any[] }) => {
options.onSistersUpdate?.(data.sisters);
});
socket.on('activity:new', (data: { item: any }) => {
options.onActivityNew?.(data.item);
});
socketRef.current = socket;
return () => {
socket.disconnect();
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const ping = useCallback(() => {
socketRef.current?.emit('ping');
}, []);
return { connected, ping };
}