frontend 속도 개선
This commit is contained in:
Generated
+184
@@ -25,6 +25,7 @@
|
|||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/sockjs-client": "^1.5.4",
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
@@ -1672,6 +1673,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/define-lazy-prop": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dijkstrajs": {
|
"node_modules/dijkstrajs": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
@@ -1826,6 +1837,22 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-docker": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"is-docker": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -1835,6 +1862,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-wsl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -1962,6 +2002,24 @@
|
|||||||
"lightweight-charts": "^5.0.0"
|
"lightweight-charts": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/open": {
|
||||||
|
"version": "8.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||||
|
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-lazy-prop": "^2.0.0",
|
||||||
|
"is-docker": "^2.1.1",
|
||||||
|
"is-wsl": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
@@ -2014,6 +2072,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
@@ -2176,6 +2247,109 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer": {
|
||||||
|
"version": "5.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||||
|
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"open": "^8.4.0",
|
||||||
|
"picomatch": "^4.0.2",
|
||||||
|
"source-map": "^0.7.4",
|
||||||
|
"yargs": "^17.5.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rolldown": "1.x",
|
||||||
|
"rollup": "2.x || 3.x || 4.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2249,6 +2423,16 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||||
|
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/sockjs-client": "^1.5.4",
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-46
@@ -34,14 +34,33 @@ import { clearAdminUnlock } from './utils/adminUnlock';
|
|||||||
import type { LoginResponse } from './utils/backendApi';
|
import type { LoginResponse } from './utils/backendApi';
|
||||||
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||||
import { ChartWorkspaceRoot } from './chart/ChartWorkspaceRoot';
|
|
||||||
|
const LAST_MENU_KEY = 'gc_last_menu';
|
||||||
|
const CHART_ONLY_INITIAL: ReadonlySet<MenuPage> = new Set(['dashboard', 'settings', 'verification-board']);
|
||||||
|
|
||||||
|
function loadLastMenu(): MenuPage {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(LAST_MENU_KEY) as MenuPage | null;
|
||||||
|
// 가상매매·투자관리 등 로그인이 필요한 메뉴는 첫 로드 시 복원하지 않음
|
||||||
|
if (saved && saved !== 'chart') return saved;
|
||||||
|
} catch { /* localStorage 불가 */ }
|
||||||
|
return 'chart';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLastMenu(page: MenuPage): void {
|
||||||
|
try { localStorage.setItem(LAST_MENU_KEY, page); } catch { /* no-op */ }
|
||||||
|
}
|
||||||
import { useChartWorkspaceContext } from './chart/ChartWorkspaceContext';
|
import { useChartWorkspaceContext } from './chart/ChartWorkspaceContext';
|
||||||
|
|
||||||
|
const ChartWorkspaceRoot = lazy(() =>
|
||||||
|
import('./chart/ChartWorkspaceRoot').then(m => ({ default: m.ChartWorkspaceRoot }))
|
||||||
|
);
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import './styles/appPopup.css';
|
// virtualTradingDashboard.css는 VirtualTradingPage.tsx에서 import
|
||||||
import './styles/paperDashboard.css';
|
// paperDashboard.css는 PaperTradingPage.tsx에서 import
|
||||||
import './styles/tradeRightPanel.css';
|
// tradeRightPanel.css는 ChartWorkspaceView.tsx에서 import
|
||||||
import './styles/virtualTradingDashboard.css';
|
// backtestDashboard.css는 BacktestHistoryPage.tsx, AnalysisReportPage.tsx에서 import
|
||||||
import './styles/backtestDashboard.css';
|
// appPopup.css는 AppPopup.tsx에서 import
|
||||||
|
|
||||||
const DashboardPage = lazy(() => import('./components/DashboardPage'));
|
const DashboardPage = lazy(() => import('./components/DashboardPage'));
|
||||||
const StrategyPage = lazy(() => import('./components/StrategyPage'));
|
const StrategyPage = lazy(() => import('./components/StrategyPage'));
|
||||||
@@ -91,6 +110,8 @@ function AppMainContent({
|
|||||||
indicatorSettingsRevision,
|
indicatorSettingsRevision,
|
||||||
handleLoginSuccess,
|
handleLoginSuccess,
|
||||||
handleLogout,
|
handleLogout,
|
||||||
|
goToMarketChart,
|
||||||
|
chartSlot,
|
||||||
}: {
|
}: {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
onThemeToggle: () => void;
|
onThemeToggle: () => void;
|
||||||
@@ -123,19 +144,21 @@ function AppMainContent({
|
|||||||
indicatorSettingsRevision: number;
|
indicatorSettingsRevision: number;
|
||||||
handleLoginSuccess: (res: LoginResponse) => void;
|
handleLoginSuccess: (res: LoginResponse) => void;
|
||||||
handleLogout: () => void;
|
handleLogout: () => void;
|
||||||
|
/** 차트로 이동하는 App 레벨 콜백 */
|
||||||
|
goToMarketChart: (market: string) => void;
|
||||||
|
/** 차트 화면 슬롯 (lazy ChartWorkspaceRoot) */
|
||||||
|
chartSlot: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const {
|
const {
|
||||||
symbol,
|
symbol,
|
||||||
indicators,
|
indicators,
|
||||||
goToMarketChart,
|
|
||||||
refreshPaperAccount,
|
refreshPaperAccount,
|
||||||
handlePaperOrderFilled,
|
handlePaperOrderFilled,
|
||||||
paperRefreshKey,
|
paperRefreshKey,
|
||||||
mode,
|
mode,
|
||||||
marketTickers,
|
marketTickers,
|
||||||
settingsPageProps,
|
settingsPageProps,
|
||||||
chartScreen,
|
|
||||||
} = useChartWorkspaceContext();
|
} = useChartWorkspaceContext();
|
||||||
|
|
||||||
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
|
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
|
||||||
@@ -405,7 +428,7 @@ function AppMainContent({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chartScreen}
|
{chartSlot}
|
||||||
|
|
||||||
<AppNotificationLayer
|
<AppNotificationLayer
|
||||||
menuPage={menuPage}
|
menuPage={menuPage}
|
||||||
@@ -451,6 +474,48 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
const [appDownloadOpen, setAppDownloadOpen] = useState(false);
|
const [appDownloadOpen, setAppDownloadOpen] = useState(false);
|
||||||
|
|
||||||
|
const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
|
||||||
|
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
|
||||||
|
const [menuPage, setMenuPage] = useState<MenuPage>(loadLastMenu);
|
||||||
|
const [settingsInitialCategory, setSettingsInitialCategory] = useState<SettingsCategoryId | undefined>();
|
||||||
|
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// 차트 lazy mount 관련 상태: 처음 chart 메뉴 진입 시 마운트, 이후 유지
|
||||||
|
const [chartEverActivated, setChartEverActivated] = useState(() => loadLastMenu() === 'chart');
|
||||||
|
const [pendingChartMarket, setPendingChartMarket] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// menuPage 변경 시 localStorage 저장 + chart 활성화 추적
|
||||||
|
const setMenuPagePersist = useCallback((page: MenuPage) => {
|
||||||
|
saveLastMenu(page);
|
||||||
|
setMenuPage(page);
|
||||||
|
if (page === 'chart') setChartEverActivated(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 외부(대시보드·알림 등)에서 특정 마켓으로 차트 이동 요청
|
||||||
|
const handleGoToMarketChart = useCallback((market: string) => {
|
||||||
|
setPendingChartMarket(market);
|
||||||
|
setMenuPagePersist('chart');
|
||||||
|
setChartEverActivated(true);
|
||||||
|
}, [setMenuPagePersist]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuPage === 'chart') setChartEverActivated(true);
|
||||||
|
}, [menuPage]);
|
||||||
|
|
||||||
|
// 지표 설정: 차트·전략·알림 관련 페이지 첫 진입 시 로드 (이후 캐시 유지)
|
||||||
|
const INDICATOR_SETTINGS_PAGES: MenuPage[] = ['chart', 'strategy-editor', 'notifications', 'strategy', 'backtest', 'analysis-report', 'settings'];
|
||||||
|
const [indicatorSettingsEnabled, setIndicatorSettingsEnabled] = useState(
|
||||||
|
() => INDICATOR_SETTINGS_PAGES.includes(menuPage),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!indicatorSettingsEnabled && INDICATOR_SETTINGS_PAGES.includes(menuPage)) {
|
||||||
|
setIndicatorSettingsEnabled(true);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [menuPage, indicatorSettingsEnabled]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getParams: getIndicatorParams,
|
getParams: getIndicatorParams,
|
||||||
saveParams: saveIndicatorParams,
|
saveParams: saveIndicatorParams,
|
||||||
@@ -458,20 +523,12 @@ function App() {
|
|||||||
saveVisual: saveIndicatorVisual,
|
saveVisual: saveIndicatorVisual,
|
||||||
saveAllIndicatorDefaults,
|
saveAllIndicatorDefaults,
|
||||||
settingsRevision: indicatorSettingsRevision,
|
settingsRevision: indicatorSettingsRevision,
|
||||||
} = useIndicatorSettings(sessionKey);
|
} = useIndicatorSettings(sessionKey, indicatorSettingsEnabled);
|
||||||
const getParams = getIndicatorParams;
|
const getParams = getIndicatorParams;
|
||||||
const saveParams = saveIndicatorParams;
|
const saveParams = saveIndicatorParams;
|
||||||
const getVisualConfig = getIndicatorVisual;
|
const getVisualConfig = getIndicatorVisual;
|
||||||
const saveVisual = saveIndicatorVisual;
|
const saveVisual = saveIndicatorVisual;
|
||||||
|
|
||||||
const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
|
|
||||||
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
|
||||||
|
|
||||||
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
|
|
||||||
const [menuPage, setMenuPage] = useState<MenuPage>('chart');
|
|
||||||
const [settingsInitialCategory, setSettingsInitialCategory] = useState<SettingsCategoryId | undefined>();
|
|
||||||
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuPage !== 'settings') setSettingsInitialCategory(undefined);
|
if (menuPage !== 'settings') setSettingsInitialCategory(undefined);
|
||||||
}, [menuPage]);
|
}, [menuPage]);
|
||||||
@@ -532,22 +589,22 @@ function App() {
|
|||||||
}
|
}
|
||||||
if (page === 'notifications' && !canMenu('notifications')) return;
|
if (page === 'notifications' && !canMenu('notifications')) return;
|
||||||
if (page !== 'notifications' && !canMenu(page)) {
|
if (page !== 'notifications' && !canMenu(page)) {
|
||||||
setMenuPage(firstAllowedTopMenu(menuPermissions));
|
setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMenuPage(page);
|
setMenuPagePersist(page);
|
||||||
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin]);
|
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin, setMenuPagePersist]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!menuPermsLoaded) return;
|
if (!menuPermsLoaded) return;
|
||||||
if (menuPage === 'notifications') {
|
if (menuPage === 'notifications') {
|
||||||
if (!canMenu('notifications')) setMenuPage(firstAllowedTopMenu(menuPermissions));
|
if (!canMenu('notifications')) setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!canMenu(menuPage)) {
|
if (!canMenu(menuPage)) {
|
||||||
setMenuPage(firstAllowedTopMenu(menuPermissions));
|
setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||||
}
|
}
|
||||||
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu]);
|
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu, setMenuPagePersist]);
|
||||||
|
|
||||||
const handleThemeToggle = useCallback(() => {
|
const handleThemeToggle = useCallback(() => {
|
||||||
setTheme(t => {
|
setTheme(t => {
|
||||||
@@ -580,32 +637,40 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 차트 슬롯: 처음 chart 페이지 진입 시 마운트, 이후 visible 토글
|
||||||
|
const chartSlot = chartEverActivated ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ChartWorkspaceRoot
|
||||||
|
visible={menuPage === 'chart'}
|
||||||
|
pendingChartMarket={pendingChartMarket}
|
||||||
|
onPendingChartMarketConsumed={() => setPendingChartMarket(null)}
|
||||||
|
menuPage={menuPage}
|
||||||
|
sessionKey={sessionKey}
|
||||||
|
formalLogin={formalLogin}
|
||||||
|
requireFormalLogin={requireFormalLogin}
|
||||||
|
theme={theme}
|
||||||
|
onThemeToggle={handleThemeToggle}
|
||||||
|
appDefaults={appDefaults as unknown as import('./utils/backendApi').AppSettingsDto}
|
||||||
|
appSettingsLoaded={appSettingsLoaded}
|
||||||
|
saveAppDef={saveAppDef}
|
||||||
|
getParams={getParams}
|
||||||
|
saveParams={saveParams}
|
||||||
|
getVisualConfig={getVisualConfig}
|
||||||
|
saveVisual={saveVisual}
|
||||||
|
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
|
||||||
|
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||||
|
onOpenNotificationsPage={openNotificationsPage}
|
||||||
|
onOpenAppDownload={() => setAppDownloadOpen(true)}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TradeNotificationProvider
|
<TradeNotificationProvider
|
||||||
popupEnabled={appDefaults.tradeAlertPopup ?? true}
|
popupEnabled={appDefaults.tradeAlertPopup ?? true}
|
||||||
soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
|
soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
|
||||||
soundId={appDefaults.tradeAlertSound ?? 'bell'}
|
soundId={appDefaults.tradeAlertSound ?? 'bell'}
|
||||||
settingsSessionKey={authUser?.userId ?? 0}
|
settingsSessionKey={authUser?.userId ?? 0}
|
||||||
>
|
|
||||||
<ChartWorkspaceRoot
|
|
||||||
visible={menuPage === 'chart'}
|
|
||||||
menuPage={menuPage}
|
|
||||||
sessionKey={sessionKey}
|
|
||||||
formalLogin={formalLogin}
|
|
||||||
requireFormalLogin={requireFormalLogin}
|
|
||||||
theme={theme}
|
|
||||||
onThemeToggle={handleThemeToggle}
|
|
||||||
appDefaults={appDefaults as unknown as import('./utils/backendApi').AppSettingsDto}
|
|
||||||
appSettingsLoaded={appSettingsLoaded}
|
|
||||||
saveAppDef={saveAppDef}
|
|
||||||
getParams={getParams}
|
|
||||||
saveParams={saveParams}
|
|
||||||
getVisualConfig={getVisualConfig}
|
|
||||||
saveVisual={saveVisual}
|
|
||||||
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
|
|
||||||
indicatorSettingsRevision={indicatorSettingsRevision}
|
|
||||||
onOpenNotificationsPage={openNotificationsPage}
|
|
||||||
onOpenAppDownload={() => setAppDownloadOpen(true)}
|
|
||||||
>
|
>
|
||||||
<AppMainContent
|
<AppMainContent
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -620,7 +685,7 @@ function App() {
|
|||||||
appDownloadOpen={appDownloadOpen}
|
appDownloadOpen={appDownloadOpen}
|
||||||
setAppDownloadOpen={setAppDownloadOpen}
|
setAppDownloadOpen={setAppDownloadOpen}
|
||||||
menuPage={menuPage}
|
menuPage={menuPage}
|
||||||
setMenuPage={setMenuPage}
|
setMenuPage={setMenuPagePersist}
|
||||||
guardedSetMenuPage={guardedSetMenuPage}
|
guardedSetMenuPage={guardedSetMenuPage}
|
||||||
settingsInitialCategory={settingsInitialCategory}
|
settingsInitialCategory={settingsInitialCategory}
|
||||||
setSettingsInitialCategory={setSettingsInitialCategory}
|
setSettingsInitialCategory={setSettingsInitialCategory}
|
||||||
@@ -639,8 +704,9 @@ function App() {
|
|||||||
indicatorSettingsRevision={indicatorSettingsRevision}
|
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||||
handleLoginSuccess={handleLoginSuccess}
|
handleLoginSuccess={handleLoginSuccess}
|
||||||
handleLogout={handleLogout}
|
handleLogout={handleLogout}
|
||||||
|
goToMarketChart={handleGoToMarketChart}
|
||||||
|
chartSlot={chartSlot}
|
||||||
/>
|
/>
|
||||||
</ChartWorkspaceRoot>
|
|
||||||
</TradeNotificationProvider>
|
</TradeNotificationProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import React, { createContext, useContext } from 'react';
|
|||||||
import type { ChartMode, IndicatorConfig } from '../types';
|
import type { ChartMode, IndicatorConfig } from '../types';
|
||||||
import type { TickerData } from '../hooks/useMarketTicker';
|
import type { TickerData } from '../hooks/useMarketTicker';
|
||||||
import type { SettingsPageChartBindings } from './useChartWorkspace';
|
import type { SettingsPageChartBindings } from './useChartWorkspace';
|
||||||
|
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||||
|
import { DEFAULT_CHART_TIME_FORMAT } from '../utils/chartTimeFormat';
|
||||||
|
import { DEFAULT_TRADE_ALERT_TIME_FORMAT } from '../utils/tradeAlertTimeFormat';
|
||||||
|
|
||||||
export interface ChartWorkspaceSharedState {
|
export interface ChartWorkspaceSharedState {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -21,12 +24,66 @@ export interface ChartWorkspaceSharedState {
|
|||||||
settingsPageProps: SettingsPageChartBindings;
|
settingsPageProps: SettingsPageChartBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChartWorkspaceContextValue extends ChartWorkspaceSharedState {
|
export type ChartWorkspaceContextValue = ChartWorkspaceSharedState;
|
||||||
/** 실시간 차트 화면 — `.app` flex 레이아웃 안에서 렌더해야 좌·우 패널·메뉴가 정상 표시됨 */
|
|
||||||
chartScreen: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartWorkspaceContext = createContext<ChartWorkspaceContextValue | null>(null);
|
const NULL_SETTINGS_PAGE_PROPS: SettingsPageChartBindings = {
|
||||||
|
magnetMode: 'off',
|
||||||
|
onMagnetMode: () => {},
|
||||||
|
liveStrategies: [],
|
||||||
|
onClearLiveMarkers: () => {},
|
||||||
|
liveSettings: { market: '', strategyId: null, isLiveCheck: false, executionType: 'CANDLE_CLOSE', positionMode: 'LONG_ONLY' },
|
||||||
|
onLiveSettingsChange: () => {},
|
||||||
|
watchlistCount: 0,
|
||||||
|
monitoredMarkets: [],
|
||||||
|
virtualDriven: false,
|
||||||
|
chartIndicators: [],
|
||||||
|
onAddIndicatorToChart: () => {},
|
||||||
|
onRemoveIndicatorFromChart: () => {},
|
||||||
|
onIndicatorListOrderChange: () => {},
|
||||||
|
chartCandleAreaPriceLabels: false,
|
||||||
|
onChartCandleAreaPriceLabels: () => {},
|
||||||
|
chartSeriesPriceLabels: false,
|
||||||
|
onChartSeriesPriceLabels: () => {},
|
||||||
|
chartVolumeVisible: true,
|
||||||
|
onChartVolumeVisible: () => {},
|
||||||
|
chartLiveReceiveHighlight: true,
|
||||||
|
onChartLiveReceiveHighlight: () => {},
|
||||||
|
chartLegendOptions: undefined,
|
||||||
|
onChartLegendOptionsChange: () => {},
|
||||||
|
chartCrosshairInfoVisible: true,
|
||||||
|
onChartCrosshairInfoVisible: () => {},
|
||||||
|
chartHoverToolbarVisible: true,
|
||||||
|
onChartHoverToolbarVisible: () => {},
|
||||||
|
chartPaneSeparator: undefined,
|
||||||
|
onChartPaneSeparatorChange: () => {},
|
||||||
|
onPaperAccountReset: async () => {},
|
||||||
|
onDisplayTimezoneChange: () => {},
|
||||||
|
onChartTimeFormatChange: () => {},
|
||||||
|
onTradeAlertTimeFormatChange: () => {},
|
||||||
|
displayTimezone: DEFAULT_DISPLAY_TIMEZONE,
|
||||||
|
chartTimeFormat: DEFAULT_CHART_TIME_FORMAT,
|
||||||
|
tradeAlertTimeFormat: DEFAULT_TRADE_ALERT_TIME_FORMAT,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NULL_CHART_WORKSPACE_CONTEXT: ChartWorkspaceContextValue = {
|
||||||
|
symbol: 'KRW-BTC',
|
||||||
|
indicators: [],
|
||||||
|
goToMarketChart: () => {},
|
||||||
|
refreshPaperAccount: async () => {},
|
||||||
|
paperCashBalance: 0,
|
||||||
|
paperCoinQty: 0,
|
||||||
|
handlePaperOrderFilled: () => {},
|
||||||
|
paperRefreshKey: 0,
|
||||||
|
mode: 'chart',
|
||||||
|
priorityMarkets: [],
|
||||||
|
marketPanelOpen: false,
|
||||||
|
marketTickers: new Map(),
|
||||||
|
marketLoading: false,
|
||||||
|
usdRate: 0,
|
||||||
|
settingsPageProps: NULL_SETTINGS_PAGE_PROPS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartWorkspaceContext = createContext<ChartWorkspaceContextValue>(NULL_CHART_WORKSPACE_CONTEXT);
|
||||||
|
|
||||||
export function ChartWorkspaceProvider({
|
export function ChartWorkspaceProvider({
|
||||||
value,
|
value,
|
||||||
@@ -43,9 +100,5 @@ export function ChartWorkspaceProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useChartWorkspaceContext(): ChartWorkspaceContextValue {
|
export function useChartWorkspaceContext(): ChartWorkspaceContextValue {
|
||||||
const ctx = useContext(ChartWorkspaceContext);
|
return useContext(ChartWorkspaceContext);
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('useChartWorkspaceContext must be used within ChartWorkspaceProvider');
|
|
||||||
}
|
|
||||||
return ctx;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
import React, { forwardRef, useImperativeHandle, useMemo, useEffect, useRef } from 'react';
|
||||||
import { LiveSignalNotifier } from '../components/LiveSignalNotifier';
|
import { LiveSignalNotifier } from '../components/LiveSignalNotifier';
|
||||||
import { ChartWorkspaceProvider } from './ChartWorkspaceContext';
|
import { ChartWorkspaceProvider } from './ChartWorkspaceContext';
|
||||||
import { ChartWorkspaceView } from './ChartWorkspaceView';
|
import { ChartWorkspaceView } from './ChartWorkspaceView';
|
||||||
@@ -7,12 +7,14 @@ import type { ChartWorkspaceBridge } from './chartWorkspaceBridge';
|
|||||||
|
|
||||||
export interface ChartWorkspaceRootProps extends UseChartWorkspaceParams {
|
export interface ChartWorkspaceRootProps extends UseChartWorkspaceParams {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
children: React.ReactNode;
|
/** 외부에서 이동 요청된 마켓 (알림·대시보드 등에서 차트로 이동 시) */
|
||||||
|
pendingChartMarket?: string | null;
|
||||||
|
onPendingChartMarketConsumed?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartWorkspaceRoot = forwardRef<ChartWorkspaceBridge, ChartWorkspaceRootProps>(
|
export const ChartWorkspaceRoot = forwardRef<ChartWorkspaceBridge, ChartWorkspaceRootProps>(
|
||||||
function ChartWorkspaceRoot(props, ref) {
|
function ChartWorkspaceRoot(props, ref) {
|
||||||
const { visible, children, ...workspaceParams } = props;
|
const { visible, pendingChartMarket, onPendingChartMarketConsumed, ...workspaceParams } = props;
|
||||||
const {
|
const {
|
||||||
bridge,
|
bridge,
|
||||||
contextValue,
|
contextValue,
|
||||||
@@ -23,19 +25,31 @@ export const ChartWorkspaceRoot = forwardRef<ChartWorkspaceBridge, ChartWorkspac
|
|||||||
|
|
||||||
useImperativeHandle(ref, () => bridge, [bridge]);
|
useImperativeHandle(ref, () => bridge, [bridge]);
|
||||||
|
|
||||||
|
// 외부에서 마켓 이동 요청 처리
|
||||||
|
const pendingRef = useRef(pendingChartMarket);
|
||||||
|
pendingRef.current = pendingChartMarket;
|
||||||
|
const consumedRef = useRef(onPendingChartMarketConsumed);
|
||||||
|
consumedRef.current = onPendingChartMarketConsumed;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingChartMarket) {
|
||||||
|
bridge.goToMarket(pendingChartMarket);
|
||||||
|
consumedRef.current?.();
|
||||||
|
}
|
||||||
|
// bridge.goToMarket is stable (useCallback), pendingChartMarket is the trigger
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pendingChartMarket]);
|
||||||
|
|
||||||
const providerValue = useMemo(() => ({
|
const providerValue = useMemo(() => ({
|
||||||
...contextValue,
|
...contextValue,
|
||||||
chartScreen: (
|
}), [contextValue]);
|
||||||
<div style={{ display: visible ? 'contents' : 'none' }}>
|
|
||||||
<ChartWorkspaceView {...viewProps} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}), [contextValue, visible, viewProps]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartWorkspaceProvider value={providerValue}>
|
<ChartWorkspaceProvider value={providerValue}>
|
||||||
<LiveSignalNotifier ref={liveNotifierRef} {...liveNotifierProps} />
|
<LiveSignalNotifier ref={liveNotifierRef} {...liveNotifierProps} />
|
||||||
{children}
|
<div style={{ display: visible ? 'contents' : 'none' }}>
|
||||||
|
<ChartWorkspaceView {...viewProps} />
|
||||||
|
</div>
|
||||||
</ChartWorkspaceProvider>
|
</ChartWorkspaceProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import '../styles/tradeRightPanel.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DrawingToolbar from '../components/DrawingToolbar';
|
import DrawingToolbar from '../components/DrawingToolbar';
|
||||||
import BottomBar from '../components/BottomBar';
|
import BottomBar from '../components/BottomBar';
|
||||||
|
|||||||
@@ -1175,6 +1175,8 @@ export function useChartWorkspace({
|
|||||||
|
|
||||||
// ── Watchlist 가격 맵 ──────────────────────────────────────────────────
|
// ── Watchlist 가격 맵 ──────────────────────────────────────────────────
|
||||||
const priceMap = useMemo(() => {
|
const priceMap = useMemo(() => {
|
||||||
|
// 시세 패널이 열려 있고 ticker가 활성 상태일 때만 계산 (데모 심볼용)
|
||||||
|
if (!showMarketPanel || !tickerFeedActive) return {} as Record<string, { price: number; changePct: number }>;
|
||||||
const map: Record<string, { price: number; changePct: number }> = {};
|
const map: Record<string, { price: number; changePct: number }> = {};
|
||||||
for (const sym of watchlist) {
|
for (const sym of watchlist) {
|
||||||
const b = isUpbitMarket(sym) ? [] : generateBars(sym, '1D');
|
const b = isUpbitMarket(sym) ? [] : generateBars(sym, '1D');
|
||||||
@@ -1183,7 +1185,7 @@ export function useChartWorkspace({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [watchlist]);
|
}, [watchlist, showMarketPanel, tickerFeedActive]);
|
||||||
|
|
||||||
|
|
||||||
// ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
|
// ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* AppPopup — TradeAlertModal(tam-) 기준 공통 팝업 셸
|
* AppPopup — TradeAlertModal(tam-) 기준 공통 팝업 셸
|
||||||
* 신규 팝업은 이 컴포넌트를 사용하세요.
|
* 신규 팝업은 이 컴포넌트를 사용하세요.
|
||||||
*/
|
*/
|
||||||
|
import '../styles/appPopup.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* BacktestHistoryPage — Golden Analysis Command Center (참조 UI 3열)
|
* BacktestHistoryPage — Golden Analysis Command Center (참조 UI 3열)
|
||||||
*/
|
*/
|
||||||
|
import '../styles/backtestDashboard.css';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
loadBacktestResults,
|
loadBacktestResults,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 모의투자 대시보드 — 가상매매와 동일 3열 레이아웃(좌·중앙·우 접기/리사이즈)
|
* 모의투자 대시보드 — 가상매매와 동일 3열 레이아웃(좌·중앙·우 접기/리사이즈)
|
||||||
*/
|
*/
|
||||||
|
import '../styles/paperDashboard.css';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
loadPaperSummary,
|
loadPaperSummary,
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
type="button"
|
type="button"
|
||||||
className={`tmb-nav-btn ${activePage === page ? 'tmb-nav-btn--active' : ''}`}
|
className={`tmb-nav-btn ${activePage === page ? 'tmb-nav-btn--active' : ''}`}
|
||||||
onClick={() => onPage(page)}
|
onClick={() => onPage(page)}
|
||||||
|
onMouseEnter={page === 'chart' ? () => { void import('../chart/ChartWorkspaceRoot'); } : undefined}
|
||||||
>
|
>
|
||||||
<span className="tmb-nav-icon">{icon}</span>
|
<span className="tmb-nav-icon">{icon}</span>
|
||||||
<span className="tmb-nav-label">{label}</span>
|
<span className="tmb-nav-label">{label}</span>
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ export function invalidateIndicatorSettingsCache() {
|
|||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function useIndicatorSettings(sessionKey = 0) {
|
/**
|
||||||
|
* @param sessionKey 로그인 세션 키 (변경 시 캐시 무효화 및 재로드)
|
||||||
|
* @param enabled false 이면 DB 로드를 건너뜀 — 차트·전략편집·알림 페이지 진입 시에만 true
|
||||||
|
*/
|
||||||
|
export function useIndicatorSettings(sessionKey = 0, enabled = true) {
|
||||||
const [settingsRevision, setSettingsRevision] = useState(0);
|
const [settingsRevision, setSettingsRevision] = useState(0);
|
||||||
const [settingsLoaded, setSettingsLoaded] = useState(
|
const [settingsLoaded, setSettingsLoaded] = useState(
|
||||||
() => _cache !== null && _visualCache !== null,
|
() => _cache !== null && _visualCache !== null,
|
||||||
@@ -130,6 +134,8 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = subscribeIndicatorSettings(() => setSettingsRevision(r => r + 1));
|
const unsub = subscribeIndicatorSettings(() => setSettingsRevision(r => r + 1));
|
||||||
|
|
||||||
|
if (!enabled) return unsub;
|
||||||
|
|
||||||
const cacheWarm = _loadedSessionKey === sessionKey
|
const cacheWarm = _loadedSessionKey === sessionKey
|
||||||
&& _cache !== null
|
&& _cache !== null
|
||||||
&& _visualCache !== null;
|
&& _visualCache !== null;
|
||||||
@@ -157,7 +163,7 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
setSettingsRevision(r => r + 1);
|
setSettingsRevision(r => r + 1);
|
||||||
});
|
});
|
||||||
return unsub;
|
return unsub;
|
||||||
}, [sessionKey]);
|
}, [sessionKey, enabled]);
|
||||||
|
|
||||||
// ── 파라미터 ────────────────────────────────────────────────────────────
|
// ── 파라미터 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
// 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'];
|
||||||
@@ -12,7 +13,10 @@ logger.error = (msg, opts) => {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
customLogger: logger,
|
customLogger: logger,
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
visualizer({ filename: 'dist/stats.html', open: false, gzipSize: true, brotliSize: true }),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
dedupe: ['lightweight-charts'],
|
dedupe: ['lightweight-charts'],
|
||||||
},
|
},
|
||||||
@@ -27,6 +31,13 @@ export default defineConfig({
|
|||||||
if (id.includes('node_modules/@xyflow')) return 'vendor-flow';
|
if (id.includes('node_modules/@xyflow')) return 'vendor-flow';
|
||||||
if (id.includes('node_modules/@stomp') || id.includes('node_modules/sockjs-client')) return 'vendor-stomp';
|
if (id.includes('node_modules/@stomp') || id.includes('node_modules/sockjs-client')) return 'vendor-stomp';
|
||||||
if (id.includes('node_modules/@tanstack/react-virtual')) return 'vendor-virtual';
|
if (id.includes('node_modules/@tanstack/react-virtual')) return 'vendor-virtual';
|
||||||
|
// 차트 워크스페이스 전용 청크 — lazy import로 분리된 코드
|
||||||
|
if (
|
||||||
|
id.includes('/chart/ChartWorkspaceRoot') ||
|
||||||
|
id.includes('/chart/useChartWorkspace') ||
|
||||||
|
id.includes('/chart/ChartWorkspaceView') ||
|
||||||
|
id.includes('/utils/ChartManager')
|
||||||
|
) return 'chart-workspace';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+109
@@ -62,6 +62,7 @@
|
|||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/sockjs-client": "^1.5.4",
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
@@ -2804,6 +2805,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/plist": {
|
"node_modules/plist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
|
||||||
@@ -3044,6 +3058,91 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer": {
|
||||||
|
"version": "5.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||||
|
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"open": "^8.4.0",
|
||||||
|
"picomatch": "^4.0.2",
|
||||||
|
"source-map": "^0.7.4",
|
||||||
|
"yargs": "^17.5.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rolldown": "1.x",
|
||||||
|
"rollup": "2.x || 3.x || 4.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -3182,6 +3281,16 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||||
|
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@
|
|||||||
"cap:android": "npm run cap:open:android -w @goldenchart/app",
|
"cap:android": "npm run cap:open:android -w @goldenchart/app",
|
||||||
"cap:ios": "npm run cap:open:ios -w @goldenchart/app"
|
"cap:ios": "npm run cap:open:ios -w @goldenchart/app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user