버전명 표시

This commit is contained in:
Macbook
2026-06-15 10:31:44 +09:00
parent 0e09825cab
commit 6481a432fd
7 changed files with 112 additions and 17 deletions
+2
View File
@@ -10,7 +10,9 @@ RUN npm ci
# Docker 환경에서 /api 는 nginx 가 backend:8080 으로 프록시하므로 그대로 사용 # Docker 환경에서 /api 는 nginx 가 backend:8080 으로 프록시하므로 그대로 사용
COPY . . COPY . .
ARG VITE_API_BASE_URL=/api ARG VITE_API_BASE_URL=/api
ARG VITE_APP_VERSION=
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
ENV VITE_APP_VERSION=${VITE_APP_VERSION}
RUN npm run build RUN npm run build
# ── 2단계: 서비스 ───────────────────────────────────────────────────────────── # ── 2단계: 서비스 ─────────────────────────────────────────────────────────────
+8 -4
View File
@@ -206,6 +206,14 @@ html.theme-blue {
white-space: nowrap; white-space: nowrap;
} }
.tmb-logo-version {
font-size: 12px;
font-weight: 500;
color: var(--text3);
letter-spacing: 0;
white-space: nowrap;
}
html.desktop-client .tmb-logo-text { html.desktop-client .tmb-logo-text {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
@@ -213,10 +221,6 @@ html.desktop-client .tmb-logo-text {
html.desktop-client .tmb-logo-version { html.desktop-client .tmb-logo-version {
font-size: 12px; font-size: 12px;
font-weight: 500;
color: var(--text3);
letter-spacing: 0;
white-space: nowrap;
} }
/* 구분선 */ /* 구분선 */
+4 -4
View File
@@ -10,7 +10,7 @@ import type { AuthSession } from '../utils/auth';
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout'; import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
import { canAccessMenu } from '../utils/permissions'; import { canAccessMenu } from '../utils/permissions';
import { isDesktop } from '../utils/platform'; import { isDesktop } from '../utils/platform';
import { useDesktopAppTitle } from '../hooks/useDesktopAppTitle'; import { useAppTitleBarVersion } from '../hooks/useDesktopAppTitle';
import DesktopUpdateModal from './DesktopUpdateModal'; import DesktopUpdateModal from './DesktopUpdateModal';
import TopMenuBarNavGroups from './TopMenuBarNavGroups'; import TopMenuBarNavGroups from './TopMenuBarNavGroups';
@@ -298,7 +298,7 @@ export const TopMenuBar = memo(function TopMenuBar({
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement); const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false); const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
const desktopClient = isDesktop(); const desktopClient = isDesktop();
const desktopAppVersion = useDesktopAppTitle(); const appVersion = useAppTitleBarVersion();
useEffect(() => { useEffect(() => {
const sync = () => setIsFullscreen(!!document.fullscreenElement); const sync = () => setIsFullscreen(!!document.fullscreenElement);
@@ -320,8 +320,8 @@ export const TopMenuBar = memo(function TopMenuBar({
<polyline points="3,14 7,9 10,12 14,6 17,6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/> <polyline points="3,14 7,9 10,12 14,6 17,6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
</svg> </svg>
<span className="tmb-logo-text">GoldenChart</span> <span className="tmb-logo-text">GoldenChart</span>
{desktopClient && desktopAppVersion ? ( {appVersion ? (
<span className="tmb-logo-version">(v{desktopAppVersion})</span> <span className="tmb-logo-version">(v{appVersion})</span>
) : null} ) : null}
</div> </div>
+30 -9
View File
@@ -1,26 +1,47 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { fetchPublishedAppVersion, readBakedAppVersion } from '../utils/appVersion';
import { isDesktop } from '../utils/platform'; import { isDesktop } from '../utils/platform';
import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge'; import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge';
/** Desktop Tauri getVersion() 기준 (설치된 앱 버전). baked 0.1.4 등 git 고정값 사용 안 함 */ /** 타이틀바 GoldenChart 옆 (vX.Y.Z) — Desktop: Tauri, Web: latest.json */
export function useDesktopAppTitle(): string | null { export function useAppTitleBarVersion(): string | null {
const [version, setVersion] = useState<string | null>(null); const desktop = isDesktop();
const [version, setVersion] = useState<string | null>(() => readBakedAppVersion());
useEffect(() => { useEffect(() => {
if (!isDesktop()) return;
let cancelled = false; let cancelled = false;
if (desktop) {
void (async () => {
const v = await getDesktopAppVersion();
if (cancelled) return;
if (v?.trim()) {
setVersion(v.trim());
await syncDesktopMainWindowTitle();
return;
}
setVersion(readBakedAppVersion());
})();
return () => { cancelled = true; };
}
void (async () => { void (async () => {
const v = await getDesktopAppVersion(); const published = await fetchPublishedAppVersion();
if (cancelled || !v?.trim()) return; if (cancelled) return;
setVersion(v.trim()); setVersion(published ?? readBakedAppVersion());
await syncDesktopMainWindowTitle();
})(); })();
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, [desktop]);
return version; return version;
} }
/** @deprecated useAppTitleBarVersion 사용 */
export function useDesktopAppTitle(): string | null {
return useAppTitleBarVersion();
}
export function formatDesktopAppLabel(version: string | null | undefined): string { export function formatDesktopAppLabel(version: string | null | undefined): string {
if (!version?.trim()) return 'GoldenChart'; if (!version?.trim()) return 'GoldenChart';
return `GoldenChart (v${version.trim()})`; return `GoldenChart (v${version.trim()})`;
+23
View File
@@ -0,0 +1,23 @@
import { readBakedDesktopAppVersion } from './desktopAppVersion';
const LATEST_JSON_PATH = '/desktop/updates/latest.json';
/** Vite 빌드·desktop 번들에 주입된 앱 버전 */
export function readBakedAppVersion(): string | null {
const fromVite = import.meta.env.VITE_APP_VERSION?.trim();
if (fromVite) return fromVite;
return readBakedDesktopAppVersion();
}
/** 서버에 배포된 desktop latest.json — 웹 타이틀바 런타임 버전 */
export async function fetchPublishedAppVersion(): Promise<string | null> {
try {
const res = await fetch(LATEST_JSON_PATH, { cache: 'no-store' });
if (!res.ok) return null;
const data = (await res.json()) as { version?: string };
const v = data.version?.trim();
return v || null;
} catch {
return null;
}
}
+9
View File
@@ -1 +1,10 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION?: string;
readonly VITE_API_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+36
View File
@@ -1,6 +1,39 @@
import { defineConfig, createLogger } from 'vite'; import { defineConfig, createLogger } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
import { readFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
function readAppVersionForBuild(): string {
const fromEnv =
process.env.VITE_APP_VERSION?.trim()
|| process.env.FRONTEND_APP_VERSION?.trim()
|| process.env.DESKTOP_APP_VERSION?.trim();
if (fromEnv) return fromEnv;
try {
const latestPath = path.resolve(__dirname, 'public/desktop/updates/latest.json');
const latest = JSON.parse(readFileSync(latestPath, 'utf8')) as { version?: string };
const fromLatest = latest.version?.trim();
if (fromLatest) return fromLatest;
} catch {
/* optional */
}
try {
const confPath = path.resolve(repoRoot, 'desktop/src-tauri/tauri.conf.json');
const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string };
return String(conf.version ?? '').trim();
} catch {
return '';
}
}
const appVersion = readAppVersionForBuild();
// WebSocket proxy 재연결/종료 시 발생하는 ECONNRESET·EPIPE 는 정상 TCP 이벤트이므로 로그 억제 // WebSocket proxy 재연결/종료 시 발생하는 ECONNRESET·EPIPE 는 정상 TCP 이벤트이므로 로그 억제
const SUPPRESS = ['ECONNRESET', 'EPIPE', 'ws proxy socket error']; const SUPPRESS = ['ECONNRESET', 'EPIPE', 'ws proxy socket error'];
@@ -13,6 +46,9 @@ logger.error = (msg, opts) => {
export default defineConfig({ export default defineConfig({
customLogger: logger, customLogger: logger,
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
},
plugins: [ plugins: [
react(), react(),
visualizer({ filename: 'dist/stats.html', open: false, gzipSize: true, brotliSize: true }), visualizer({ filename: 'dist/stats.html', open: false, gzipSize: true, brotliSize: true }),