From 5045a548a4ef02fa3f790ce65c376e4d26794129 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 19:20:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EB=B2=84=EC=A0=84=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desktop/src-tauri/src/lib.rs | 28 ++++++++++++--- desktop/src/setMainWindowTitle.ts | 46 +++++++++++++++++++----- desktop/vite.config.ts | 20 ++++++++++- frontend/src/App.css | 8 +++++ frontend/src/components/TopMenuBar.tsx | 9 ++--- frontend/src/hooks/useDesktopAppTitle.ts | 13 ++----- frontend/src/utils/desktopAppVersion.ts | 25 +++++++++++++ frontend/src/utils/platform.ts | 4 ++- packages/shared/src/platform.ts | 4 ++- scripts/build-desktop.sh | 11 ++++++ 10 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 frontend/src/utils/desktopAppVersion.ts diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 964d8d3..c4d5c16 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -4,12 +4,24 @@ use tauri::{ AppHandle, Manager, RunEvent, }; +fn format_main_window_title(version: &str) -> String { + format!("GoldenChart (v{version})") +} + +fn apply_main_window_title(app: &AppHandle) { + let title = format_main_window_title(&app.package_info().version.to_string()); + if let Some(w) = app.get_webview_window("main") { + let _ = w.set_title(&title); + } +} + fn show_main_window(app: &AppHandle) { if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); } + apply_main_window_title(app); } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -21,11 +33,7 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { - let version = app.package_info().version.to_string(); - let main_title = format!("GoldenChart (v{version})"); - if let Some(w) = app.get_webview_window("main") { - let _ = w.set_title(&main_title); - } + apply_main_window_title(app.handle()); let show_i = MenuItemBuilder::with_id("show", "GoldenChart 열기").build(app)?; let quit_i = MenuItemBuilder::with_id("quit", "종료").build(app)?; @@ -63,10 +71,19 @@ pub fn run() { show_main_window(app.handle()); Ok(()) }) + .on_page_load(|webview, _payload| { + if webview.label() != "main" { + return; + } + apply_main_window_title(webview.app_handle()); + }) .on_window_event(|window, event| { if window.label() != "main" { return; } + if matches!(event, tauri::WindowEvent::Focused(true)) { + apply_main_window_title(window.app_handle()); + } if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); let _ = window.hide(); @@ -75,6 +92,7 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| match event { + RunEvent::Ready => apply_main_window_title(&app_handle), #[cfg(target_os = "macos")] RunEvent::Reopen { .. } => show_main_window(&app_handle), RunEvent::ExitRequested { api, .. } => { diff --git a/desktop/src/setMainWindowTitle.ts b/desktop/src/setMainWindowTitle.ts index afbb941..bfdf4e3 100644 --- a/desktop/src/setMainWindowTitle.ts +++ b/desktop/src/setMainWindowTitle.ts @@ -1,32 +1,60 @@ import { getVersion } from '@tauri-apps/api/app'; import { getCurrentWindow } from '@tauri-apps/api/window'; +import { + formatDesktopWindowTitle, + readBakedDesktopAppVersion, +} from '@frontend/utils/desktopAppVersion'; -const APP_NAME = 'GoldenChart'; - -export function formatDesktopWindowTitle(version: string): string { - const v = version.trim(); - return v ? `${APP_NAME} (v${v})` : APP_NAME; -} +export { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion'; /** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */ export async function syncMainWindowTitle(): Promise { try { - const version = await getVersion(); + const version = (await getVersion()) || readBakedDesktopAppVersion() || ''; const title = formatDesktopWindowTitle(version); await getCurrentWindow().setTitle(title); - if (typeof document !== 'undefined') { + 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; } } +let titleGuardInstalled = false; + +/** macOS WebKit — document.title 변경 시 네이티브 타이틀을 다시 맞춤 */ +export function installDocumentTitleGuard(): void { + if (titleGuardInstalled || typeof document === 'undefined') return; + titleGuardInstalled = true; + + const rebalance = () => { + void syncMainWindowTitle().catch(() => {}); + }; + + const titleEl = document.querySelector('title'); + if (titleEl) { + const observer = new MutationObserver(() => { + if (!document.title.includes('(v')) rebalance(); + }); + observer.observe(titleEl, { childList: true, characterData: true, subtree: true }); + } + + window.setInterval(() => { + if (!document.title.includes('(v')) rebalance(); + }, 3000); +} + /** WebView 로드 후 macOS가 document.title로 타이틀을 덮어쓸 수 있어 재시도 */ export function scheduleMainWindowTitleSync(): void { - const delays = [0, 150, 600, 2000]; + installDocumentTitleGuard(); + const delays = [0, 50, 150, 400, 1000, 2500, 5000]; for (const ms of delays) { window.setTimeout(() => { void syncMainWindowTitle().catch(() => {}); diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index c400b36..6bd5f4a 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -20,6 +20,24 @@ function readDesktopAppVersion(): string { } } +const desktopVersion = readDesktopAppVersion(); +const desktopWindowTitle = `GoldenChart (v${desktopVersion})`; + +function desktopHtmlTitlePlugin() { + return { + name: 'desktop-html-title', + transformIndexHtml(html: string, ctx: { filename: string }) { + if (!ctx.filename.endsWith('index.html')) return html; + return html + .replace(/[^<]*<\/title>/i, `<title>${desktopWindowTitle}`) + .replace( + '', + ``, + ); + }, + }; +} + export default defineConfig({ /** Tauri 패키지 앱 — 절대 경로(/assets) 사용 시 빈 화면 */ base: './', @@ -32,7 +50,7 @@ export default defineConfig({ __DESKTOP_APP_VERSION__: JSON.stringify(readDesktopAppVersion()), }, envDir: __dirname, - plugins: [react()], + plugins: [react(), desktopHtmlTitlePlugin()], resolve: { dedupe: ['react', 'react-dom', 'lightweight-charts'], alias: { diff --git a/frontend/src/App.css b/frontend/src/App.css index 758a695..eeba2fe 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -211,6 +211,14 @@ html.desktop-client .tmb-logo-text { font-weight: 600; } +html.desktop-client .tmb-logo-version { + font-size: 12px; + font-weight: 500; + color: var(--text3); + letter-spacing: 0; + white-space: nowrap; +} + /* 구분선 */ .tmb-divider { width: 1px; diff --git a/frontend/src/components/TopMenuBar.tsx b/frontend/src/components/TopMenuBar.tsx index b04827d..8ae64fb 100644 --- a/frontend/src/components/TopMenuBar.tsx +++ b/frontend/src/components/TopMenuBar.tsx @@ -10,7 +10,7 @@ import type { AuthSession } from '../utils/auth'; import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout'; import { canAccessMenu } from '../utils/permissions'; import { isDesktop } from '../utils/platform'; -import { useDesktopAppTitle, formatDesktopAppLabel } from '../hooks/useDesktopAppTitle'; +import { useDesktopAppTitle } from '../hooks/useDesktopAppTitle'; import DesktopUpdateModal from './DesktopUpdateModal'; import TopMenuBarNavGroups from './TopMenuBarNavGroups'; @@ -319,9 +319,10 @@ export const TopMenuBar = memo(function TopMenuBar({ - - {desktopClient ? formatDesktopAppLabel(desktopAppVersion) : 'GoldenChart'} - + GoldenChart + {desktopClient && desktopAppVersion ? ( + (v{desktopAppVersion}) + ) : null}
diff --git a/frontend/src/hooks/useDesktopAppTitle.ts b/frontend/src/hooks/useDesktopAppTitle.ts index ae7f6b2..9dc2091 100644 --- a/frontend/src/hooks/useDesktopAppTitle.ts +++ b/frontend/src/hooks/useDesktopAppTitle.ts @@ -1,26 +1,19 @@ import { useEffect, useState } from 'react'; import { isDesktop } from '../utils/platform'; import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge'; - -declare const __DESKTOP_APP_VERSION__: string | undefined; - -function readBakedDesktopVersion(): string | null { - if (typeof __DESKTOP_APP_VERSION__ === 'undefined') return null; - const v = __DESKTOP_APP_VERSION__.trim(); - return v || null; -} +import { readBakedDesktopAppVersion } from '../utils/desktopAppVersion'; /** Desktop — 네이티브 창 타이틀·document.title을 GoldenChart (vX.Y.Z) 로 맞춤 */ export function useDesktopAppTitle(): string | null { const [version, setVersion] = useState(() => - isDesktop() ? readBakedDesktopVersion() : null, + isDesktop() ? readBakedDesktopAppVersion() : null, ); useEffect(() => { if (!isDesktop()) return; let cancelled = false; void (async () => { - const v = (await getDesktopAppVersion()) ?? readBakedDesktopVersion(); + const v = (await getDesktopAppVersion()) ?? readBakedDesktopAppVersion(); if (cancelled || !v) return; setVersion(v); await syncDesktopMainWindowTitle(); diff --git a/frontend/src/utils/desktopAppVersion.ts b/frontend/src/utils/desktopAppVersion.ts new file mode 100644 index 0000000..9c6564c --- /dev/null +++ b/frontend/src/utils/desktopAppVersion.ts @@ -0,0 +1,25 @@ +declare const __DESKTOP_APP_VERSION__: string | undefined; + +declare global { + interface Window { + __GC_APP_VERSION__?: string; + } +} + +/** Desktop Vite 빌드·index.html 인라인 스크립트에 주입된 앱 버전 */ +export function readBakedDesktopAppVersion(): string | null { + if (typeof window !== 'undefined') { + const fromWindow = window.__GC_APP_VERSION__?.trim(); + if (fromWindow) return fromWindow; + } + if (typeof __DESKTOP_APP_VERSION__ !== 'undefined') { + const fromDefine = __DESKTOP_APP_VERSION__.trim(); + if (fromDefine) return fromDefine; + } + return null; +} + +export function formatDesktopWindowTitle(version: string | null | undefined): string { + const v = version?.trim(); + return v ? `GoldenChart (v${v})` : 'GoldenChart'; +} diff --git a/frontend/src/utils/platform.ts b/frontend/src/utils/platform.ts index f6bcff4..b3e33cb 100644 --- a/frontend/src/utils/platform.ts +++ b/frontend/src/utils/platform.ts @@ -2,7 +2,9 @@ export type RuntimePlatform = 'web' | 'desktop' | 'mobile'; export function isDesktop(): boolean { - return typeof window !== 'undefined' && '__TAURI__' in window; + if (typeof window === 'undefined') return false; + if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) return true; + return document.documentElement.classList.contains('desktop-client'); } export function isMobileCapacitor(): boolean { diff --git a/packages/shared/src/platform.ts b/packages/shared/src/platform.ts index 900acec..3f5c13d 100644 --- a/packages/shared/src/platform.ts +++ b/packages/shared/src/platform.ts @@ -2,7 +2,9 @@ export type RuntimePlatform = 'web' | 'desktop' | 'mobile'; export function isDesktop(): boolean { - return typeof window !== 'undefined' && '__TAURI__' in window; + if (typeof window === 'undefined') return false; + if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) return true; + return document.documentElement.classList.contains('desktop-client'); } export function isMobileCapacitor(): boolean { diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index 4c3e1f9..175bf12 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -64,6 +64,17 @@ fi VERSION="$(node -p "require('$CONF').version")" echo "Version: $VERSION" +node -e " +const fs = require('fs'); +const confPath = process.argv[1]; +const version = process.argv[2]; +const title = 'GoldenChart (v' + version + ')'; +const conf = JSON.parse(fs.readFileSync(confPath, 'utf8')); +if (conf.app?.windows?.[0]) conf.app.windows[0].title = title; +fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n'); +console.log('window title →', title); +" "$CONF" "$VERSION" + if [[ -f "$ROOT/scripts/patch-tauri-updater-pubkey.sh" ]]; then chmod +x "$ROOT/scripts/patch-tauri-updater-pubkey.sh" fi