버전명 표시
This commit is contained in:
@@ -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단계: 서비스 ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 구분선 */
|
/* 구분선 */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
void (async () => {
|
||||||
const v = await getDesktopAppVersion();
|
const v = await getDesktopAppVersion();
|
||||||
if (cancelled || !v?.trim()) return;
|
if (cancelled) return;
|
||||||
|
if (v?.trim()) {
|
||||||
setVersion(v.trim());
|
setVersion(v.trim());
|
||||||
await syncDesktopMainWindowTitle();
|
await syncDesktopMainWindowTitle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVersion(readBakedAppVersion());
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const published = await fetchPublishedAppVersion();
|
||||||
|
if (cancelled) return;
|
||||||
|
setVersion(published ?? readBakedAppVersion());
|
||||||
|
})();
|
||||||
|
|
||||||
|
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()})`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
Reference in New Issue
Block a user