/** * STOMP WebSocket 클라이언트 * 참조: mda-react-front/src/common/stompClient.ts * 참조: mda-react-front/src/map/MapUpdater.tsx */ import { Client } from '@stomp/stompjs'; import { SHIP_MSG_INDEX, STOMP_TOPICS } from '../types/constants'; /** * STOMP 클라이언트 인스턴스 * 환경변수: VITE_SIGNAL_WS (예: ws://10.26.252.39:9090/connect) */ export const signalStompClient = new Client({ brokerURL: import.meta.env.VITE_SIGNAL_WS || 'ws://localhost:8080/connect', reconnectDelay: 10000, connectionTimeout: 5000, debug: () => { // STOMP 디버그 로그 비활성화 (너무 많은 로그 발생) }, }); /** * 파이프 구분 문자열을 배열로 파싱 * 메인 프로젝트 메시지 형식: "value1|value2|value3|..." * @param {string} msgString - 파이프 구분 문자열 * @returns {Array} 파싱된 배열 */ export function parsePipeMessage(msgString) { return msgString.split('|'); } /** * 메시지 배열을 선박 객체로 변환 * 참조: mda-react-front/src/feature/commonFeature.ts - deckSplitCommonTarget() * * @param {Array} row - 파싱된 메시지 배열 (38개 요소) * @returns {Object} 선박 데이터 객체 */ export function rowToShipObject(row) { const idx = SHIP_MSG_INDEX; const targetId = row[idx.TARGET_ID] || ''; const originalTargetId = row[idx.ORIGINAL_TARGET_ID] || ''; const signalSourceCode = row[idx.SIGNAL_SOURCE_CODE] || ''; return { // 고유 식별자 (signalSourceCode + originalTargetId) featureId: signalSourceCode + originalTargetId, // 기본 식별 정보 targetId, // 통합 TARGET_ID originalTargetId, // 개별 장비 고유 TARGET_ID signalSourceCode, shipName: row[idx.SHIP_NAME] || '', // 선박명 shipType: row[idx.SHIP_TYPE] || '', // 선박 타입 // 위치 정보 longitude: parseFloat(row[idx.LONGITUDE]) || 0, latitude: parseFloat(row[idx.LATITUDE]) || 0, // 항해 정보 sog: parseFloat(row[idx.SOG]) || 0, cog: parseFloat(row[idx.COG]) || 0, // 시간 정보 receivedTime: row[idx.RECV_DATE_TIME] || '', // 선종 코드 signalKindCode: row[idx.SIGNAL_KIND_CODE] || '', // 상태 플래그 lost: row[idx.LOST] === '1', integrate: row[idx.INTEGRATE] === '1', isPriority: row[idx.IS_PRIORITY] === '1', // 위험물 카테고리 hazardousCategory: row[idx.HAZARDOUS_CATEGORY] || '', // 국적 코드 nationalCode: row[idx.NATIONAL_CODE] || '', // IMO 번호 imo: row[idx.IMO] || '', // 흘수 draught: row[idx.DRAUGHT] || '', // 선박 크기 (DIM) dimA: row[idx.DIM_A] || '', dimB: row[idx.DIM_B] || '', dimC: row[idx.DIM_C] || '', dimD: row[idx.DIM_D] || '', // AVETDR 신호장비 플래그 // 값: '1'=활성, '0'=비활성, ''=장비없음 ais: row[idx.AIS], // AIS vpass: row[idx.VPASS], // V-Pass enav: row[idx.ENAV], // E-Nav vtsAis: row[idx.VTS_AIS], // VTS-AIS dMfHf: row[idx.D_MF_HF], // D-MF/HF vtsRadar: row[idx.VTS_RADAR], // VTS-Radar // 다크시그널 판단은 shipStore에서 처리 (darkSignalIds Set + isAnyEquipmentActive) // 장비 활성 플래그(ais, vpass, enav, vtsAis, dMfHf, vtsRadar)는 위에서 개별 저장 // 원본 배열 (상세정보 등에 필요) _raw: row, }; } /** * AVETDR 문자열 파싱 * @param {string} avetdr - "A_V_E_T_D_R" 형식 (0 또는 1) * @returns {Object} 각 신호원 활성 상태 */ export function parseAvetdr(avetdr) { const parts = (avetdr || '0_0_0_0_0_0').split('_'); return { A: parts[0] === '1', // AIS V: parts[1] === '1', // VPASS E: parts[2] === '1', // ENAV T: parts[3] === '1', // VTS_AIS D: parts[4] === '1', // D_MF_HF R: parts[5] === '1', // RADAR }; } /** * STOMP 연결 시작 * @param {Object} callbacks - 콜백 함수들 * @param {Function} callbacks.onConnect - 연결 성공 시 * @param {Function} callbacks.onDisconnect - 연결 해제 시 * @param {Function} callbacks.onError - 에러 발생 시 */ export function connectStomp(callbacks = {}) { const { onConnect, onDisconnect, onError } = callbacks; signalStompClient.onConnect = (frame) => { console.log('[STOMP] Connected'); onConnect?.(frame); }; signalStompClient.onDisconnect = () => { console.log('[STOMP] Disconnected'); onDisconnect?.(); }; signalStompClient.onStompError = (frame) => { console.error('[STOMP] Error:', frame.headers?.message || 'Unknown error'); onError?.(frame); }; signalStompClient.activate(); } /** * STOMP 연결 해제 */ export function disconnectStomp() { if (signalStompClient.connected) { signalStompClient.deactivate(); } } /** * 선박 토픽 구독 * - 개발: /topic/ship (실시간) * - 프로덕션: /topic/ship-throttled-60s (위성망 대응) * @param {Function} onMessage - 메시지 수신 콜백 (파싱된 선박 데이터 배열) * @returns {Object} 구독 객체 (unsubscribe 호출용) */ export function subscribeShips(onMessage) { // 환경변수로 쓰로틀 설정 (VITE_SHIP_THROTTLE: 0=실시간, 5/10/30/60=쓰로틀) // const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10); const throttleSeconds = 60; const topic = throttleSeconds > 0 ? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s` : STOMP_TOPICS.SHIP; console.log(`[STOMP] Subscribing to ${topic}`); return signalStompClient.subscribe(topic, (message) => { try { const body = message.body; const lines = body.split('\n').filter(line => line.trim()); const ships = lines.map(line => { const row = parsePipeMessage(line); return rowToShipObject(row); }); onMessage(ships); } catch (error) { console.error('[STOMP] Ship message parse error:', error); } }); } /** * 선박 삭제 토픽 구독 * @param {Function} onDelete - 삭제 메시지 수신 콜백 (featureId) * @returns {Object} 구독 객체 */ export function subscribeShipDelete(onDelete) { console.log(`[STOMP] Subscribing to ${STOMP_TOPICS.SHIP_DELETE}`); return signalStompClient.subscribe(STOMP_TOPICS.SHIP_DELETE, (message) => { try { const body = message.body; const [signalSourceCode, targetId] = body.split('|'); if (signalSourceCode && targetId) { const featureId = signalSourceCode + targetId; onDelete(featureId); } } catch (error) { console.error('[STOMP] Ship delete parse error:', error); } }); } /** * 선박 카운트 토픽 구독 * @param {Function} onCount - 카운트 메시지 수신 콜백 * @returns {Object} 구독 객체 */ export function subscribeShipCount(onCount) { return signalStompClient.subscribe(STOMP_TOPICS.COUNT, (message) => { try { const counts = JSON.parse(message.body); onCount(counts); } catch (error) { console.error('[STOMP] Count message parse error:', error); } }); }