앱 실시간 갱신 수정

This commit is contained in:
Macbook
2026-06-14 19:41:49 +09:00
parent c105ae209b
commit ad6347f2a2
9 changed files with 71 additions and 39 deletions
@@ -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 (브라우저 동일 오리진)
}
}
+7 -19
View File
@@ -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<string> {
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;
+2
View File
@@ -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 };
+5 -8
View File
@@ -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<string | null>(() =>
isDesktop() ? readBakedDesktopAppVersion() : null,
);
const [version, setVersion] = useState<string | null>(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; };
+7
View File
@@ -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') {
+28 -11
View File
@@ -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;
+10
View File
@@ -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') {
+1
View File
@@ -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"
@@ -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"