desktop 앱 적용

This commit is contained in:
Macbook
2026-06-14 10:15:54 +09:00
parent 7a2f65088c
commit 86b99d8c10
81 changed files with 8164 additions and 37 deletions
+158
View File
@@ -0,0 +1,158 @@
import './forceServerApi';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingWidgetWindow';
import {
defaultGridFr,
type FloatingWidgetInstance,
} from '@frontend/types/floatingWidget';
import type { Theme } from '@frontend/types';
import type { WidgetSlot } from '@frontend/types/widgetDashboard';
import { useMarketTicker } from '@frontend/hooks/useMarketTicker';
import {
loadPaperSummary,
loadPaperTrades,
loadStrategies,
type PaperSummaryDto,
type PaperTradeDto,
type StrategyDto,
} from '@frontend/utils/backendApi';
import { PAPER_TRADES_CHANGED_EVENT } from '@frontend/utils/paperTradeEvents';
import { WidgetDashboardProvider } from '@frontend/widgets/WidgetDashboardContext';
import { readDesktopSync, subscribeDesktopSync } from '@frontend/utils/desktopSync';
import '@frontend/styles/paperDashboard.css';
import '@frontend/styles/widgetDashboard.css';
import '@frontend/styles/floatingWidget.css';
function parseInstanceFromUrl(): FloatingWidgetInstance | null {
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) return null;
const rows = Number(params.get('rows') ?? 1);
const cols = Number(params.get('cols') ?? 1);
const width = Number(params.get('width') ?? 640);
const height = Number(params.get('height') ?? 480);
let slots: WidgetSlot[] = [];
try {
slots = JSON.parse(params.get('slots') ?? '[]') as WidgetSlot[];
} catch {
slots = [];
}
const rowFrRaw = params.get('rowFr');
const colFrRaw = params.get('colFr');
const rowFr = rowFrRaw ? rowFrRaw.split(',').map(Number) : defaultGridFr(rows);
const colFr = colFrRaw ? colFrRaw.split(',').map(Number) : defaultGridFr(cols);
return {
id,
rows,
cols,
slots,
rowFr,
colFr,
position: { x: 0, y: 0 },
width,
height,
zIndex: 1,
};
}
const WidgetApp: React.FC = () => {
const initial = useMemo(() => parseInstanceFromUrl(), []);
const [instance, setInstance] = useState<FloatingWidgetInstance | null>(initial);
const sync = readDesktopSync();
const [theme, setTheme] = useState<Theme>((sync?.theme as Theme | undefined) ?? 'dark');
const [defaultMarket, setDefaultMarket] = useState(sync?.selectedMarket ?? 'KRW-BTC');
const { tickers, marketInfos, loading: marketLoading, usdRate } = useMarketTicker(
useMemo(() => ({ enabled: true, loadFull: true }), []),
);
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
const [trades, setTrades] = useState<PaperTradeDto[]>([]);
const refreshPaperData = useCallback(async () => {
try {
const [sum, tr] = await Promise.all([loadPaperSummary(), loadPaperTrades()]);
setSummary(sum);
setTrades(tr ?? []);
} catch { /* ignore */ }
}, []);
useEffect(() => {
void loadStrategies().then(list => setStrategies(list ?? []));
void refreshPaperData();
}, [refreshPaperData]);
useEffect(() => {
const onChanged = () => { void refreshPaperData(); };
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onChanged);
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onChanged);
}, [refreshPaperData]);
useEffect(() => {
return subscribeDesktopSync(state => {
if (state.theme) setTheme(state.theme as Theme);
if (state.selectedMarket) setDefaultMarket(state.selectedMarket);
});
}, []);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const handleClose = useCallback(() => {
void getCurrentWindow().close();
}, []);
if (!instance) {
return <p className="wd-widget-empty"> .</p>;
}
return (
<WidgetDashboardProvider
theme={theme}
tickers={tickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
usdRate={usdRate}
defaultMarket={defaultMarket}
strategies={strategies}
summary={summary}
trades={trades}
refreshPaperData={() => { void refreshPaperData(); }}
paperTradingEnabled
paperAutoTradeEnabled={false}
chartRealtimeSource="BACKEND_STOMP"
>
<div className="fw-widget-native-root">
<FloatingWidgetWindow
instance={instance}
focused
onClose={handleClose}
onFocus={() => {}}
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
onUpdateSize={(width, height) => setInstance(prev => (prev ? { ...prev, width, height } : prev))}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/>
</div>
</WidgetDashboardProvider>
);
};
async function bootstrap() {
await initStorage();
ensureDesktopApiBase();
refreshApiBaseFromStorage();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WidgetApp />
</React.StrictMode>,
);
}
void bootstrap();