트레이딩창 UI 배치 및 UX 수정 및 기획서 추가

This commit is contained in:
2026-02-24 15:43:56 +09:00
parent 19ebb1c6ea
commit a16af8ad7d
16 changed files with 1615 additions and 479 deletions

View File

@@ -104,7 +104,8 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
// 소켓 생성
// socket 변수에 할당하기 전에 로컬 변수로 제어하여 이벤트 핸들러 클로저 문제 방지
const ws = new WebSocket(`${wsConnection.wsUrl}/tryitout`);
const ws = new WebSocket(wsConnection.wsUrl);
console.log("[KisWebSocket] Connecting to:", wsConnection.wsUrl);
socket = ws;
ws.onopen = () => {
@@ -147,7 +148,7 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
reconnectAttempt += 1;
const delayMs = getReconnectDelayMs(reconnectAttempt);
console.warn(
`[KisWebSocket] Disconnected (code=${event.code}, reason=${event.reason || "none"}) -> reconnect in ${delayMs}ms (attempt ${reconnectAttempt}/${MAX_AUTO_RECONNECT_ATTEMPTS})`,
`[KisWebSocket] Disconnected (code=${event.code}, reason=${event.reason || "none"}, wasClean=${event.wasClean}) -> reconnect in ${delayMs}ms (attempt ${reconnectAttempt}/${MAX_AUTO_RECONNECT_ATTEMPTS})`,
);
window.clearTimeout(reconnectRetryTimer);
@@ -158,7 +159,10 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
return;
}
if (hasSubscribers && reconnectAttempt >= MAX_AUTO_RECONNECT_ATTEMPTS) {
if (
hasSubscribers &&
reconnectAttempt >= MAX_AUTO_RECONNECT_ATTEMPTS
) {
set({
error:
"실시간 연결이 반복 종료되어 자동 재연결을 중단했습니다. 새로고침 또는 수동 재연결을 시도해 주세요.",
@@ -175,7 +179,13 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
ws.onerror = (event) => {
if (socket === ws) {
isConnecting = false;
console.error("[KisWebSocket] Error", event);
const errEvent = event as ErrorEvent;
console.error("[KisWebSocket] Error", {
type: event.type,
message: errEvent?.message,
url: ws.url,
readyState: ws.readyState,
});
set({
isConnected: false,
error: "웹소켓 연결 중 오류가 발생했습니다.",
@@ -207,15 +217,24 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
});
// KIS 제어 메시지: ALREADY IN USE appkey
// 이전 세션이 닫히기 전에 재연결될 때 간헐적으로 발생합니다.
// 이전 세션이 닫히기 전에 재연결될 때 발생합니다.
// KIS 서버가 세션을 정리하는 데 최소 10~30초가 필요하므로
// 충분한 대기 후 재연결합니다.
if (control.msgCd === "OPSP8996") {
const now = Date.now();
if (now - lastAppKeyConflictAt > 5_000) {
lastAppKeyConflictAt = now;
console.warn(
"[KisWebSocket] ALREADY IN USE appkey - 현재 소켓을 닫고 30초 후 재연결합니다.",
);
// 현재 소켓을 즉시 닫아 서버 측 세션 정리 유도
if (socket === ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, "ALREADY IN USE - graceful close");
}
window.clearTimeout(reconnectRetryTimer);
reconnectRetryTimer = window.setTimeout(() => {
void get().reconnect({ refreshApproval: false });
}, 1_200);
}, 30_000); // 30초 쿨다운
}
}
@@ -255,9 +274,19 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
reconnect: async (options) => {
const refreshApproval = options?.refreshApproval ?? false;
// disconnect()는 manualDisconnectRequested=true를 설정하므로 직접 호출 금지
// 대신 소켓만 직접 닫습니다.
manualDisconnectRequested = false;
window.clearTimeout(reconnectRetryTimer);
reconnectRetryTimer = undefined;
const currentSocket = socket;
get().disconnect();
if (
currentSocket &&
(currentSocket.readyState === WebSocket.OPEN ||
currentSocket.readyState === WebSocket.CONNECTING)
) {
currentSocket.close();
}
if (currentSocket && currentSocket.readyState !== WebSocket.CLOSED) {
await waitForSocketClose(currentSocket);
}
@@ -277,7 +306,10 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
) {
currentSocket.close();
}
if (currentSocket?.readyState === WebSocket.CLOSED && socket === currentSocket) {
if (
currentSocket?.readyState === WebSocket.CLOSED &&
socket === currentSocket
) {
socket = null;
}
set({ isConnected: false });
@@ -306,11 +338,6 @@ export const useKisWebSocketStore = create<KisWebSocketState>((set, get) => ({
}
subscriberCounts.set(key, currentCount + 1);
// **연결이 안 되어 있으면 연결 시도**
if (!socket || socket.readyState !== WebSocket.OPEN) {
get().connect();
}
// 3. 구독 해제 함수 반환
return () => {
const callbacks = subscribers.get(key);
@@ -414,7 +441,9 @@ function buildControlErrorMessage(message: KisWsControlMessage) {
return "실시간 연결이 다른 세션과 충돌해 재연결을 시도합니다.";
}
const detail = [message.msg1, message.msgCd].filter(Boolean).join(" / ");
return detail ? `실시간 제어 메시지 오류: ${detail}` : "실시간 제어 메시지 오류";
return detail
? `실시간 제어 메시지 오류: ${detail}`
: "실시간 제어 메시지 오류";
}
/**
@@ -530,7 +559,8 @@ function dispatchRealtimeMessageToSubscribers(
if (subscribedTrId !== trId) return;
if (!normalizedIncomingSymbol) return;
const normalizedSubscribedSymbol = normalizeRealtimeSymbol(subscribedSymbol);
const normalizedSubscribedSymbol =
normalizeRealtimeSymbol(subscribedSymbol);
if (!normalizedSubscribedSymbol) return;
if (normalizedIncomingSymbol !== normalizedSubscribedSymbol) return;