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
@@ -0,0 +1,62 @@
import React, { useCallback, useEffect, useState } from 'react';
import { isDesktop } from '../utils/platform';
import {
checkDesktopUpdates,
getDesktopAppVersion,
} from '../utils/desktopBridge';
/** PC(Tauri) 앱 — 버전 표시 및 업데이트 확인 */
const DesktopUpdatePanel: React.FC = () => {
const [version, setVersion] = useState<string>('—');
const [status, setStatus] = useState<string>('');
const [busy, setBusy] = useState(false);
useEffect(() => {
if (!isDesktop()) return;
void getDesktopAppVersion().then(v => {
if (v) setVersion(v);
});
}, []);
const handleCheck = useCallback(async () => {
setBusy(true);
setStatus('업데이트 확인 중…');
try {
const res = await checkDesktopUpdates();
setStatus(res.message ?? (res.available ? `새 버전 ${res.version}` : '최신 버전입니다.'));
} finally {
setBusy(false);
}
}, []);
if (!isDesktop()) return null;
return (
<div className="stg-section">
<h3 className="stg-section-title">PC (GoldenChart Desktop)</h3>
<div className="stg-row">
<div className="stg-row-label">
<strong> </strong>
<p className="stg-row-desc"> .</p>
</div>
<div className="stg-row-control">
<span>{version}</span>
</div>
</div>
<div className="stg-row">
<div className="stg-row-label">
<strong></strong>
<p className="stg-row-desc"> .</p>
</div>
<div className="stg-row-control">
<button type="button" className="stg-btn-secondary" disabled={busy} onClick={() => { void handleCheck(); }}>
{busy ? '확인 중…' : '업데이트 확인'}
</button>
{status && <span className="stg-hint" style={{ marginLeft: 8 }}>{status}</span>}
</div>
</div>
</div>
);
};
export default DesktopUpdatePanel;
+3
View File
@@ -17,6 +17,7 @@ import type { IndicatorConfig } from '../types';
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
import ChartTimeFormatPicker from './ChartTimeFormatPicker';
import DesktopUpdatePanel from './DesktopUpdatePanel';
import {
TRADE_ALERT_SOUND_OPTIONS,
normalizeTradeAlertSoundId,
@@ -911,6 +912,8 @@ const GeneralPanel: React.FC<{
</label>
</SettingRow>
</SettingSection>
<DesktopUpdatePanel />
</>
);
};
@@ -1,6 +1,3 @@
/**
* 플로팅 위젯 레이어 — 메뉴바에서 다중 팝업 위젯 실행
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import type { Theme } from '../../types';
@@ -24,6 +21,9 @@ import {
import type { WidgetSlot } from '../../types/widgetDashboard';
import FloatingWidgetLayoutPicker from './FloatingWidgetLayoutPicker';
import FloatingWidgetWindow from './FloatingWidgetWindow';
import { isDesktop } from '../../utils/platform';
import { getDesktopBridge } from '../../utils/desktopBridge';
import { writeDesktopSync } from '../../utils/desktopSync';
import '../../styles/paperDashboard.css';
import '../../styles/widgetDashboard.css';
import '../../styles/floatingWidget.css';
@@ -40,6 +40,7 @@ interface Props {
onPaperOrderFilled?: () => void;
chartRealtimeSource?: ChartRealtimeSource;
onInstancesChange?: (count: number) => void;
selectedStrategyId?: number | null;
}
const FloatingWidgetLayer: React.FC<Props> = ({
@@ -52,7 +53,9 @@ const FloatingWidgetLayer: React.FC<Props> = ({
onPaperOrderFilled,
chartRealtimeSource = 'BACKEND_STOMP',
onInstancesChange,
selectedStrategyId = null,
}) => {
const desktopMode = isDesktop();
const [instances, setInstances] = useState<FloatingWidgetInstance[]>([]);
const [focusedId, setFocusedId] = useState<string | null>(null);
const zCounterRef = useRef(BASE_Z + 100);
@@ -83,14 +86,28 @@ const FloatingWidgetLayer: React.FC<Props> = ({
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onChanged);
}, [refreshPaperData]);
useEffect(() => {
if (!desktopMode) return;
writeDesktopSync({
selectedMarket: defaultMarket,
selectedStrategyId,
theme,
});
}, [desktopMode, defaultMarket, selectedStrategyId, theme]);
const bringToFront = useCallback((id: string) => {
if (desktopMode) {
void getDesktopBridge()?.focusWidgetWindow(id);
setFocusedId(id);
return;
}
zCounterRef.current += 1;
const nextZ = zCounterRef.current;
setFocusedId(id);
setInstances(prev => prev.map(inst =>
inst.id === id ? { ...inst, zIndex: nextZ } : inst,
));
}, []);
}, [desktopMode]);
const handleLayoutSelect = useCallback((preset: FloatingWidgetLayoutPreset) => {
zCounterRef.current += 1;
@@ -102,14 +119,25 @@ const FloatingWidgetLayer: React.FC<Props> = ({
zCounterRef.current,
);
inst.zIndex = zCounterRef.current;
if (desktopMode) {
void getDesktopBridge()?.openWidgetWindow(inst);
setInstances(prev => [...prev, inst]);
setFocusedId(inst.id);
return;
}
setInstances(prev => [...prev, inst]);
setFocusedId(inst.id);
}, [instances.length]);
}, [instances.length, desktopMode]);
const handleClose = useCallback((id: string) => {
if (desktopMode) {
void getDesktopBridge()?.closeWidgetWindow(id);
}
setInstances(prev => prev.filter(i => i.id !== id));
setFocusedId(prev => (prev === id ? null : prev));
}, []);
}, [desktopMode]);
const handleUpdateSlots = useCallback((id: string, slots: WidgetSlot[]) => {
setInstances(prev => prev.map(inst => (inst.id === id ? { ...inst, slots } : inst)));
@@ -122,9 +150,9 @@ const FloatingWidgetLayer: React.FC<Props> = ({
}, []);
const handleUpdateGridFr = useCallback((id: string, rowFr: number[], colFr: number[]) => {
setInstances(prev => prev.map(inst => (
inst.id === id ? { ...inst, rowFr, colFr } : inst
)));
setInstances(prev => prev.map(inst =>
inst.id === id ? { ...inst, rowFr, colFr } : inst,
));
}, []);
useEffect(() => {
@@ -133,6 +161,42 @@ const FloatingWidgetLayer: React.FC<Props> = ({
if (instances.length === 0 && !layoutPickerOpen) return null;
const picker = (
<FloatingWidgetLayoutPicker
open={layoutPickerOpen}
onClose={() => onLayoutPickerOpenChange(false)}
onSelect={handleLayoutSelect}
/>
);
if (desktopMode) {
return (
<WidgetDashboardProvider
theme={theme}
tickers={tickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
usdRate={usdRate}
defaultMarket={defaultMarket}
strategies={strategies}
summary={summary}
trades={trades}
refreshPaperData={() => { void refreshPaperData(); }}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
chartRealtimeSource={chartRealtimeSource}
>
{layoutPickerOpen && createPortal(
<div className="fw-layer fw-layer--picker-only" aria-label="위젯 레이아웃 선택">
{picker}
</div>,
document.body,
)}
</WidgetDashboardProvider>
);
}
return (
<WidgetDashboardProvider
theme={theme}
@@ -165,11 +229,7 @@ const FloatingWidgetLayer: React.FC<Props> = ({
/>
))}
<FloatingWidgetLayoutPicker
open={layoutPickerOpen}
onClose={() => onLayoutPickerOpenChange(false)}
onSelect={handleLayoutSelect}
/>
{picker}
</div>,
document.body,
)}