From ad6347f2a23681f08d033b38ddb54e7e387124a8 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 19:41:49 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../goldenchart/config/WebSocketConfig.java | 6 ++- desktop/src/setMainWindowTitle.ts | 26 ++++--------- desktop/vite.config.ts | 2 + frontend/src/hooks/useDesktopAppTitle.ts | 13 +++---- frontend/src/utils/backendApi.ts | 7 ++++ frontend/src/utils/stompChartBroker.ts | 39 +++++++++++++------ packages/shared/src/api/backendApi.ts | 10 +++++ scripts/build-desktop.sh | 1 + scripts/bump-desktop-version-from-release.sh | 6 +++ 9 files changed, 71 insertions(+), 39 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/config/WebSocketConfig.java b/backend/src/main/java/com/goldenchart/config/WebSocketConfig.java index 5befd32..756a6be 100644 --- a/backend/src/main/java/com/goldenchart/config/WebSocketConfig.java +++ b/backend/src/main/java/com/goldenchart/config/WebSocketConfig.java @@ -35,8 +35,12 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { + // Tauri Desktop — SockJS(XHR·쿠키)는 cross-origin WebView에서 실패 → 순수 WebSocket + registry.addEndpoint("/ws/trading/ws") + .setAllowedOriginPatterns("*"); + registry.addEndpoint("/ws/trading") .setAllowedOriginPatterns("*") - .withSockJS(); // SockJS fallback (브라우저 환경 대비) + .withSockJS(); // SockJS fallback (브라우저 동일 오리진) } } diff --git a/desktop/src/setMainWindowTitle.ts b/desktop/src/setMainWindowTitle.ts index bfdf4e3..53e39bc 100644 --- a/desktop/src/setMainWindowTitle.ts +++ b/desktop/src/setMainWindowTitle.ts @@ -1,30 +1,18 @@ import { getVersion } from '@tauri-apps/api/app'; import { getCurrentWindow } from '@tauri-apps/api/window'; -import { - formatDesktopWindowTitle, - readBakedDesktopAppVersion, -} from '@frontend/utils/desktopAppVersion'; +import { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion'; export { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion'; /** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */ export async function syncMainWindowTitle(): Promise { - try { - const version = (await getVersion()) || readBakedDesktopAppVersion() || ''; - const title = formatDesktopWindowTitle(version); - await getCurrentWindow().setTitle(title); - if (typeof document !== 'undefined' && document.title !== title) { - document.title = title; - } - return title; - } catch (e) { - const fallback = formatDesktopWindowTitle(readBakedDesktopAppVersion()); - if (typeof document !== 'undefined') { - document.title = fallback; - } - console.warn('[desktop] syncMainWindowTitle failed:', e); - throw e; + const version = (await getVersion()).trim(); + const title = formatDesktopWindowTitle(version); + await getCurrentWindow().setTitle(title); + if (typeof document !== 'undefined' && document.title !== title) { + document.title = title; } + return title; } let titleGuardInstalled = false; diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index 6bd5f4a..d58f36e 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -11,6 +11,8 @@ const sharedRoot = path.resolve(__dirname, '../packages/shared/src'); const host = process.env.TAURI_DEV_HOST; function readDesktopAppVersion(): string { + const fromEnv = process.env.DESKTOP_APP_VERSION?.trim(); + if (fromEnv) return fromEnv; try { const confPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json'); const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string }; diff --git a/frontend/src/hooks/useDesktopAppTitle.ts b/frontend/src/hooks/useDesktopAppTitle.ts index 9dc2091..fd50dde 100644 --- a/frontend/src/hooks/useDesktopAppTitle.ts +++ b/frontend/src/hooks/useDesktopAppTitle.ts @@ -1,21 +1,18 @@ import { useEffect, useState } from 'react'; import { isDesktop } from '../utils/platform'; import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge'; -import { readBakedDesktopAppVersion } from '../utils/desktopAppVersion'; -/** Desktop — 네이티브 창 타이틀·document.title을 GoldenChart (vX.Y.Z) 로 맞춤 */ +/** Desktop — Tauri getVersion() 기준 (설치된 앱 버전). baked 0.1.4 등 git 고정값 사용 안 함 */ export function useDesktopAppTitle(): string | null { - const [version, setVersion] = useState(() => - isDesktop() ? readBakedDesktopAppVersion() : null, - ); + const [version, setVersion] = useState(null); useEffect(() => { if (!isDesktop()) return; let cancelled = false; void (async () => { - const v = (await getDesktopAppVersion()) ?? readBakedDesktopAppVersion(); - if (cancelled || !v) return; - setVersion(v); + const v = await getDesktopAppVersion(); + if (cancelled || !v?.trim()) return; + setVersion(v.trim()); await syncDesktopMainWindowTitle(); })(); return () => { cancelled = true; }; diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 3677ced..a91e7f6 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -61,6 +61,13 @@ export function getStompSockJsUrl(): string { return path; } +/** Desktop — 순수 WebSocket STOMP (SockJS 미사용) */ +export function getStompNativeWebSocketUrl(): string { + const secureOrigin = getSecureApiOrigin().replace(/^http:\/\//i, 'https://'); + const wsOrigin = secureOrigin.replace(/^https:\/\//i, 'wss://'); + return `${wsOrigin}/api/ws/trading/ws`; +} + // UUID 생성 — HTTPS/localhost: crypto.randomUUID() 사용, HTTP: 폴리필 function generateUUID(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { diff --git a/frontend/src/utils/stompChartBroker.ts b/frontend/src/utils/stompChartBroker.ts index 8bc7957..1be482f 100644 --- a/frontend/src/utils/stompChartBroker.ts +++ b/frontend/src/utils/stompChartBroker.ts @@ -1,10 +1,11 @@ /** - * 차트·실시간 전략용 STOMP 단일 연결 (SockJS 1회). - * 훅마다 StompClient 를 만들면 "closed before connection is established" 가 반복된다. + * 차트·실시간 전략용 STOMP 단일 연결. + * 웹: SockJS 1회 / Desktop: 순수 WebSocket (cross-origin SockJS 세션 불가) */ import { Client as StompClient, IMessage, StompSubscription } from '@stomp/stompjs'; import SockJS from 'sockjs-client'; -import { getStompSockJsUrl } from './backendApi'; +import { getStompNativeWebSocketUrl, getStompSockJsUrl } from './backendApi'; +import { isDesktop } from './platform'; type MessageHandler = (msg: IMessage) => void; type ConnectionListener = (connected: boolean) => void; @@ -49,11 +50,12 @@ function resubscribeAll(): void { } } -function getOrCreateClient(): StompClient { - if (client) return client; +function stompConnectLabel(): string { + return isDesktop() ? getStompNativeWebSocketUrl() : getStompSockJsUrl(); +} - client = new StompClient({ - webSocketFactory: () => new SockJS(getStompSockJsUrl()), +function createStompClient(): StompClient { + const shared = { reconnectDelay: 5000, heartbeatIncoming: 10_000, heartbeatOutgoing: 10_000, @@ -68,11 +70,11 @@ function getOrCreateClient(): StompClient { entry.stompSub = undefined; } }, - onStompError: frame => { + onStompError: (frame: { headers?: { message?: string } }) => { console.warn('[stompChartBroker] STOMP error', frame.headers?.message ?? frame); }, - onWebSocketError: ev => { - console.warn('[stompChartBroker] WebSocket error', getStompSockJsUrl(), ev); + onWebSocketError: (ev: Event) => { + console.warn('[stompChartBroker] WebSocket error', stompConnectLabel(), ev); }, onWebSocketClose: () => { notifyConnection(false); @@ -80,7 +82,23 @@ function getOrCreateClient(): StompClient { entry.stompSub = undefined; } }, + }; + + if (isDesktop()) { + const brokerURL = getStompNativeWebSocketUrl(); + return new StompClient({ ...shared, brokerURL }); + } + + const sockJsUrl = getStompSockJsUrl(); + return new StompClient({ + ...shared, + webSocketFactory: () => new SockJS(sockJsUrl), }); +} + +function getOrCreateClient(): StompClient { + if (client) return client; + client = createStompClient(); return client; } @@ -96,7 +114,6 @@ function scheduleDisconnect(): void { disconnectTimer = null; if (handlerCount() > 0) return; if (!client?.active) return; - // 연결 수립 중 deactivate 하면 "closed before connection is established" 발생 if (!connected) { scheduleDisconnect(); return; diff --git a/packages/shared/src/api/backendApi.ts b/packages/shared/src/api/backendApi.ts index 30ab3f8..273ddef 100644 --- a/packages/shared/src/api/backendApi.ts +++ b/packages/shared/src/api/backendApi.ts @@ -101,6 +101,16 @@ export function getStompSockJsUrl(): string { return `${secureOrigin}/api/ws/trading`; } +/** + * Desktop Tauri — SockJS 없이 순수 WebSocket STOMP (/ws/trading/ws). + * cross-origin WebView에서 SockJS 세션·쿠키가 막히므로 Desktop 전용. + */ +export function getStompNativeWebSocketUrl(): string { + const secureOrigin = getSecureApiOrigin().replace(/^http:\/\//i, 'https://'); + const wsOrigin = secureOrigin.replace(/^https:\/\//i, 'wss://'); + return `${wsOrigin}/api/ws/trading/ws`; +} + // UUID 생성 — HTTPS/localhost: crypto.randomUUID() 사용, HTTP: 폴리필 function generateUUID(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index 175bf12..c4ef041 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -85,6 +85,7 @@ source "$ROOT/scripts/prepare-tauri-build.sh" npm install echo "--- Vite build (desktop) ---" +export DESKTOP_APP_VERSION="$VERSION" npm run build -w @goldenchart/desktop DESKTOP_DIR="$ROOT/desktop" diff --git a/scripts/bump-desktop-version-from-release.sh b/scripts/bump-desktop-version-from-release.sh index 3014593..07e4cf9 100755 --- a/scripts/bump-desktop-version-from-release.sh +++ b/scripts/bump-desktop-version-from-release.sh @@ -59,5 +59,11 @@ parts[2] += 1; const nv = parts.join('.'); conf.version = nv; fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n'); +const pkgPath = path.join(path.dirname(confPath), '..', 'package.json'); +try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + pkg.version = nv; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} catch { /* ignore */ } console.log(nv); " "$CONF" "$LATEST" "$UPDATES_DIR" "$BUILD_INFO"