diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 7b81515..c15512a 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -10,6 +10,7 @@ "core:window:allow-hide", "core:window:allow-show", "core:window:allow-set-focus", + "core:window:allow-set-title", "core:window:allow-start-dragging", "core:webview:allow-create-webview-window", "core:webview:allow-webview-close", diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 00c58a8..964d8d3 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -21,6 +21,12 @@ 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); + } + let show_i = MenuItemBuilder::with_id("show", "GoldenChart 열기").build(app)?; let quit_i = MenuItemBuilder::with_id("quit", "종료").build(app)?; let menu = MenuBuilder::new(app) diff --git a/desktop/src/StartupUpdateGate.tsx b/desktop/src/StartupUpdateGate.tsx index d51c0d4..7fd36b1 100644 --- a/desktop/src/StartupUpdateGate.tsx +++ b/desktop/src/StartupUpdateGate.tsx @@ -4,6 +4,7 @@ import { type DesktopUpdatePhase, type DesktopUpdateProgress, } from './bridge/updater'; +import { scheduleMainWindowTitleSync } from './setMainWindowTitle'; import '@frontend/App.css'; const PHASE_LABEL: Record = { @@ -143,6 +144,7 @@ export function StartupUpdateGate({ children }: { children: React.ReactNode }) { if (result.phase === 'error') { console.warn('[startup-update]', result.message); } + scheduleMainWindowTitleSync(); setReady(true); }); diff --git a/desktop/src/bridge/installDesktopBridge.ts b/desktop/src/bridge/installDesktopBridge.ts index 94af28e..ea95b69 100644 --- a/desktop/src/bridge/installDesktopBridge.ts +++ b/desktop/src/bridge/installDesktopBridge.ts @@ -1,6 +1,7 @@ import { openWidgetWindow, closeWidgetWindow, focusWidgetWindow } from './widgetWindows'; import { showNativeNotification } from './notifications'; import { checkForUpdates, getAppVersion, runDesktopUpdate } from './updater'; +import { syncMainWindowTitle } from '../setMainWindowTitle'; import type { DesktopBridge } from './types'; /** frontend App 로드 전 호출 — window.__goldenDesktopBridge 등록 */ @@ -13,6 +14,7 @@ export function installDesktopBridge(): void { checkForUpdates, runDesktopUpdate, getAppVersion, + syncMainWindowTitle, }; window.__goldenDesktopBridge = bridge; } diff --git a/desktop/src/bridge/types.ts b/desktop/src/bridge/types.ts index 6bf98cb..9cbd657 100644 --- a/desktop/src/bridge/types.ts +++ b/desktop/src/bridge/types.ts @@ -16,6 +16,7 @@ export interface DesktopBridge { onProgress?: (progress: DesktopUpdateProgress) => void, ) => Promise; getAppVersion: () => Promise; + syncMainWindowTitle: () => Promise; } declare global { diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index 672370c..7c0d352 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom/client'; import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared'; import { installDesktopBridge } from './bridge/installDesktopBridge'; import { ensureDesktopApiBase } from './ensureDesktopApiBase'; +import { scheduleMainWindowTitleSync, syncMainWindowTitle } from './setMainWindowTitle'; import { StartupUpdateGate } from './StartupUpdateGate'; import App from '@frontend/App'; @@ -13,6 +14,13 @@ async function bootstrap() { await initStorage(); ensureDesktopApiBase(); refreshApiBaseFromStorage(); + document.documentElement.classList.add('desktop-client'); + try { + await syncMainWindowTitle(); + } catch { + /* 권한·타이밍 이슈 — scheduleMainWindowTitleSync 가 재시도 */ + } + scheduleMainWindowTitleSync(); ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/desktop/src/setMainWindowTitle.ts b/desktop/src/setMainWindowTitle.ts new file mode 100644 index 0000000..afbb941 --- /dev/null +++ b/desktop/src/setMainWindowTitle.ts @@ -0,0 +1,40 @@ +import { getVersion } from '@tauri-apps/api/app'; +import { getCurrentWindow } from '@tauri-apps/api/window'; + +const APP_NAME = 'GoldenChart'; + +export function formatDesktopWindowTitle(version: string): string { + const v = version.trim(); + return v ? `${APP_NAME} (v${v})` : APP_NAME; +} + +/** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */ +export async function syncMainWindowTitle(): Promise { + try { + const version = await getVersion(); + const title = formatDesktopWindowTitle(version); + await getCurrentWindow().setTitle(title); + if (typeof document !== 'undefined') { + document.title = title; + } + return title; + } catch (e) { + console.warn('[desktop] syncMainWindowTitle failed:', e); + throw e; + } +} + +/** WebView 로드 후 macOS가 document.title로 타이틀을 덮어쓸 수 있어 재시도 */ +export function scheduleMainWindowTitleSync(): void { + const delays = [0, 150, 600, 2000]; + for (const ms of delays) { + window.setTimeout(() => { + void syncMainWindowTitle().catch(() => {}); + }, ms); + } + void getCurrentWindow() + .listen('tauri://focus', () => { + void syncMainWindowTitle().catch(() => {}); + }) + .catch(() => {}); +} diff --git a/desktop/src/vite-env.d.ts b/desktop/src/vite-env.d.ts index 11f02fe..7ca71c4 100644 --- a/desktop/src/vite-env.d.ts +++ b/desktop/src/vite-env.d.ts @@ -1 +1,3 @@ /// + +declare const __DESKTOP_APP_VERSION__: string; diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index e8883be..c400b36 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; +import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -9,6 +10,16 @@ const sharedRoot = path.resolve(__dirname, '../packages/shared/src'); const host = process.env.TAURI_DEV_HOST; +function readDesktopAppVersion(): string { + try { + const confPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json'); + const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string }; + return String(conf.version ?? '0.0.0'); + } catch { + return '0.0.0'; + } +} + export default defineConfig({ /** Tauri 패키지 앱 — 절대 경로(/assets) 사용 시 빈 화면 */ base: './', @@ -18,6 +29,7 @@ export default defineConfig({ /** desktop은 항상 exdev 서버 (localhost 금지) */ 'import.meta.env.VITE_API_BASE_URL': JSON.stringify('http://exdev.co.kr/api'), __DESKTOP_CLIENT__: 'true', + __DESKTOP_APP_VERSION__: JSON.stringify(readDesktopAppVersion()), }, envDir: __dirname, plugins: [react()], diff --git a/frontend/src/App.css b/frontend/src/App.css index f3d8c96..758a695 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -206,6 +206,11 @@ html.theme-blue { white-space: nowrap; } +html.desktop-client .tmb-logo-text { + font-size: 13px; + font-weight: 600; +} + /* 구분선 */ .tmb-divider { width: 1px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 43ee3cf..b0c897d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,6 +36,7 @@ import type { LoginResponse } from './utils/backendApi'; import { invalidateAppSettingsCache } from './hooks/useAppSettings'; import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings'; import { isDesktop } from './utils/platform'; +import { syncDesktopMainWindowTitle } from './utils/desktopBridge'; import { writeDesktopSync } from './utils/desktopSync'; const LAST_MENU_KEY = 'gc_last_menu'; @@ -513,6 +514,11 @@ function AppMainContent({ function App() { const [sessionKey, setSessionKey] = useState(0); + useEffect(() => { + if (!isDesktop()) return; + void syncDesktopMainWindowTitle(); + }, []); + const [appEntered, setAppEnteredState] = useState(() => isAppEntered()); const [authUser, setAuthUser] = useState(() => isAppEntered() ? getAuthSession() : null, diff --git a/frontend/src/components/TopMenuBar.tsx b/frontend/src/components/TopMenuBar.tsx index 9116de6..b04827d 100644 --- a/frontend/src/components/TopMenuBar.tsx +++ b/frontend/src/components/TopMenuBar.tsx @@ -10,6 +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 DesktopUpdateModal from './DesktopUpdateModal'; import TopMenuBarNavGroups from './TopMenuBarNavGroups'; @@ -297,6 +298,7 @@ export const TopMenuBar = memo(function TopMenuBar({ const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement); const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false); const desktopClient = isDesktop(); + const desktopAppVersion = useDesktopAppTitle(); useEffect(() => { const sync = () => setIsFullscreen(!!document.fullscreenElement); @@ -317,7 +319,9 @@ export const TopMenuBar = memo(function TopMenuBar({ - GoldenChart + + {desktopClient ? formatDesktopAppLabel(desktopAppVersion) : 'GoldenChart'} +
diff --git a/frontend/src/hooks/useDesktopAppTitle.ts b/frontend/src/hooks/useDesktopAppTitle.ts new file mode 100644 index 0000000..ae7f6b2 --- /dev/null +++ b/frontend/src/hooks/useDesktopAppTitle.ts @@ -0,0 +1,37 @@ +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; +} + +/** Desktop — 네이티브 창 타이틀·document.title을 GoldenChart (vX.Y.Z) 로 맞춤 */ +export function useDesktopAppTitle(): string | null { + const [version, setVersion] = useState(() => + isDesktop() ? readBakedDesktopVersion() : null, + ); + + useEffect(() => { + if (!isDesktop()) return; + let cancelled = false; + void (async () => { + const v = (await getDesktopAppVersion()) ?? readBakedDesktopVersion(); + if (cancelled || !v) return; + setVersion(v); + await syncDesktopMainWindowTitle(); + })(); + return () => { cancelled = true; }; + }, []); + + return version; +} + +export function formatDesktopAppLabel(version: string | null | undefined): string { + if (!version?.trim()) return 'GoldenChart'; + return `GoldenChart (v${version.trim()})`; +} diff --git a/frontend/src/styles/analysisReportPage.css b/frontend/src/styles/analysisReportPage.css index 9c078ba..35cba8a 100644 --- a/frontend/src/styles/analysisReportPage.css +++ b/frontend/src/styles/analysisReportPage.css @@ -63,6 +63,8 @@ overflow: hidden; padding: 10px 12px 12px; background: var(--se-center-bg, var(--se-bg)); + width: 100%; + box-sizing: border-box; } /* 좌·중·우 패널 동일 높이 — flex 체인 */ @@ -193,6 +195,8 @@ flex: 1; min-height: 0; height: 100%; + width: 100%; + max-width: none; display: flex; flex-direction: column; border-radius: 14px; diff --git a/frontend/src/utils/desktopBridge.ts b/frontend/src/utils/desktopBridge.ts index c06a2b3..ba7f263 100644 --- a/frontend/src/utils/desktopBridge.ts +++ b/frontend/src/utils/desktopBridge.ts @@ -37,6 +37,7 @@ export interface DesktopBridge { onProgress?: (progress: DesktopUpdateProgress) => void, ) => Promise; getAppVersion: () => Promise; + syncMainWindowTitle: () => Promise; } declare global { @@ -85,3 +86,13 @@ export async function getDesktopAppVersion(): Promise { return null; } } + +export async function syncDesktopMainWindowTitle(): Promise { + const bridge = getDesktopBridge(); + if (!bridge?.syncMainWindowTitle) return null; + try { + return await bridge.syncMainWindowTitle(); + } catch { + return null; + } +}