앱 실시간 갱신 수정
This commit is contained in:
@@ -35,8 +35,12 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
// Tauri Desktop — SockJS(XHR·쿠키)는 cross-origin WebView에서 실패 → 순수 WebSocket
|
||||||
|
registry.addEndpoint("/ws/trading/ws")
|
||||||
|
.setAllowedOriginPatterns("*");
|
||||||
|
|
||||||
registry.addEndpoint("/ws/trading")
|
registry.addEndpoint("/ws/trading")
|
||||||
.setAllowedOriginPatterns("*")
|
.setAllowedOriginPatterns("*")
|
||||||
.withSockJS(); // SockJS fallback (브라우저 환경 대비)
|
.withSockJS(); // SockJS fallback (브라우저 동일 오리진)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
import { getVersion } from '@tauri-apps/api/app';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import {
|
import { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion';
|
||||||
formatDesktopWindowTitle,
|
|
||||||
readBakedDesktopAppVersion,
|
|
||||||
} from '@frontend/utils/desktopAppVersion';
|
|
||||||
|
|
||||||
export { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion';
|
export { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion';
|
||||||
|
|
||||||
/** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */
|
/** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */
|
||||||
export async function syncMainWindowTitle(): Promise<string> {
|
export async function syncMainWindowTitle(): Promise<string> {
|
||||||
try {
|
const version = (await getVersion()).trim();
|
||||||
const version = (await getVersion()) || readBakedDesktopAppVersion() || '';
|
const title = formatDesktopWindowTitle(version);
|
||||||
const title = formatDesktopWindowTitle(version);
|
await getCurrentWindow().setTitle(title);
|
||||||
await getCurrentWindow().setTitle(title);
|
if (typeof document !== 'undefined' && document.title !== title) {
|
||||||
if (typeof document !== 'undefined' && document.title !== title) {
|
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;
|
|
||||||
}
|
}
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleGuardInstalled = false;
|
let titleGuardInstalled = false;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const sharedRoot = path.resolve(__dirname, '../packages/shared/src');
|
|||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
function readDesktopAppVersion(): string {
|
function readDesktopAppVersion(): string {
|
||||||
|
const fromEnv = process.env.DESKTOP_APP_VERSION?.trim();
|
||||||
|
if (fromEnv) return fromEnv;
|
||||||
try {
|
try {
|
||||||
const confPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json');
|
const confPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json');
|
||||||
const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string };
|
const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string };
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { isDesktop } from '../utils/platform';
|
import { isDesktop } from '../utils/platform';
|
||||||
import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge';
|
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 {
|
export function useDesktopAppTitle(): string | null {
|
||||||
const [version, setVersion] = useState<string | null>(() =>
|
const [version, setVersion] = useState<string | null>(null);
|
||||||
isDesktop() ? readBakedDesktopAppVersion() : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesktop()) return;
|
if (!isDesktop()) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const v = (await getDesktopAppVersion()) ?? readBakedDesktopAppVersion();
|
const v = await getDesktopAppVersion();
|
||||||
if (cancelled || !v) return;
|
if (cancelled || !v?.trim()) return;
|
||||||
setVersion(v);
|
setVersion(v.trim());
|
||||||
await syncDesktopMainWindowTitle();
|
await syncDesktopMainWindowTitle();
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ export function getStompSockJsUrl(): string {
|
|||||||
return path;
|
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: 폴리필
|
// UUID 생성 — HTTPS/localhost: crypto.randomUUID() 사용, HTTP: 폴리필
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 차트·실시간 전략용 STOMP 단일 연결 (SockJS 1회).
|
* 차트·실시간 전략용 STOMP 단일 연결.
|
||||||
* 훅마다 StompClient 를 만들면 "closed before connection is established" 가 반복된다.
|
* 웹: SockJS 1회 / Desktop: 순수 WebSocket (cross-origin SockJS 세션 불가)
|
||||||
*/
|
*/
|
||||||
import { Client as StompClient, IMessage, StompSubscription } from '@stomp/stompjs';
|
import { Client as StompClient, IMessage, StompSubscription } from '@stomp/stompjs';
|
||||||
import SockJS from 'sockjs-client';
|
import SockJS from 'sockjs-client';
|
||||||
import { getStompSockJsUrl } from './backendApi';
|
import { getStompNativeWebSocketUrl, getStompSockJsUrl } from './backendApi';
|
||||||
|
import { isDesktop } from './platform';
|
||||||
|
|
||||||
type MessageHandler = (msg: IMessage) => void;
|
type MessageHandler = (msg: IMessage) => void;
|
||||||
type ConnectionListener = (connected: boolean) => void;
|
type ConnectionListener = (connected: boolean) => void;
|
||||||
@@ -49,11 +50,12 @@ function resubscribeAll(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateClient(): StompClient {
|
function stompConnectLabel(): string {
|
||||||
if (client) return client;
|
return isDesktop() ? getStompNativeWebSocketUrl() : getStompSockJsUrl();
|
||||||
|
}
|
||||||
|
|
||||||
client = new StompClient({
|
function createStompClient(): StompClient {
|
||||||
webSocketFactory: () => new SockJS(getStompSockJsUrl()),
|
const shared = {
|
||||||
reconnectDelay: 5000,
|
reconnectDelay: 5000,
|
||||||
heartbeatIncoming: 10_000,
|
heartbeatIncoming: 10_000,
|
||||||
heartbeatOutgoing: 10_000,
|
heartbeatOutgoing: 10_000,
|
||||||
@@ -68,11 +70,11 @@ function getOrCreateClient(): StompClient {
|
|||||||
entry.stompSub = undefined;
|
entry.stompSub = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStompError: frame => {
|
onStompError: (frame: { headers?: { message?: string } }) => {
|
||||||
console.warn('[stompChartBroker] STOMP error', frame.headers?.message ?? frame);
|
console.warn('[stompChartBroker] STOMP error', frame.headers?.message ?? frame);
|
||||||
},
|
},
|
||||||
onWebSocketError: ev => {
|
onWebSocketError: (ev: Event) => {
|
||||||
console.warn('[stompChartBroker] WebSocket error', getStompSockJsUrl(), ev);
|
console.warn('[stompChartBroker] WebSocket error', stompConnectLabel(), ev);
|
||||||
},
|
},
|
||||||
onWebSocketClose: () => {
|
onWebSocketClose: () => {
|
||||||
notifyConnection(false);
|
notifyConnection(false);
|
||||||
@@ -80,7 +82,23 @@ function getOrCreateClient(): StompClient {
|
|||||||
entry.stompSub = undefined;
|
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;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +114,6 @@ function scheduleDisconnect(): void {
|
|||||||
disconnectTimer = null;
|
disconnectTimer = null;
|
||||||
if (handlerCount() > 0) return;
|
if (handlerCount() > 0) return;
|
||||||
if (!client?.active) return;
|
if (!client?.active) return;
|
||||||
// 연결 수립 중 deactivate 하면 "closed before connection is established" 발생
|
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
scheduleDisconnect();
|
scheduleDisconnect();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ export function getStompSockJsUrl(): string {
|
|||||||
return `${secureOrigin}/api/ws/trading`;
|
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: 폴리필
|
// UUID 생성 — HTTPS/localhost: crypto.randomUUID() 사용, HTTP: 폴리필
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ source "$ROOT/scripts/prepare-tauri-build.sh"
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
echo "--- Vite build (desktop) ---"
|
echo "--- Vite build (desktop) ---"
|
||||||
|
export DESKTOP_APP_VERSION="$VERSION"
|
||||||
npm run build -w @goldenchart/desktop
|
npm run build -w @goldenchart/desktop
|
||||||
|
|
||||||
DESKTOP_DIR="$ROOT/desktop"
|
DESKTOP_DIR="$ROOT/desktop"
|
||||||
|
|||||||
@@ -59,5 +59,11 @@ parts[2] += 1;
|
|||||||
const nv = parts.join('.');
|
const nv = parts.join('.');
|
||||||
conf.version = nv;
|
conf.version = nv;
|
||||||
fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
|
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);
|
console.log(nv);
|
||||||
" "$CONF" "$LATEST" "$UPDATES_DIR" "$BUILD_INFO"
|
" "$CONF" "$LATEST" "$UPDATES_DIR" "$BUILD_INFO"
|
||||||
|
|||||||
Reference in New Issue
Block a user