Files
stocksearch/static/js/websocket.js
2026-03-31 19:32:59 +09:00

149 lines
4.5 KiB
JavaScript

/**
* StockWebSocket - 키움 주식 실시간 시세 WebSocket 클라이언트
* 자동 재연결 (지수 백오프), 구독 목록 자동 복구 지원
*/
class StockWebSocket {
constructor() {
this.ws = null;
this.subscriptions = new Set(); // 현재 구독 중인 종목 코드
// 메시지 타입별 핸들러 맵: type → { code → callbacks[] }
this.handlers = {
price: new Map(),
orderbook: new Map(),
program: new Map(),
meta: new Map(),
};
// 전역 핸들러 (코드 무관한 메시지용)
this.globalHandlers = {
market: [],
};
this.reconnectDelay = 1000; // 초기 재연결 대기 시간 (ms)
this.maxReconnectDelay = 30000; // 최대 재연결 대기 시간 (ms)
this.intentionalClose = false;
this.connect();
}
connect() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${protocol}//${location.host}/ws`;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('WebSocket 연결됨');
this.reconnectDelay = 1000; // 성공 시 재연결 대기 시간 초기화
// 기존 구독 목록 자동 복구
this.subscriptions.forEach(code => this._send({ type: 'subscribe', code }));
};
this.ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
this._handleMessage(msg);
} catch (e) {
console.error('메시지 파싱 실패:', e);
}
};
this.ws.onclose = () => {
if (!this.intentionalClose) {
console.log(`WebSocket 연결 끊김. ${this.reconnectDelay}ms 후 재연결...`);
setTimeout(() => this.connect(), this.reconnectDelay);
// 지수 백오프
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}
};
this.ws.onerror = (err) => {
console.error('WebSocket 오류:', err);
};
}
// 종목 구독
subscribe(code) {
this.subscriptions.add(code);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this._send({ type: 'subscribe', code });
}
}
// 종목 구독 해제
unsubscribe(code) {
this.subscriptions.delete(code);
Object.values(this.handlers).forEach(map => map.delete(code));
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this._send({ type: 'unsubscribe', code });
}
}
// 현재가 수신 콜백 등록
onPrice(code, callback) {
this._addCodeHandler('price', code, callback);
}
// 호가창 수신 콜백 등록
onOrderBook(code, callback) {
this._addCodeHandler('orderbook', code, callback);
}
// 프로그램 매매 수신 콜백 등록
onProgram(code, callback) {
this._addCodeHandler('program', code, callback);
}
// 종목 메타 수신 콜백 등록
onMeta(code, callback) {
this._addCodeHandler('meta', code, callback);
}
// 장운영 상태 수신 콜백 등록 (전역)
onMarket(callback) {
this.globalHandlers.market.push(callback);
}
// 내부: 코드별 핸들러 등록
_addCodeHandler(type, code, callback) {
if (!this.handlers[type]) return;
if (!this.handlers[type].has(code)) {
this.handlers[type].set(code, []);
}
this.handlers[type].get(code).push(callback);
}
// 내부: 메시지 처리
_handleMessage(msg) {
const { type, code, data } = msg;
if (type === 'market') {
this.globalHandlers.market.forEach(fn => fn(data));
return;
}
if (type === 'error') {
console.warn(`서버 오류 [${code}]:`, data?.message);
return;
}
if (this.handlers[type] && code) {
const callbacks = this.handlers[type].get(code) || [];
callbacks.forEach(fn => fn(data));
}
}
// 내부: JSON 메시지 전송
_send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
close() {
this.intentionalClose = true;
this.ws?.close();
}
}
// 전역 인스턴스 (stock_detail.html에서 사용)
const stockWS = new StockWebSocket();