perf: Web Worker 데이터 파싱 및 배치 처리 최적화
- signalWorker.js 추가 (메인 스레드 파싱 오프로딩) - useShipData 500ms 배치 인터벌 + Worker 연동 - stompClient에 subscribeShipsRaw 추가 (Worker용) - signalApi에 fetchAllSignalsRaw 추가 (Worker용) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
부모
f273080d5e
커밋
fa34c6cd0c
@ -48,4 +48,48 @@ export async function fetchAllSignals() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 12분 이내 전체 선박 신호 조회 (Raw 문자열 배열 반환, Worker용)
|
||||
* Web Worker에서 파싱을 수행할 때 사용
|
||||
*
|
||||
* @returns {Promise<string[]>} 파이프 구분 문자열 배열
|
||||
*/
|
||||
export async function fetchAllSignalsRaw() {
|
||||
try {
|
||||
const response = await fetch('/signal-api/all/12');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// API 응답 구조: { data: [...] } 또는 직접 배열
|
||||
const rawData = result?.data || result || [];
|
||||
|
||||
if (!Array.isArray(rawData)) {
|
||||
console.warn('[fetchAllSignalsRaw] Invalid response format:', result);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 문자열 배열로 변환 (각 행이 이미 문자열이면 그대로, 배열이면 파이프로 조인)
|
||||
const rawLines = rawData.map((row) => {
|
||||
if (typeof row === 'string') {
|
||||
return row;
|
||||
}
|
||||
if (Array.isArray(row)) {
|
||||
return row.join('|');
|
||||
}
|
||||
return '';
|
||||
}).filter(line => line.trim());
|
||||
|
||||
console.log(`[fetchAllSignalsRaw] Loaded ${rawLines.length} raw lines for Worker`);
|
||||
|
||||
return rawLines;
|
||||
} catch (error) {
|
||||
console.error('[fetchAllSignalsRaw] Error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default fetchAllSignals;
|
||||
|
||||
@ -10,12 +10,18 @@ import { SHIP_MSG_INDEX, STOMP_TOPICS } from '../types/constants';
|
||||
* STOMP 클라이언트 인스턴스
|
||||
* 환경변수: VITE_SIGNAL_WS (예: ws://10.26.252.39:9090/connect)
|
||||
*/
|
||||
const brokerURL = import.meta.env.VITE_SIGNAL_WS || 'ws://localhost:8080/connect';
|
||||
console.log('[STOMP] Broker URL:', brokerURL);
|
||||
|
||||
export const signalStompClient = new Client({
|
||||
brokerURL: import.meta.env.VITE_SIGNAL_WS || 'ws://localhost:8080/connect',
|
||||
brokerURL,
|
||||
reconnectDelay: 10000,
|
||||
connectionTimeout: 5000,
|
||||
debug: () => {
|
||||
// STOMP 디버그 로그 비활성화 (너무 많은 로그 발생)
|
||||
debug: (str) => {
|
||||
// STOMP 디버그 로그 (연결 관련 메시지만 출력)
|
||||
if (str.includes('Opening') || str.includes('connected') || str.includes('error') || str.includes('closed')) {
|
||||
console.log('[STOMP Debug]', str);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -147,7 +153,7 @@ export function disconnectStomp() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 토픽 구독
|
||||
* 선박 토픽 구독 (파싱된 객체 반환)
|
||||
* - 개발: /topic/ship (실시간)
|
||||
* - 프로덕션: /topic/ship-throttled-60s (위성망 대응)
|
||||
* @param {Function} onMessage - 메시지 수신 콜백 (파싱된 선박 데이터 배열)
|
||||
@ -155,8 +161,7 @@ export function disconnectStomp() {
|
||||
*/
|
||||
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 = 0;
|
||||
const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10);
|
||||
|
||||
const topic = throttleSeconds > 0
|
||||
? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s`
|
||||
@ -181,6 +186,33 @@ export function subscribeShips(onMessage) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 토픽 구독 (Raw 문자열 반환, Worker용)
|
||||
* - Web Worker에서 파싱을 수행할 때 사용
|
||||
* @param {Function} onMessage - 메시지 수신 콜백 (파이프 구분 문자열 배열)
|
||||
* @returns {Object} 구독 객체 (unsubscribe 호출용)
|
||||
*/
|
||||
export function subscribeShipsRaw(onMessage) {
|
||||
const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10);
|
||||
|
||||
const topic = throttleSeconds > 0
|
||||
? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s`
|
||||
: STOMP_TOPICS.SHIP;
|
||||
|
||||
console.log(`[STOMP] Subscribing to ${topic} (Raw mode for Worker)`);
|
||||
|
||||
return signalStompClient.subscribe(topic, (message) => {
|
||||
try {
|
||||
const body = message.body;
|
||||
// 파싱 없이 줄 단위로 분리만 해서 전달
|
||||
const lines = body.split('\n').filter(line => line.trim());
|
||||
onMessage(lines);
|
||||
} catch (error) {
|
||||
console.error('[STOMP] Ship message parse error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 삭제 토픽 구독
|
||||
* @param {Function} onDelete - 삭제 메시지 수신 콜백 (featureId)
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
* 선박 데이터 관리 훅
|
||||
* - 초기 선박 데이터 API 로드 (/all/12)
|
||||
* - STOMP WebSocket 연결 및 구독
|
||||
* - 선박 데이터 수신 및 스토어 업데이트
|
||||
* - 배치 머지 최적화 (1초 or 500건)
|
||||
* - Web Worker를 통한 데이터 파싱 (메인 스레드 부담 감소)
|
||||
* - 배치 머지 최적화 (500ms 인터벌)
|
||||
*
|
||||
* 참조: mda-react-front/src/map/MapUpdater.tsx
|
||||
* 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙
|
||||
@ -13,19 +13,25 @@ import {
|
||||
signalStompClient,
|
||||
connectStomp,
|
||||
disconnectStomp,
|
||||
subscribeShips,
|
||||
subscribeShipsRaw,
|
||||
subscribeShipDelete,
|
||||
} from '../common/stompClient';
|
||||
import useShipStore from '../stores/shipStore';
|
||||
import { fetchAllSignals } from '../api/signalApi';
|
||||
import { fetchAllSignalsRaw } from '../api/signalApi';
|
||||
|
||||
// =====================
|
||||
// 배치 머지 설정
|
||||
// Web Worker 인스턴스 생성
|
||||
// =====================
|
||||
const BATCH_CONFIG = {
|
||||
maxInterval: 1000, // 최대 대기 시간 (1초)
|
||||
maxCount: 500, // 최대 버퍼 크기 (500건)
|
||||
};
|
||||
const SignalWorker = new Worker(
|
||||
new URL('../workers/signalWorker.js', import.meta.url),
|
||||
{ type: 'module' }
|
||||
);
|
||||
|
||||
// =====================
|
||||
// 배치 처리 설정
|
||||
// 참조: mda-react-front/src/map/MapUpdater.tsx
|
||||
// =====================
|
||||
const WEBSOCKET_CHUNK_INTERVAL = 500; // WebSocket 데이터 청크 처리 주기 (ms)
|
||||
|
||||
/**
|
||||
* 선박 데이터 관리 훅
|
||||
@ -37,52 +43,86 @@ export default function useShipData(options = {}) {
|
||||
const { autoConnect = true } = options;
|
||||
|
||||
const subscriptionsRef = useRef([]);
|
||||
const shipBufferRef = useRef([]);
|
||||
const batchTimerRef = useRef(null);
|
||||
const shipBufferRef = useRef([]); // Raw 문자열 버퍼
|
||||
const batchIntervalRef = useRef(null); // 배치 처리 인터벌
|
||||
const initialLoadDoneRef = useRef(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { mergeFeatures, deleteFeatureById, setConnected, isConnected } = useShipStore();
|
||||
const mergeFeatures = useShipStore((s) => s.mergeFeatures);
|
||||
const deleteFeatureById = useShipStore((s) => s.deleteFeatureById);
|
||||
const setConnected = useShipStore((s) => s.setConnected);
|
||||
const isConnected = useShipStore((s) => s.isConnected);
|
||||
|
||||
/**
|
||||
* 버퍼된 선박 데이터 머지 실행
|
||||
* Worker 메시지 핸들러 (파싱된 선박 데이터 수신)
|
||||
*/
|
||||
const handleWorkerMessage = useCallback((e) => {
|
||||
const ships = e.data;
|
||||
if (ships.length > 0) {
|
||||
mergeFeatures(ships);
|
||||
}
|
||||
}, [mergeFeatures]);
|
||||
|
||||
/**
|
||||
* Worker 설정
|
||||
*/
|
||||
useEffect(() => {
|
||||
SignalWorker.onmessage = handleWorkerMessage;
|
||||
SignalWorker.onerror = (err) => {
|
||||
console.error('[SignalWorker] Error:', err);
|
||||
};
|
||||
|
||||
return () => {
|
||||
SignalWorker.onmessage = null;
|
||||
SignalWorker.onerror = null;
|
||||
};
|
||||
}, [handleWorkerMessage]);
|
||||
|
||||
/**
|
||||
* Raw 선박 메시지 수신 핸들러 (버퍼에 누적)
|
||||
* @param {string[]} lines - 파이프 구분 문자열 배열
|
||||
*/
|
||||
const handleShipMessageRaw = useCallback((lines) => {
|
||||
// 버퍼에 추가
|
||||
shipBufferRef.current.push(...lines);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 버퍼 플러시 - Worker로 전송
|
||||
*/
|
||||
const flushBuffer = useCallback(() => {
|
||||
if (shipBufferRef.current.length === 0) return;
|
||||
|
||||
// 버퍼 복사 후 초기화
|
||||
const ships = shipBufferRef.current;
|
||||
const rawMessages = shipBufferRef.current;
|
||||
shipBufferRef.current = [];
|
||||
|
||||
// 타이머 클리어
|
||||
if (batchTimerRef.current) {
|
||||
clearTimeout(batchTimerRef.current);
|
||||
batchTimerRef.current = null;
|
||||
}
|
||||
|
||||
// 머지 실행
|
||||
mergeFeatures(ships);
|
||||
}, [mergeFeatures]);
|
||||
// Worker로 전송 (파싱은 Worker에서 수행)
|
||||
SignalWorker.postMessage(rawMessages);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 선박 메시지 수신 핸들러 (배치 처리)
|
||||
* 조건: 1초 경과 OR 500건 누적 시 머지
|
||||
* 배치 처리 인터벌 시작
|
||||
*/
|
||||
const handleShipMessage = useCallback((ships) => {
|
||||
// 버퍼에 추가
|
||||
shipBufferRef.current.push(...ships);
|
||||
const startBatchInterval = useCallback(() => {
|
||||
if (batchIntervalRef.current) return;
|
||||
|
||||
// 조건 1: 500건 이상이면 즉시 머지
|
||||
if (shipBufferRef.current.length >= BATCH_CONFIG.maxCount) {
|
||||
batchIntervalRef.current = setInterval(() => {
|
||||
flushBuffer();
|
||||
return;
|
||||
}
|
||||
}, WEBSOCKET_CHUNK_INTERVAL);
|
||||
|
||||
// 조건 2: 타이머가 없으면 1초 타이머 설정
|
||||
if (!batchTimerRef.current) {
|
||||
batchTimerRef.current = setTimeout(() => {
|
||||
flushBuffer();
|
||||
}, BATCH_CONFIG.maxInterval);
|
||||
console.log(`[useShipData] Batch interval started: ${WEBSOCKET_CHUNK_INTERVAL}ms`);
|
||||
}, [flushBuffer]);
|
||||
|
||||
/**
|
||||
* 배치 처리 인터벌 중지
|
||||
*/
|
||||
const stopBatchInterval = useCallback(() => {
|
||||
if (batchIntervalRef.current) {
|
||||
clearInterval(batchIntervalRef.current);
|
||||
batchIntervalRef.current = null;
|
||||
}
|
||||
// 남은 버퍼 플러시
|
||||
flushBuffer();
|
||||
}, [flushBuffer]);
|
||||
|
||||
/**
|
||||
@ -107,14 +147,17 @@ export default function useShipData(options = {}) {
|
||||
});
|
||||
subscriptionsRef.current = [];
|
||||
|
||||
// 선박 토픽 구독
|
||||
const shipSub = subscribeShips(handleShipMessage);
|
||||
// 선박 토픽 구독 (Raw 모드 - Worker용)
|
||||
const shipSub = subscribeShipsRaw(handleShipMessageRaw);
|
||||
subscriptionsRef.current.push(shipSub);
|
||||
|
||||
// 선박 삭제 토픽 구독
|
||||
const deleteSub = subscribeShipDelete(handleShipDelete);
|
||||
subscriptionsRef.current.push(deleteSub);
|
||||
}, [handleShipMessage, handleShipDelete]);
|
||||
|
||||
// 배치 처리 인터벌 시작
|
||||
startBatchInterval();
|
||||
}, [handleShipMessageRaw, handleShipDelete, startBatchInterval]);
|
||||
|
||||
/**
|
||||
* 연결 성공 시 토픽 구독
|
||||
@ -129,7 +172,8 @@ export default function useShipData(options = {}) {
|
||||
*/
|
||||
const handleDisconnect = useCallback(() => {
|
||||
setConnected(false);
|
||||
}, [setConnected]);
|
||||
stopBatchInterval();
|
||||
}, [setConnected, stopBatchInterval]);
|
||||
|
||||
/**
|
||||
* 에러 발생 시
|
||||
@ -153,14 +197,8 @@ export default function useShipData(options = {}) {
|
||||
* STOMP 연결 해제
|
||||
*/
|
||||
const disconnect = useCallback(() => {
|
||||
// 남은 버퍼 머지
|
||||
flushBuffer();
|
||||
|
||||
// 타이머 클리어
|
||||
if (batchTimerRef.current) {
|
||||
clearTimeout(batchTimerRef.current);
|
||||
batchTimerRef.current = null;
|
||||
}
|
||||
// 배치 처리 인터벌 중지
|
||||
stopBatchInterval();
|
||||
|
||||
// 구독 해제
|
||||
subscriptionsRef.current.forEach((sub) => {
|
||||
@ -173,11 +211,11 @@ export default function useShipData(options = {}) {
|
||||
subscriptionsRef.current = [];
|
||||
|
||||
disconnectStomp();
|
||||
}, [flushBuffer]);
|
||||
}, [stopBatchInterval]);
|
||||
|
||||
/**
|
||||
* 초기 선박 데이터 로드 (API 호출)
|
||||
* 참조: mda-react-front/src/map/MapUpdater.tsx (라인 128-152)
|
||||
* Worker를 통해 파싱
|
||||
*/
|
||||
const loadInitialData = useCallback(async () => {
|
||||
if (initialLoadDoneRef.current) return;
|
||||
@ -185,11 +223,12 @@ export default function useShipData(options = {}) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('[useShipData] Loading initial ship data...');
|
||||
const ships = await fetchAllSignals();
|
||||
const rawLines = await fetchAllSignalsRaw();
|
||||
|
||||
if (ships.length > 0) {
|
||||
mergeFeatures(ships);
|
||||
console.log(`[useShipData] Initial load complete: ${ships.length} ships`);
|
||||
if (rawLines.length > 0) {
|
||||
// Worker로 전송하여 파싱
|
||||
SignalWorker.postMessage(rawLines);
|
||||
console.log(`[useShipData] Initial data sent to Worker: ${rawLines.length} ships`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useShipData] Initial load error:', error);
|
||||
@ -197,14 +236,12 @@ export default function useShipData(options = {}) {
|
||||
initialLoadDoneRef.current = true;
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mergeFeatures]);
|
||||
}, []);
|
||||
|
||||
// 초기화: API로 선박 데이터 로드 후 STOMP 연결
|
||||
useEffect(() => {
|
||||
if (!autoConnect) return;
|
||||
|
||||
// 1단계: API로 초기 선박 데이터 로드
|
||||
// 2단계: 로드 완료 후 STOMP 연결 (실시간 업데이트)
|
||||
const initialize = async () => {
|
||||
await loadInitialData();
|
||||
connect();
|
||||
@ -213,14 +250,10 @@ export default function useShipData(options = {}) {
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
// 타이머 클리어
|
||||
if (batchTimerRef.current) {
|
||||
clearTimeout(batchTimerRef.current);
|
||||
batchTimerRef.current = null;
|
||||
}
|
||||
stopBatchInterval();
|
||||
disconnect();
|
||||
};
|
||||
}, [autoConnect]); // loadInitialData, connect, disconnect를 deps에서 제외 (의도적)
|
||||
}, [autoConnect]); // loadInitialData, connect, disconnect, stopBatchInterval를 deps에서 제외 (의도적)
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
|
||||
143
src/workers/signalWorker.js
Normal file
143
src/workers/signalWorker.js
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 선박 신호 데이터 파싱 Web Worker
|
||||
* - 메인 스레드 부담 감소를 위해 파싱 + 객체 변환을 Worker에서 수행
|
||||
* - 참조: mda-react-front/src/workers/signalWorker.js
|
||||
* - 참조: dark/src/common/stompClient.js - rowToShipObject()
|
||||
*/
|
||||
|
||||
// SHIP_MSG_INDEX 인라인 (Worker는 별도 모듈이라 import 불가)
|
||||
const IDX = {
|
||||
TARGET_ID: 0,
|
||||
RECV_DATE_TIME: 1,
|
||||
SIGNAL_SOURCE_CODE: 2,
|
||||
COG: 3,
|
||||
SOG: 4,
|
||||
LONGITUDE: 5,
|
||||
LATITUDE: 6,
|
||||
SHIP_NAME: 7,
|
||||
SHIP_TYPE: 8,
|
||||
DIM_A: 9,
|
||||
DIM_B: 10,
|
||||
DIM_C: 11,
|
||||
DIM_D: 12,
|
||||
LOST: 16,
|
||||
HAZARDOUS_CATEGORY: 17,
|
||||
AIS: 19,
|
||||
VPASS: 20,
|
||||
ENAV: 21,
|
||||
VTS_AIS: 22,
|
||||
D_MF_HF: 23,
|
||||
VTS_RADAR: 24,
|
||||
INTEGRATE: 26,
|
||||
SIGNAL_KIND_CODE: 27,
|
||||
DRAUGHT: 28,
|
||||
IMO: 29,
|
||||
NATIONAL_CODE: 35,
|
||||
IS_PRIORITY: 36,
|
||||
ORIGINAL_TARGET_ID: 37,
|
||||
};
|
||||
|
||||
/**
|
||||
* 파이프 구분 문자열을 선박 객체로 변환
|
||||
* @param {string[]} row - 파싱된 배열
|
||||
* @returns {Object} 선박 데이터 객체
|
||||
*/
|
||||
function rowToShipObject(row) {
|
||||
const targetId = row[IDX.TARGET_ID] || '';
|
||||
const originalTargetId = row[IDX.ORIGINAL_TARGET_ID] || '';
|
||||
const signalSourceCode = row[IDX.SIGNAL_SOURCE_CODE] || '';
|
||||
const recvDateTime = row[IDX.RECV_DATE_TIME] || '';
|
||||
|
||||
return {
|
||||
// 고유 식별자 (signalSourceCode + originalTargetId)
|
||||
featureId: signalSourceCode + originalTargetId,
|
||||
|
||||
// 수신 시간 (타임스탬프, 정렬/비교용)
|
||||
receivedTimestamp: recvDateTime ? Number(recvDateTime) : Date.now(),
|
||||
|
||||
// 기본 식별 정보
|
||||
targetId,
|
||||
originalTargetId,
|
||||
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: recvDateTime,
|
||||
|
||||
// 선종 코드
|
||||
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 신호장비 플래그
|
||||
ais: row[IDX.AIS],
|
||||
vpass: row[IDX.VPASS],
|
||||
enav: row[IDX.ENAV],
|
||||
vtsAis: row[IDX.VTS_AIS],
|
||||
dMfHf: row[IDX.D_MF_HF],
|
||||
vtsRadar: row[IDX.VTS_RADAR],
|
||||
|
||||
// 원본 배열 (상세정보 등에 필요)
|
||||
_raw: row,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker 메시지 핸들러
|
||||
* 입력: string[] (파이프 구분 문자열 배열, 각 문자열은 한 척의 선박 데이터)
|
||||
* 출력: Object[] (선박 객체 배열)
|
||||
*/
|
||||
// Worker 초기화 로그
|
||||
console.log('[SignalWorker] Initialized');
|
||||
|
||||
self.onmessage = (e) => {
|
||||
const rawMessages = e.data;
|
||||
const ships = [];
|
||||
|
||||
for (let i = 0; i < rawMessages.length; i++) {
|
||||
try {
|
||||
const row = rawMessages[i].split('|');
|
||||
const ship = rowToShipObject(row);
|
||||
ships.push(ship);
|
||||
} catch (err) {
|
||||
// 파싱 에러는 무시하고 계속 진행
|
||||
}
|
||||
}
|
||||
|
||||
// 처리 결과 로그 (500ms마다 출력되므로 주기적 확인 가능)
|
||||
if (ships.length > 0) {
|
||||
console.log(`[SignalWorker] Parsed ${ships.length} ships`);
|
||||
}
|
||||
|
||||
self.postMessage(ships);
|
||||
};
|
||||
불러오는 중...
Reference in New Issue
Block a user