189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
import './forceServerApi';
|
||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import ReactDOM from 'react-dom/client';
|
||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||
import { LogicalSize } from '@tauri-apps/api/dpi';
|
||
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 { syncDocumentTheme } from '@frontend/utils/documentTheme';
|
||
import '@frontend/App.css';
|
||
import '@frontend/styles/appPopup.css';
|
||
import '@frontend/styles/paperDashboard.css';
|
||
import '@frontend/styles/widgetDashboard.css';
|
||
import '@frontend/styles/floatingWidget.css';
|
||
|
||
document.documentElement.classList.add('fw-native-widget');
|
||
|
||
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(() => {
|
||
syncDocumentTheme(theme);
|
||
}, [theme]);
|
||
|
||
useEffect(() => {
|
||
if (!instance) return;
|
||
const title = `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`;
|
||
void getCurrentWindow().setTitle(title);
|
||
}, [instance?.rows, instance?.cols, instance]);
|
||
|
||
useEffect(() => {
|
||
if (!instance) return;
|
||
let unlisten: (() => void) | undefined;
|
||
void (async () => {
|
||
unlisten = await getCurrentWindow().onResized(async () => {
|
||
const size = await getCurrentWindow().innerSize();
|
||
setInstance(prev => (
|
||
prev ? { ...prev, width: size.width, height: size.height } : prev
|
||
));
|
||
});
|
||
})();
|
||
return () => { unlisten?.(); };
|
||
}, [instance?.id]);
|
||
|
||
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
|
||
nativeShell
|
||
onClose={handleClose}
|
||
onFocus={() => {}}
|
||
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
|
||
onUpdateSize={(width, height) => {
|
||
setInstance(prev => (prev ? { ...prev, width, height } : prev));
|
||
void getCurrentWindow().setSize(new LogicalSize(width, height));
|
||
}}
|
||
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();
|