From 7c12f24232e99ed223b9e55b7773766d41aef9c8 Mon Sep 17 00:00:00 2001 From: kyj0503 Date: Sat, 1 Aug 2026 02:58:02 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore(fe):=20React=2018=20=E2=86=92=2019=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - react, react-dom ^18.2.0 → ^19.2.8 - @types/react ^19.2.18, @types/react-dom ^19.2.4 소스 변경은 PerformanceMonitor.tsx 두 곳뿐이다. React 19에서 useRef가 인자를 요구하도록 바뀌어 useRef() → useRef(undefined) 로 수정했다. main.tsx는 이미 createRoot를 쓰고 있어 손댈 것이 없었다. vite.config.ts의 manualChunks도 함께 수정했다. React 19는 진입점이 react-dom/client 이라 기존 설정('react-dom'만 명시)으로는 react-dom 본체가 vendor 청크에 잡히지 않고 index로 흘러들어갔다. 그 결과 index 청크가 232kB → 573kB(gzip 70 → 171kB)로 불어나 코드 스플리팅이 사실상 무너졌다. react-dom/client를 명시해 index 218kB / react-vendor 422kB로 복구했다. 검증: test:run 99 passed(기존 실패 4건 동일), lint 74 problems 동일, type-check 통과, build 성공, dev 서버 200. Co-Authored-By: Claude Opus 5 (1M context) --- backtest_fe/package-lock.json | 125 ++++++++++++------ backtest_fe/package.json | 8 +- .../components/debug/PerformanceMonitor.tsx | 4 +- backtest_fe/vite.config.ts | 4 +- 4 files changed, 97 insertions(+), 44 deletions(-) diff --git a/backtest_fe/package-lock.json b/backtest_fe/package-lock.json index 4c8fcef3..f8c931dc 100644 --- a/backtest_fe/package-lock.json +++ b/backtest_fe/package-lock.json @@ -26,8 +26,8 @@ "date-fns": "^4.1.0", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", "recharts": "^2.9.0", "sonner": "^2.0.7", @@ -38,8 +38,8 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react": "^4.0.0", @@ -1456,25 +1456,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -1482,7 +1488,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@hono/node-server": { @@ -1880,6 +1888,8 @@ }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -1901,6 +1911,8 @@ }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1998,6 +2010,8 @@ }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2063,6 +2077,8 @@ }, "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -2123,6 +2139,8 @@ }, "node_modules/@radix-ui/react-menu": { "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2161,6 +2179,8 @@ }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -2191,6 +2211,8 @@ }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", @@ -2213,6 +2235,8 @@ }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -2235,6 +2259,8 @@ }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -2256,6 +2282,8 @@ }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2498,6 +2526,8 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -2540,6 +2570,8 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -2556,6 +2588,8 @@ }, "node_modules/@radix-ui/react-use-size": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2572,6 +2606,8 @@ }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -2593,6 +2629,8 @@ }, "node_modules/@radix-ui/rect": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { @@ -3214,26 +3252,24 @@ "undici-types": "~7.12.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "devOptional": true, - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.24", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "devOptional": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/statuses": { @@ -4631,7 +4667,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-array": { @@ -4896,6 +4934,8 @@ }, "node_modules/dom-helpers": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -5507,7 +5547,9 @@ "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.2.2", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -6772,6 +6814,8 @@ }, "node_modules/loose-envify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -7881,6 +7925,8 @@ }, "node_modules/prop-types": { "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -7890,6 +7936,8 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/proxy-addr": { @@ -8007,24 +8055,24 @@ } }, "node_modules/react": { - "version": "18.3.1", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.8" } }, "node_modules/react-is": { @@ -8139,6 +8187,8 @@ }, "node_modules/react-smooth": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", @@ -8172,6 +8222,8 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -8459,11 +8511,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "7.8.5", diff --git a/backtest_fe/package.json b/backtest_fe/package.json index 0ccd1527..482f4330 100644 --- a/backtest_fe/package.json +++ b/backtest_fe/package.json @@ -47,8 +47,8 @@ "date-fns": "^4.1.0", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", "recharts": "^2.9.0", "sonner": "^2.0.7", @@ -59,8 +59,8 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react": "^4.0.0", diff --git a/backtest_fe/src/shared/components/debug/PerformanceMonitor.tsx b/backtest_fe/src/shared/components/debug/PerformanceMonitor.tsx index 67c0fe3c..c1715451 100644 --- a/backtest_fe/src/shared/components/debug/PerformanceMonitor.tsx +++ b/backtest_fe/src/shared/components/debug/PerformanceMonitor.tsx @@ -13,7 +13,7 @@ export const withPerformanceMonitor =

( componentName: string ) => { return React.memo((props: P) => { - const renderStartTime = useRef(); + const renderStartTime = useRef(undefined); const renderCount = useRef(0); useEffect(() => { @@ -101,7 +101,7 @@ export const useMemoryMonitor = (componentName: string) => { * 렌더링 성능 측정 훅 */ export const useRenderPerformance = (componentName: string) => { - const renderStartTime = useRef(); + const renderStartTime = useRef(undefined); const renderCount = useRef(0); useEffect(() => { diff --git a/backtest_fe/vite.config.ts b/backtest_fe/vite.config.ts index 99f757a4..1fb2748e 100644 --- a/backtest_fe/vite.config.ts +++ b/backtest_fe/vite.config.ts @@ -51,7 +51,9 @@ export default defineConfig(({ mode }) => ({ output: { manualChunks: { // React 관련 라이브러리 분리 - 'react-vendor': ['react', 'react-dom', 'react-router-dom'], + // React 19에서는 진입점이 react-dom/client 이므로 명시하지 않으면 + // react-dom 본체가 vendor 청크에 잡히지 않고 index로 흘러들어간다. + 'react-vendor': ['react', 'react-dom', 'react-dom/client', 'react-router-dom'], // 차트 라이브러리 분리 'chart-vendor': ['recharts'], // 아이콘 라이브러리 분리 From 698202ef754a5fdb62646ce3758ade45eb38cdd7 Mon Sep 17 00:00:00 2001 From: kyj0503 Date: Sat, 1 Aug 2026 03:11:36 +0900 Subject: [PATCH 2/8] =?UTF-8?q?chore(fe):=20recharts=202.15.4=20=E2=86=92?= =?UTF-8?q?=203.10.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 두 가지 breaking change에 대응했다. 1. CartesianGrid가 자체 yAxisId를 갖게 되어 YAxis와 일치시켜야 한다. 타입 오류가 나지 않고 조용히 깨지는 종류다. 커스텀 yAxisId를 쓰는 EquityChart(return)와 OHLCChart(price) 두 곳이 해당되며, 불일치 시 가로 그리드가 눈금에 맞춘 5개가 아니라 경계선 2개로 축소된다. 2. Tooltip의 formatter/labelFormatter 시그니처가 좁혀졌다(14건). 파라미터를 unknown으로 받고 Number()/String()으로 변환해 해결했다. 실제로 넘어오는 값이 숫자/문자열이라 런타임 출력은 동일하다. any나 @ts-ignore는 쓰지 않았다. 제거된 v3 prop(alwaysShow, isFront, animateNewValues, blendStroke 등)은 사용처가 없었다. CustomTooltip은 TooltipProps를 import하지 않고 자체 인터페이스를 쓰고 있어 TooltipContentProps 개명의 영향을 받지 않았다. 차트 렌더 회귀를 잡을 장치가 없어 ChartRendering.test.tsx를 추가했다. 실제 EquityChart/OHLCChart를 렌더해 SVG 구조(그리드 선 개수, 축 개수, 곡선/막대 존재)를 검증한다. 색상·여백·툴팁 포맷·범례는 다루지 않는다. 알려진 동작 변화: v3는 범례 순서가 JSX 선언 순서를 따르는 것을 보장하지 않는다. EquityChart와 OHLCChart의 범례 항목 순서가 v2와 달라진다. 데이터에는 영향이 없고, v2 순서를 강제하려면 v3가 제거한 Legend payload가 필요하다. 검증: test:run 104 passed(신규 5건 포함, 기존 실패 4건 동일), lint 74 problems 동일, type-check 통과, build 성공, chart-vendor 청크 유지(445kB, index 유출 0건), dev 서버 200. Co-Authored-By: Claude Opus 5 (1M context) --- backtest_fe/package-lock.json | 305 +++++++++++------- backtest_fe/package.json | 2 +- .../backtest/components/EquityChart.tsx | 3 +- .../backtest/components/ExchangeRateChart.tsx | 2 +- .../backtest/components/OHLCChart.tsx | 3 +- .../backtest/components/StockPriceChart.tsx | 11 +- .../__tests__/ChartRendering.test.tsx | 155 +++++++++ .../results/BenchmarkIndexChart.tsx | 7 +- .../results/BenchmarkReturnsChart.tsx | 7 +- .../results/ChartsSection/PortfolioCharts.tsx | 10 +- .../ChartsSection/SupplementaryCharts.tsx | 4 +- .../components/results/WeightHistoryChart.tsx | 7 +- 12 files changed, 377 insertions(+), 139 deletions(-) create mode 100644 backtest_fe/src/features/backtest/components/__tests__/ChartRendering.test.tsx diff --git a/backtest_fe/package-lock.json b/backtest_fe/package-lock.json index f8c931dc..6b504b27 100644 --- a/backtest_fe/package-lock.json +++ b/backtest_fe/package-lock.json @@ -29,7 +29,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", - "recharts": "^2.9.0", + "recharts": "^3.10.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, @@ -519,6 +519,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2633,6 +2634,32 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "dev": true, @@ -3008,7 +3035,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@testing-library/dom": { @@ -3174,18 +3206,26 @@ }, "node_modules/@types/d3-array": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -3193,17 +3233,23 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "3.1.7", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3211,10 +3257,14 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/deep-eql": { @@ -3277,6 +3327,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -4670,10 +4726,13 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -4684,6 +4743,8 @@ }, "node_modules/d3-color": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -4691,13 +4752,17 @@ }, "node_modules/d3-ease": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" } }, "node_modules/d3-format": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -4705,6 +4770,8 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -4715,6 +4782,8 @@ }, "node_modules/d3-path": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "engines": { "node": ">=12" @@ -4722,6 +4791,8 @@ }, "node_modules/d3-scale": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -4736,6 +4807,8 @@ }, "node_modules/d3-shape": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -4746,6 +4819,8 @@ }, "node_modules/d3-time": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -4756,6 +4831,8 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -4766,6 +4843,8 @@ }, "node_modules/d3-timer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -4825,6 +4904,8 @@ }, "node_modules/decimal.js-light": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, "node_modules/dedent": { @@ -4932,16 +5013,6 @@ "license": "MIT", "peer": true }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dotenv": { "version": "17.2.2", "dev": true, @@ -5083,6 +5154,17 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -5393,7 +5475,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/eventsource": { @@ -5546,15 +5630,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", - "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "dev": true, @@ -6228,6 +6303,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "dev": true, @@ -6268,6 +6353,8 @@ }, "node_modules/internmap": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -6540,6 +6627,7 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6769,12 +6857,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/log-symbols": { "version": "6.0.0", "dev": true, @@ -6812,18 +6894,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7244,6 +7314,7 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7923,23 +7994,6 @@ "node": ">=6" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8079,10 +8133,32 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT", "peer": true }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "dev": true, @@ -8185,21 +8261,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "license": "MIT", @@ -8220,22 +8281,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -8271,37 +8316,35 @@ } }, "node_modules/recharts": { - "version": "2.15.4", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -8316,6 +8359,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -8334,6 +8392,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "dev": true, @@ -9500,6 +9564,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -9514,7 +9587,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.9.2", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", diff --git a/backtest_fe/package.json b/backtest_fe/package.json index 482f4330..51629e73 100644 --- a/backtest_fe/package.json +++ b/backtest_fe/package.json @@ -50,7 +50,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", - "recharts": "^2.9.0", + "recharts": "^3.10.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, diff --git a/backtest_fe/src/features/backtest/components/EquityChart.tsx b/backtest_fe/src/features/backtest/components/EquityChart.tsx index 3f12fcd5..ee869393 100644 --- a/backtest_fe/src/features/backtest/components/EquityChart.tsx +++ b/backtest_fe/src/features/backtest/components/EquityChart.tsx @@ -60,7 +60,8 @@ const EquityChart: React.FC = memo(({ data }) => { return ( - + {/* recharts v3: CartesianGrid의 yAxisId가 YAxis와 일치해야 가로 그리드가 눈금에 맞춰 그려진다. */} + = ({ tickFormatter={(value: number) => `₩${value.toFixed(0)}`} /> [`₩${value.toFixed(2)}`, '환율']} + formatter={(value: unknown) => [`₩${Number(value).toFixed(2)}`, '환율']} labelFormatter={(label: any) => `날짜: ${label}`} /> = memo(({ data, indicators, trades })

- + {/* recharts v3: CartesianGrid의 yAxisId가 YAxis와 일치해야 가로 그리드가 눈금에 맞춰 그려진다. */} + diff --git a/backtest_fe/src/features/backtest/components/StockPriceChart.tsx b/backtest_fe/src/features/backtest/components/StockPriceChart.tsx index 218b2563..f9c0cd9e 100644 --- a/backtest_fe/src/features/backtest/components/StockPriceChart.tsx +++ b/backtest_fe/src/features/backtest/components/StockPriceChart.tsx @@ -252,12 +252,13 @@ const StockPriceChart: React.FC = memo(({ stocksData, tick /> `날짜: ${label}`} - formatter={(value: any, name: string) => { + formatter={(value: any, name: unknown) => { if (!value) return null; - if (name === 'price') return [formatPrice(value), '주가']; - if (name === 'buySignal') return [formatPrice(value), '매수']; - if (name === 'sellSignal') return [formatPrice(value), '매도']; - return [value, name]; + const key = String(name); + if (key === 'price') return [formatPrice(value), '주가']; + if (key === 'buySignal') return [formatPrice(value), '매수']; + if (key === 'sellSignal') return [formatPrice(value), '매도']; + return [value, key]; }} /> {/* 주가 라인 */} diff --git a/backtest_fe/src/features/backtest/components/__tests__/ChartRendering.test.tsx b/backtest_fe/src/features/backtest/components/__tests__/ChartRendering.test.tsx new file mode 100644 index 00000000..bdcc878a --- /dev/null +++ b/backtest_fe/src/features/backtest/components/__tests__/ChartRendering.test.tsx @@ -0,0 +1,155 @@ +/** + * 차트 SVG 렌더링 회귀 테스트 (recharts v3) + * + * **테스트 범위**: + * - 커스텀 yAxisId를 쓰는 차트(EquityChart, OHLCChart)에서 가로 그리드가 + * Y축 눈금에 맞춰 그려지는지 검증한다. + * + * **배경 (recharts v2 → v3 breaking change)**: + * - v3부터 CartesianGrid는 자체 xAxisId/yAxisId(기본값 0)를 가지며, + * 이 값이 실제 YAxis의 yAxisId와 일치해야 해당 축의 눈금 기준으로 + * 가로 그리드 라인을 그린다. + * - 불일치 시 그리드는 사라지지 않고 "경계선 2줄"로만 축소되어 그려지므로 + * 타입 에러도 런타임 에러도 없이 조용히 눈금선만 사라진다. + * → 그래서 라인 개수를 직접 세는 방식으로 검증한다. + * + * **한계**: + * - ResponsiveContainer는 happy-dom에서 크기가 0이므로 우회하기 위해 + * 차트 내부 구조를 직접 렌더링하지 않고 실제 컴포넌트를 렌더링하되, + * 컨테이너 크기를 강제로 지정한다. + * - 색상/여백/툴팁 서식 등 시각적 요소는 검증하지 않는다. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { render } from '@testing-library/react'; +import EquityChart from '../EquityChart'; +import OHLCChart from '../OHLCChart'; + +// ResponsiveContainer가 실제 크기를 갖도록 강제한다. +// happy-dom은 레이아웃 엔진이 없어 모든 요소의 크기가 0이며, +// 전역 ResizeObserver 목(src/test/setup.ts)은 콜백을 호출하지 않는다. +// 이 파일에서만 크기를 통지하는 ResizeObserver로 교체한다. +const CHART_WIDTH = 800; +const CHART_HEIGHT = 400; + +const ORIGINAL_RO = global.ResizeObserver; +const ORIGINAL_RECT = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'getBoundingClientRect' +); +const nativeRect = HTMLElement.prototype.getBoundingClientRect; + +const sizedRect = () => + ({ + width: CHART_WIDTH, + height: CHART_HEIGHT, + top: 0, + left: 0, + bottom: CHART_HEIGHT, + right: CHART_WIDTH, + x: 0, + y: 0, + toJSON: () => ({}), + }) as DOMRect; + +class SizedResizeObserver { + constructor(private callback: ResizeObserverCallback) {} + + observe(target: Element) { + this.callback( + [{ target, contentRect: sizedRect() } as unknown as ResizeObserverEntry], + this as unknown as ResizeObserver + ); + } + + unobserve() {} + disconnect() {} +} + +beforeAll(() => { + global.ResizeObserver = SizedResizeObserver as unknown as typeof ResizeObserver; + + // ResponsiveContainer가 읽는 컨테이너 요소에만 크기를 부여한다. + // 모든 요소에 크기를 주면 Legend 높이가 400px로 잡혀 플롯 영역이 + // 음수 높이가 되고 축/그리드/곡선이 전부 사라진다. + Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { + configurable: true, + value(this: HTMLElement) { + if (this.classList?.contains('recharts-responsive-container')) { + return sizedRect(); + } + return nativeRect.call(this); + }, + }); +}); + +afterAll(() => { + global.ResizeObserver = ORIGINAL_RO; + if (ORIGINAL_RECT) { + Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', ORIGINAL_RECT); + } +}); + +const equityData = Array.from({ length: 10 }, (_, i) => ({ + date: `2023-01-${String(i + 1).padStart(2, '0')}`, + return_pct: i * 1.5, + drawdown_pct: -i * 0.4, +})); + +const ohlcData = Array.from({ length: 10 }, (_, i) => ({ + date: `2023-01-${String(i + 1).padStart(2, '0')}`, + open: 100 + i, + high: 105 + i, + low: 95 + i, + close: 102 + i, + volume: 1000 + i * 10, +})); + +describe('차트 SVG 렌더링 (recharts v3)', () => { + describe('EquityChart', () => { + it('SVG와 데이터 곡선을 렌더링한다', () => { + const { container } = render(); + + expect(container.querySelector('svg.recharts-surface')).toBeTruthy(); + // 수익률 Line + 드로우다운 Area + expect(container.querySelectorAll('.recharts-curve').length).toBeGreaterThan(0); + }); + + it('가로 그리드가 Y축 눈금에 맞춰 그려진다 (경계선 2줄이 아님)', () => { + const { container } = render(); + + const horizontal = container.querySelectorAll( + '.recharts-cartesian-grid-horizontal line' + ); + // yAxisId 불일치 시 2줄(위/아래 경계)로 축소된다. + expect(horizontal.length).toBeGreaterThan(2); + }); + + it('좌/우 Y축을 모두 렌더링한다', () => { + const { container } = render(); + + expect(container.querySelectorAll('.recharts-yAxis').length).toBe(2); + }); + }); + + describe('OHLCChart', () => { + it('가로 그리드가 price 축 눈금에 맞춰 그려진다', () => { + const { container } = render( + + ); + + const horizontal = container.querySelectorAll( + '.recharts-cartesian-grid-horizontal line' + ); + expect(horizontal.length).toBeGreaterThan(2); + }); + + it('거래량 Bar와 종가 Line을 렌더링한다', () => { + const { container } = render( + + ); + + expect(container.querySelectorAll('.recharts-bar-rectangle').length).toBeGreaterThan(0); + expect(container.querySelectorAll('.recharts-line-curve').length).toBeGreaterThan(0); + }); + }); +}); diff --git a/backtest_fe/src/features/backtest/components/results/BenchmarkIndexChart.tsx b/backtest_fe/src/features/backtest/components/results/BenchmarkIndexChart.tsx index bf42cb04..13963fc6 100644 --- a/backtest_fe/src/features/backtest/components/results/BenchmarkIndexChart.tsx +++ b/backtest_fe/src/features/backtest/components/results/BenchmarkIndexChart.tsx @@ -202,15 +202,16 @@ const BenchmarkIndexChart: React.FC = memo(({ tick={{ fontSize: 13 }} /> { + formatter={(value: unknown, name: unknown) => { const labels: Record = { portfolio: '내 포트폴리오', sp500: 'S&P 500', nasdaq: 'NASDAQ', }; - return [value.toFixed(2), labels[name] || name]; + const key = String(name); + return [Number(value).toFixed(2), labels[key] || key]; }} - labelFormatter={(label: string) => `날짜: ${label}`} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} contentStyle={{ backgroundColor: 'rgba(255, 255, 255, 0.95)', border: '1px solid #ccc', diff --git a/backtest_fe/src/features/backtest/components/results/BenchmarkReturnsChart.tsx b/backtest_fe/src/features/backtest/components/results/BenchmarkReturnsChart.tsx index ed89eb08..6a907933 100644 --- a/backtest_fe/src/features/backtest/components/results/BenchmarkReturnsChart.tsx +++ b/backtest_fe/src/features/backtest/components/results/BenchmarkReturnsChart.tsx @@ -130,15 +130,16 @@ export const BenchmarkReturnsChart: React.FC = memo( tick={{ fontSize: 13 }} /> { + formatter={(value: unknown, name: unknown) => { const labels: Record = { portfolio: '내 포트폴리오', sp500: 'S&P 500', nasdaq: 'NASDAQ', }; - return [`${value.toFixed(2)}%`, labels[name] || name]; + const key = String(name); + return [`${Number(value).toFixed(2)}%`, labels[key] || key]; }} - labelFormatter={(label: string) => `날짜: ${label}`} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} contentStyle={{ backgroundColor: 'rgba(255, 255, 255, 0.95)', border: '1px solid #ccc', diff --git a/backtest_fe/src/features/backtest/components/results/ChartsSection/PortfolioCharts.tsx b/backtest_fe/src/features/backtest/components/results/ChartsSection/PortfolioCharts.tsx index 1d5a8259..c08b1dd9 100644 --- a/backtest_fe/src/features/backtest/components/results/ChartsSection/PortfolioCharts.tsx +++ b/backtest_fe/src/features/backtest/components/results/ChartsSection/PortfolioCharts.tsx @@ -117,11 +117,11 @@ export const PortfolioCharts: React.FC = memo(({ formatCurrency(value)} /> [ - formatCurrency(value), + formatter={(value: unknown) => [ + formatCurrency(Number(value)), isMultipleStocks ? '포트폴리오 가치' : '자산 가치', ]} - labelFormatter={(label: string) => `날짜: ${label}`} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} /> {/* 리밸런싱 마커 - 차트 데이터에 포함된 날짜 기준 */} {rebalance_history?.map((event, idx) => ( @@ -162,8 +162,8 @@ export const PortfolioCharts: React.FC = memo(({ tickFormatter={(value: number) => `${value.toFixed(1)}%`} /> [`${value.toFixed(2)}%`, `${periodLabel} 수익률`]} - labelFormatter={(label: string) => `날짜: ${label}`} + formatter={(value: unknown) => [`${Number(value).toFixed(2)}%`, `${periodLabel} 수익률`]} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} /> = memo(({ tickFormatter={(value: number) => formatKRW(value)} /> [formatKRW(value, 2), '환율']} - labelFormatter={(label: string) => `날짜: ${label}`} + formatter={(value: unknown) => [formatKRW(Number(value), 2), '환율']} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} /> = memo(({ domain={[0, 100]} /> [`${value.toFixed(2)}%`, symbolDisplayNames[name] || name]} - labelFormatter={(label: string) => `날짜: ${label}`} + formatter={(value: unknown, name: unknown) => { + const key = String(name); + return [`${Number(value).toFixed(2)}%`, symbolDisplayNames[key] || key]; + }} + labelFormatter={(label: unknown) => `날짜: ${String(label)}`} contentStyle={{ backgroundColor: 'rgba(255, 255, 255, 0.95)', border: '1px solid #ccc', From 38d6a8efcbef0b14b63362011212db24259b14af Mon Sep 17 00:00:00 2001 From: kyj0503 Date: Sat, 1 Aug 2026 03:31:53 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore(fe):=20Tailwind=20CSS=203=20=E2=86=92?= =?UTF-8?q?=204=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tailwindcss ^3.3.0 → ^4.3.3, @tailwindcss/postcss 추가 - autoprefixer 제거 (v4가 벤더 프리픽스를 자체 처리) - postcss.config.js 플러그인 교체 - tailwind.config.js 삭제, 설정을 src/index.css로 이관 (components.json의 config 경로도 빈 문자열로 갱신) ## 런타임 테마 주입 보존 이 앱은 useTheme이 root.style.setProperty()로 CSS 변수를 써넣어 테마를 바꾼다. v4의 @theme이 값을 빌드타임에 고정해버리면 테마 전환이 조용히 죽는다. index.css에 이미 있던 @theme inline 블록(v3에서는 무시되던 죽은 코드)이 이 용도에 정확히 맞아 그대로 활용했다. 색상 리터럴은 @theme으로 옮기지 않았다. 빌드된 CSS로 확인: 유틸리티가 var(--background) 등을 참조하는 규칙 105건, --color-background 방출 0건(고정 흔적 없음). ## darkMode: class 복원 v4는 prefers-color-scheme이 기본이라 @custom-variant dark (&:is(.dark *)) 를 추가했다. 빌드 CSS에 .dark\: 셀렉터 14건, prefers-color-scheme 0건. ## 유틸리티 변경 대응 - bg-black bg-opacity-50 → bg-black/50 (LoadingSpinner) bg-opacity-*는 v4에서 제거됐다. 그대로 뒀으면 전체 화면 로딩 오버레이가 반투명이 아니라 불투명 검정이 됐다. - outline-none → outline-hidden (16곳) v3의 outline-none은 outline: 2px solid transparent였으나 v4에서는 outline-style: none이다. 기존 동작은 outline-hidden이다. - backdrop-blur-sm → backdrop-blur-xs (5곳) v4에서 blur-sm이 4px → 8px로 바뀌었다. - flex-shrink-0 → shrink-0 (6곳) - container → app-container (6곳) v4가 container 설정 객체를 제거하고 브레이크포인트별 max-width를 가진 자체 .container를 방출한다. 그대로 두면 768~1023px 구간에서 랜딩 페이지가 768px로 좁아진다. v3 설정(center, padding 2rem, 2xl 1400px)을 재현한 .app-container를 components 레이어에 정의했다. @tailwindcss/upgrade 코드모드는 쓰지 않았다. 이 프로젝트는 --radius-*와 --shadow-*를 직접 정의하므로 코드모드의 일괄 개명(rounded-sm→rounded-xs, shadow-sm→shadow-xs 등)이 오히려 모든 모서리와 그림자를 바꿔놓는다. ## 의도된 시각 변화 v3 설정은 boxShadow를 확장하지 않아 :root의 --shadow-* 토큰이 실제로는 쓰이지 않았다. v4에서 @theme inline이 이를 연결하므로 그림자가 테마 값을 따르게 된다. rounded-xl도 고정 0.75rem에서 calc(var(--radius) + 4px)로 바뀌어 테마에 따라 달라진다. CSS 번들 49.71 → 73.81 kB (gzip 9.34 → 12.35 kB). @property 등록, @layer theme 기본 토큰, 합성 가능한 box-shadow 체인 등 v4 구조상 오버헤드다. 검증: test:run 104 passed(기존 실패 4건 동일), lint 74 problems 동일, type-check 통과, build 성공, 청크 3종 유지, dev 서버 200 및 CSS 89KB 서빙. Co-Authored-By: Claude Opus 5 (1M context) --- backtest_fe/components.json | 2 +- backtest_fe/package-lock.json | 1074 +++++++++-------- backtest_fe/package.json | 4 +- backtest_fe/postcss.config.js | 3 +- .../backtest/components/BacktestResults.tsx | 4 +- .../backtest/components/StrategyHelpModal.tsx | 2 +- .../components/results/WarningBanner.tsx | 2 +- .../components/volatility/VolatilityTable.tsx | 2 +- backtest_fe/src/index.css | 59 +- backtest_fe/src/pages/PortfolioPage.tsx | 4 +- backtest_fe/src/pages/landing/CTASection.tsx | 2 +- backtest_fe/src/pages/landing/HeroSection.tsx | 2 +- .../src/pages/landing/HowToUseSection.tsx | 2 +- .../src/pages/landing/KeyFeaturesSection.tsx | 8 +- .../pages/landing/WhatIsBacktestSection.tsx | 2 +- .../components/feedback/ErrorMessage.tsx | 4 +- .../components/layout/ErrorBoundary.tsx | 2 +- .../components/layout/ThemeSelector.tsx | 2 +- .../components/loading/LoadingSpinner.tsx | 2 +- backtest_fe/src/shared/ui/badge.tsx | 2 +- backtest_fe/src/shared/ui/button.tsx | 2 +- backtest_fe/src/shared/ui/dialog.tsx | 4 +- backtest_fe/src/shared/ui/dropdown-menu.tsx | 8 +- backtest_fe/src/shared/ui/input.tsx | 2 +- backtest_fe/src/shared/ui/select.tsx | 4 +- backtest_fe/src/shared/ui/tabs.tsx | 4 +- backtest_fe/src/shared/ui/textarea.tsx | 2 +- backtest_fe/tailwind.config.js | 92 -- 28 files changed, 673 insertions(+), 629 deletions(-) delete mode 100644 backtest_fe/tailwind.config.js diff --git a/backtest_fe/components.json b/backtest_fe/components.json index b321c760..5f270b54 100644 --- a/backtest_fe/components.json +++ b/backtest_fe/components.json @@ -4,7 +4,7 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "", "css": "src/index.css", "baseColor": "slate", "cssVariables": true, diff --git a/backtest_fe/package-lock.json b/backtest_fe/package-lock.json index 6b504b27..e4e624b1 100644 --- a/backtest_fe/package-lock.json +++ b/backtest_fe/package-lock.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.1", + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -45,7 +46,6 @@ "@vitejs/plugin-react": "^4.0.0", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", - "autoprefixer": "^10.4.21", "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", @@ -55,7 +55,7 @@ "patch-package": "^8.0.0", "postcss": "^8.5.6", "shadcn": "^3.2.1", - "tailwindcss": "^3.3.0", + "tailwindcss": "^4.3.3", "typescript": "^5.0.0", "vite": "^7.2.2", "vitest": "^4.1.10" @@ -3043,6 +3043,277 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -3945,28 +4216,6 @@ "node": ">=14" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -4038,42 +4287,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/axios": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", @@ -4129,17 +4342,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -4338,14 +4540,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001743", "dev": true, @@ -4390,40 +4584,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -4696,17 +4856,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cssstyle": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", @@ -4981,15 +5130,20 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/diff": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", @@ -5000,11 +5154,6 @@ "node": ">=0.3.1" } }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -5079,6 +5228,20 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -5885,18 +6048,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -6385,31 +6536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -6608,11 +6734,13 @@ } }, "node_modules/jiti": { - "version": "1.21.7", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/jose": { @@ -6810,32 +6938,282 @@ "node_modules/kleur": { "version": "4.1.5", "dev": true, - "license": "MIT", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -7178,16 +7556,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.16", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", @@ -7270,22 +7638,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-run-path": { "version": "6.0.0", "dev": true, @@ -7320,14 +7672,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7675,11 +8019,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "6.3.0", "dev": true, @@ -7710,22 +8049,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/pkce-challenge": { "version": "5.0.0", "dev": true, @@ -7807,121 +8130,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -8281,25 +8489,6 @@ } } }, - "node_modules/read-cache": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/recast": { "version": "0.23.11", "dev": true, @@ -8398,25 +8587,6 @@ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, - "node_modules/resolve": { - "version": "1.22.10", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -9031,37 +9201,6 @@ "node": ">=8" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -9073,17 +9212,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -9100,58 +9228,24 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } + "license": "MIT" }, - "node_modules/thenify-all": { - "version": "1.6.0", + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, "engines": { - "node": ">=0.8" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tiny-invariant": { @@ -9329,11 +9423,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ts-morph": { "version": "26.0.0", "dev": true, @@ -9573,11 +9662,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "dev": true, diff --git a/backtest_fe/package.json b/backtest_fe/package.json index 51629e73..f409f48c 100644 --- a/backtest_fe/package.json +++ b/backtest_fe/package.json @@ -56,6 +56,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.1", + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -66,7 +67,6 @@ "@vitejs/plugin-react": "^4.0.0", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", - "autoprefixer": "^10.4.21", "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", @@ -76,7 +76,7 @@ "patch-package": "^8.0.0", "postcss": "^8.5.6", "shadcn": "^3.2.1", - "tailwindcss": "^3.3.0", + "tailwindcss": "^4.3.3", "typescript": "^5.0.0", "vite": "^7.2.2", "vitest": "^4.1.10" diff --git a/backtest_fe/postcss.config.js b/backtest_fe/postcss.config.js index 2e7af2b7..a7f73a2d 100644 --- a/backtest_fe/postcss.config.js +++ b/backtest_fe/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, } diff --git a/backtest_fe/src/features/backtest/components/BacktestResults.tsx b/backtest_fe/src/features/backtest/components/BacktestResults.tsx index 72b82ae2..1120a1e9 100644 --- a/backtest_fe/src/features/backtest/components/BacktestResults.tsx +++ b/backtest_fe/src/features/backtest/components/BacktestResults.tsx @@ -48,7 +48,7 @@ const BacktestResults: React.FC = ({ data, isPortfolio }) if (!data) { return ( -
+
@@ -60,7 +60,7 @@ const BacktestResults: React.FC = ({ data, isPortfolio }) if (isPortfolio && (!('portfolio_composition' in data) || !data.portfolio_composition)) { return ( -
+
diff --git a/backtest_fe/src/features/backtest/components/StrategyHelpModal.tsx b/backtest_fe/src/features/backtest/components/StrategyHelpModal.tsx index 1a1a8b2d..f7f04e5e 100644 --- a/backtest_fe/src/features/backtest/components/StrategyHelpModal.tsx +++ b/backtest_fe/src/features/backtest/components/StrategyHelpModal.tsx @@ -64,7 +64,7 @@ const StrategyHelpModal: React.FC = ({ {strategy.description}
-
+
{strategy.category} diff --git a/backtest_fe/src/features/backtest/components/results/WarningBanner.tsx b/backtest_fe/src/features/backtest/components/results/WarningBanner.tsx index 784ff80b..6f25acc0 100644 --- a/backtest_fe/src/features/backtest/components/results/WarningBanner.tsx +++ b/backtest_fe/src/features/backtest/components/results/WarningBanner.tsx @@ -21,7 +21,7 @@ const WarningBanner: React.FC = ({ warnings }) => { return (
-
+
diff --git a/backtest_fe/src/features/backtest/components/volatility/VolatilityTable.tsx b/backtest_fe/src/features/backtest/components/volatility/VolatilityTable.tsx index 8ce6da95..e39aea40 100644 --- a/backtest_fe/src/features/backtest/components/volatility/VolatilityTable.tsx +++ b/backtest_fe/src/features/backtest/components/volatility/VolatilityTable.tsx @@ -62,7 +62,7 @@ const VolatilityTable: React.FC = ({ selectedStock, events - ); - })} -
-
- - {/* 변동성 테이블 */} - - - )} - - - - {/* 뉴스 모달 */} - - - ); -}; - -export default StockVolatilityNews; diff --git a/backtest_fe/src/features/backtest/components/lazy/LazyChartComponents.tsx b/backtest_fe/src/features/backtest/components/lazy/LazyChartComponents.tsx index 071cfc36..f2d88dd4 100644 --- a/backtest_fe/src/features/backtest/components/lazy/LazyChartComponents.tsx +++ b/backtest_fe/src/features/backtest/components/lazy/LazyChartComponents.tsx @@ -6,11 +6,5 @@ export const LazyOHLCChart = lazy(() => import('../OHLCChart')); export const LazyTradesChart = lazy(() => import('../TradesChart')); export const LazyStockPriceChart = lazy(() => import('../StockPriceChart')); -// 뉴스 관련 컴포넌트 지연 로딩 -export const LazyStockVolatilityNews = lazy(() => import('../StockVolatilityNews')); - // 통계 컴포넌트 지연 로딩 export const LazyStatsSummary = lazy(() => import('../StatsSummary')); - -// 환율 차트 지연 로딩 -export const LazyExchangeRateChart = lazy(() => import('../ExchangeRateChart')); diff --git a/backtest_fe/src/features/backtest/hooks/useExchangeRate.ts b/backtest_fe/src/features/backtest/hooks/useExchangeRate.ts deleted file mode 100644 index cf45a17b..00000000 --- a/backtest_fe/src/features/backtest/hooks/useExchangeRate.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { getExchangeRate } from '../api/backtestApi'; - -interface ExchangeRateData { - date: string; - rate: number; - volume?: number; -} - -interface UseExchangeRateParams { - startDate: string; - endDate: string; - enabled?: boolean; -} - -interface UseExchangeRateReturn { - exchangeData: ExchangeRateData[]; - loading: boolean; - error: string | null; - refetch: () => Promise; -} - -/** - * 환율 데이터 페칭을 위한 커스텀 훅 - * USD/KRW 환율 데이터를 가져오고 캐싱 기능 제공 - */ -export const useExchangeRate = ({ - startDate, - endDate, - enabled = true -}: UseExchangeRateParams): UseExchangeRateReturn => { - const [exchangeData, setExchangeData] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const abortControllerRef = useRef(null); - - const fetchExchangeRate = useCallback(async () => { - if (!enabled || !startDate || !endDate) return; - - // 이전 요청 취소 - abortControllerRef.current?.abort(); - const controller = new AbortController(); - abortControllerRef.current = controller; - - setLoading(true); - setError(null); - - try { - const response = await getExchangeRate(startDate, endDate, controller.signal); - - if (controller.signal.aborted) return; - - if (response.status === 'success' && response.data.exchange_rates) { - setExchangeData(response.data.exchange_rates); - } else { - setError(response.message || '환율 데이터를 가져올 수 없습니다.'); - setExchangeData([]); - } - } catch (err) { - if (controller.signal.aborted) return; - console.error('환율 데이터 조회 실패:', err); - setError('환율 데이터 조회에 실패했습니다.'); - setExchangeData([]); - } finally { - if (!controller.signal.aborted) { - setLoading(false); - } - } - }, [startDate, endDate, enabled]); - - useEffect(() => { - fetchExchangeRate(); - return () => { - abortControllerRef.current?.abort(); - }; - }, [fetchExchangeRate]); - - return { - exchangeData, - loading, - error, - refetch: fetchExchangeRate - }; -}; diff --git a/backtest_fe/src/features/backtest/hooks/useStockData.ts b/backtest_fe/src/features/backtest/hooks/useStockData.ts deleted file mode 100644 index c5bca078..00000000 --- a/backtest_fe/src/features/backtest/hooks/useStockData.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { getStockData } from '../api/backtestApi'; -import { StockDataItem } from '../model/types/backtest-result-types'; - -interface UseStockDataParams { - symbols: string[]; - startDate: string; - endDate: string; - enabled?: boolean; -} - -interface UseStockDataReturn { - stocksData: StockDataItem[]; - loading: boolean; - error: string | null; - refetch: () => Promise; -} - -/** - * 주가 데이터 페칭을 위한 커스텀 훅 - * 여러 종목의 주가 데이터를 병렬로 가져오고 캐싱 기능 제공 - */ -export const useStockData = ({ - symbols, - startDate, - endDate, - enabled = true -}: UseStockDataParams): UseStockDataReturn => { - const [stocksData, setStocksData] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const abortControllerRef = useRef(null); - - // 현금 자산이 아닌 유효한 심볼만 필터링 - const validSymbols = symbols.filter(symbol => - symbol.toUpperCase() !== 'CASH' && symbol !== '현금' - ); - - const fetchStockData = useCallback(async () => { - if (!enabled || validSymbols.length === 0) return; - - // 이전 요청 취소 - abortControllerRef.current?.abort(); - const controller = new AbortController(); - abortControllerRef.current = controller; - - setLoading(true); - setError(null); - - try { - const stockDataResults: StockDataItem[] = []; - - // 병렬로 모든 종목 데이터 가져오기 - const fetchPromises = validSymbols.map(async (symbol) => { - try { - const response = await getStockData(symbol, startDate, endDate, controller.signal); - if (response.status === 'success' && response.data.price_data.length > 0) { - return { - symbol: symbol, - data: response.data.price_data - }; - } - return null; - } catch (error) { - console.error(`주가 데이터 가져오기 실패 (${symbol}):`, error); - return null; - } - }); - - const results = await Promise.all(fetchPromises); - - // 취소된 요청이면 상태 업데이트 무시 - if (controller.signal.aborted) return; - - // null이 아닌 결과만 필터링 - results.forEach(result => { - if (result) { - stockDataResults.push(result); - } - }); - - setStocksData(stockDataResults); - } catch (err) { - if (controller.signal.aborted) return; - console.error('주가 데이터 가져오기 전체 실패:', err); - setError('주가 데이터를 가져오는 중 오류가 발생했습니다.'); - } finally { - if (!controller.signal.aborted) { - setLoading(false); - } - } - }, [validSymbols.join(','), startDate, endDate, enabled]); - - useEffect(() => { - fetchStockData(); - return () => { - abortControllerRef.current?.abort(); - }; - }, [fetchStockData]); - - return { - stocksData, - loading, - error, - refetch: fetchStockData - }; -}; diff --git a/backtest_fe/src/features/backtest/hooks/useVolatilityNews.ts b/backtest_fe/src/features/backtest/hooks/useVolatilityNews.ts deleted file mode 100644 index 2edb31d5..00000000 --- a/backtest_fe/src/features/backtest/hooks/useVolatilityNews.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { - VolatilityEvent, - NewsItem, -} from '../model/types/volatility-news-types'; -import { - getStockVolatilityNews, - getNaverNews, - getLatestTickerNews, -} from '../api/backtestApi'; - -interface UseVolatilityNewsParams { - symbols: string[]; - startDate: string; - endDate: string; - enabled?: boolean; - canViewNews?: boolean; -} - -interface UseVolatilityNewsReturn { - volatilityData: { [key: string]: VolatilityEvent[] }; - selectedStock: string; - newsData: NewsItem[]; - showNewsModal: boolean; - currentNewsEvent: VolatilityEvent | null; - loading: boolean; - newsLoading: boolean; - error: string | null; - actions: { - setSelectedStock: (symbol: string) => void; - openNewsModal: (date: string, event: VolatilityEvent) => Promise; - closeNewsModal: () => void; - refetch: () => Promise; - }; -} - -/** - * 주가 변동성 및 뉴스 데이터 관리를 위한 커스텀 훅 - * 변동성 이벤트 조회, 종목 선택, 뉴스 모달 상태 관리를 통합 - */ -export const useVolatilityNews = ({ - symbols, - startDate, - endDate, - enabled = true, - canViewNews = true -}: UseVolatilityNewsParams): UseVolatilityNewsReturn => { - const [volatilityData, setVolatilityData] = useState<{ [key: string]: VolatilityEvent[] }>({}); - const [selectedStock, setSelectedStock] = useState(''); - const [newsData, setNewsData] = useState([]); - const [showNewsModal, setShowNewsModal] = useState(false); - const [currentNewsEvent, setCurrentNewsEvent] = useState(null); - const [loading, setLoading] = useState(false); - const [newsLoading, setNewsLoading] = useState(false); - const [error, setError] = useState(null); - const abortControllerRef = useRef(null); - - // 현금이 아닌 유효한 심볼만 필터링 - const validSymbols = symbols.filter(symbol => - symbol.toUpperCase() !== 'CASH' && symbol !== '현금' - ); - - const fetchVolatilityData = useCallback(async () => { - if (!enabled || validSymbols.length === 0) return; - - // 이전 요청 취소 - abortControllerRef.current?.abort(); - const controller = new AbortController(); - abortControllerRef.current = controller; - - setLoading(true); - setError(null); - - try { - const results: { [key: string]: VolatilityEvent[] } = {}; - - for (const symbol of validSymbols) { - if (controller.signal.aborted) return; - try { - const response = await getStockVolatilityNews(symbol, startDate, endDate, 5.0, controller.signal); - if (response.status === 'success' && response.data.volatility_events) { - results[symbol] = response.data.volatility_events; - } else { - results[symbol] = []; - } - } catch (symbolError) { - console.warn(`Failed to fetch volatility data for ${symbol}:`, symbolError); - results[symbol] = []; - } - } - - if (controller.signal.aborted) return; - - setVolatilityData(results); - - // 첫 번째 유효한 종목을 선택 - if (validSymbols.length > 0 && !selectedStock && validSymbols[0]) { - setSelectedStock(validSymbols[0]); - } - } catch (err) { - if (controller.signal.aborted) return; - console.error('변동성 데이터 가져오기 실패:', err); - setError('변동성 데이터를 가져오는 중 오류가 발생했습니다.'); - } finally { - if (!controller.signal.aborted) { - setLoading(false); - } - } - }, [validSymbols.join(','), startDate, endDate, enabled]); - - const openNewsModal = async (_date: string, event: VolatilityEvent) => { - if (!canViewNews) { - return; - } - setCurrentNewsEvent(event); - setShowNewsModal(true); - setNewsLoading(true); - - try { - // 1. 먼저 특정 날짜의 뉴스 조회 시도 (DB 캐싱 사용) - const dateResponse = await getNaverNews(selectedStock, event.date, 50); - - if (dateResponse.status === 'success' && dateResponse.data && dateResponse.data.news_list && dateResponse.data.news_list.length > 0) { - setNewsData(dateResponse.data.news_list); - } else { - // 2. 특정 날짜에 뉴스가 없으면 최신 뉴스로 fallback - const latestResponse = await getLatestTickerNews(selectedStock, 15); - - if (latestResponse.status === 'success' && latestResponse.data && latestResponse.data.news_list) { - setNewsData(latestResponse.data.news_list); - } else { - console.warn('최신 뉴스 검색 실패:', latestResponse); - setNewsData([]); - } - } - } catch (err) { - console.error('뉴스 데이터 가져오기 실패:', err); - setNewsData([]); - } finally { - setNewsLoading(false); - } - }; - - const closeNewsModal = () => { - setShowNewsModal(false); - setCurrentNewsEvent(null); - setNewsData([]); - }; - - useEffect(() => { - fetchVolatilityData(); - return () => { - abortControllerRef.current?.abort(); - }; - }, [fetchVolatilityData]); - - return { - volatilityData, - selectedStock, - newsData, - showNewsModal, - currentNewsEvent, - loading, - newsLoading, - error, - actions: { - setSelectedStock, - openNewsModal, - closeNewsModal, - refetch: fetchVolatilityData - } - }; -}; diff --git a/backtest_fe/src/features/backtest/model/types/api-types.ts b/backtest_fe/src/features/backtest/model/types/api-types.ts index f8e4eb89..e5678527 100644 --- a/backtest_fe/src/features/backtest/model/types/api-types.ts +++ b/backtest_fe/src/features/backtest/model/types/api-types.ts @@ -1,256 +1,212 @@ -// API 타입 정의 - 통합 및 확장 -import { NewsItem } from './backtest-result-types'; - -export type { NewsItem }; - -// 전략 파라미터 값 타입 -export type StrategyParamValue = string | number | boolean; - -// 공통 응답 인터페이스 -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; - message?: string; -} - -// 페이지네이션 응답 -export interface PaginatedResponse { - items: T[]; - total: number; - page: number; - pageSize: number; - totalPages: number; -} - -// 에러 응답 -export interface ApiError { - code: string; - message: string; - details?: Record; -} - -// 포트폴리오 구성 요소 -export interface PortfolioStock { - symbol: string; - amount?: number; // 금액 기준 (amount와 weight는 배타적) - weight?: number; // 비중 기준 (amount와 weight는 배타적) - investment_type?: 'lump_sum' | 'dca'; - dca_frequency?: 'weekly_1' | 'weekly_2' | 'monthly_1' | 'monthly_2' | 'monthly_3' | 'monthly_6' | 'monthly_12'; - asset_type?: 'stock' | 'cash'; -} - -// 백테스트 요청 (단일/포트폴리오 통합) -export interface BacktestRequest { - portfolio: PortfolioStock[]; - start_date: string; - end_date: string; - strategy: string; - strategy_params?: Record; - commission?: number; - rebalance_frequency?: string; -} - -// 백테스트 결과 통계 -export interface BacktestStats { - total_return_pct: number; - total_trades: number; - win_rate_pct: number; - max_drawdown_pct: number; - sharpe_ratio: number; - profit_factor: number; - volatility_pct?: number; - alpha?: number; - beta?: number; - calmar_ratio?: number; - sortino_ratio?: number; - var_95?: number; - cvar_95?: number; - // 벤치마크 관련 (선택적) - benchmark_ticker?: string; - benchmark_total_return_pct?: number; - alpha_vs_benchmark_pct?: number; - [key: string]: unknown; -} - -// 포트폴리오 통계 (포트폴리오 백테스트용) -export interface PortfolioStats { - total_return: number; - annual_return: number; - volatility: number; - sharpe_ratio: number; - max_drawdown: number; - calmar_ratio: number; - alpha: number; - beta: number; - var_95: number; - cvar_95: number; - win_rate: number; - profit_factor: number; - total_trades: number; -} - -// 차트 데이터 포인트 -export interface ChartDataPoint { - timestamp: string; - date: string; - open: number; - high: number; - low: number; - close: number; - volume: number; - [key: string]: unknown; -} - -// 포트폴리오 차트 데이터 (필드명이 다름) -export interface PortfolioChartDataPoint { - timestamp: string; - date: string; - price: number; - cumulative_return: number; - daily_return: number; - volatility: number; -} - -export interface EquityPoint { - timestamp: string; - date: string; - equity: number; - return_pct: number; - drawdown_pct: number; - [key: string]: unknown; -} - -export interface TradeMarker { - timestamp: string; - date: string; - price: number; - type: 'entry' | 'exit'; - side: 'buy' | 'sell'; - size: number; - pnl_pct?: number; -} - -export interface IndicatorData { - name: string; - type: string; - color: string; - data: Array<{ - timestamp: string; - date: string; - value: number; - }>; - [key: string]: unknown; -} - -// 단일 종목 백테스트 응답 -export interface ChartDataResponse { - ticker: string; - strategy: string; - start_date: string; - end_date: string; - ohlc_data: ChartDataPoint[]; - equity_data: EquityPoint[]; - trade_markers: TradeMarker[]; - indicators: IndicatorData[]; - summary_stats: BacktestStats; -} - -// 포트폴리오 백테스트 응답 -export interface PortfolioBacktestResponse { - portfolio_composition: Array<{ - symbol: string; - weight: number; - amount: number; - asset_type: string; - }>; - chart_data: PortfolioChartDataPoint[]; - stats: PortfolioStats; - start_date: string; - end_date: string; - strategy: string; - sp500_benchmark?: Array<{ date: string; close: number; volume: number }>; - nasdaq_benchmark?: Array<{ date: string; close: number; volume: number }>; -} - -// 전략 관련 타입 -export interface Strategy { - name: string; - description: string; - parameters: StrategyParameter[]; - category: string; -} - -export interface StrategyParameter { - name: string; - type: 'int' | 'float' | 'str' | 'bool' | 'select'; - default: StrategyParamValue; - description: string; - min?: number; - max?: number; - options?: string[]; - required?: boolean; -} - -// 뉴스 관련 타입 -export interface NewsResponse { - lastBuildDate: string; - total: number; - start: number; - display: number; - items: NewsItem[]; -} - -// 변동성 데이터 타입 -export interface VolatilityData { - symbol: string; - period: string; - volatility: number; - price_change: number; - volume_change: number; - last_updated: string; -} - -// 환율 데이터 타입 -export interface ExchangeRateData { - date: string; - usd_krw: number; - change_rate: number; - trend: 'up' | 'down' | 'stable'; -} - -// 시스템 정보 타입 -export interface SystemInfo { - version: string; - environment: string; - last_updated: string; - database_status: 'connected' | 'disconnected'; - api_status: 'healthy' | 'degraded' | 'down'; -} - -// HTTP 메서드 타입 -export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - -// 통합 백테스트 응답 (새로운 /execute 엔드포인트용) -export interface UnifiedBacktestResponse { - status: 'success' | 'error'; - backtest_type: 'single_stock' | 'portfolio'; - data: ChartDataResponse | PortfolioBacktestResponse; - message?: string; - warnings?: string[]; // 상장일 경고 메시지 (백테스트 실행 전 검증 실패 시 반환되지 않음) -} - -// 통합 백테스트 결과 (프론트엔드 처리용) -export interface UnifiedBacktestResult extends Partial, Partial { - backtest_type?: 'single_stock' | 'portfolio'; - api_version?: 'unified' | 'legacy'; -} - -// API 엔드포인트 타입 -export type ApiEndpoint = - | '/api/v1/backtest' // 통합 백테스트 엔드포인트 - | '/api/v1/strategies' - | '/api/v1/naver-news/search' - | '/api/v1/yfinance/exchange-rate' - | '/api/v1/system/info' - | '/api/v1/optimize/run'; +// API 타입 정의 - 통합 및 확장 +import { NewsItem } from './backtest-result-types'; + +export type { NewsItem }; + +// 전략 파라미터 값 타입 +export type StrategyParamValue = string | number | boolean; + +// 공통 응답 인터페이스 +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +// 페이지네이션 응답 +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +// 에러 응답 +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +// 포트폴리오 구성 요소 +export interface PortfolioStock { + symbol: string; + amount?: number; // 금액 기준 (amount와 weight는 배타적) + weight?: number; // 비중 기준 (amount와 weight는 배타적) + investment_type?: 'lump_sum' | 'dca'; + dca_frequency?: 'weekly_1' | 'weekly_2' | 'monthly_1' | 'monthly_2' | 'monthly_3' | 'monthly_6' | 'monthly_12'; + asset_type?: 'stock' | 'cash'; +} + +// 백테스트 요청 (단일/포트폴리오 통합) +export interface BacktestRequest { + portfolio: PortfolioStock[]; + start_date: string; + end_date: string; + strategy: string; + strategy_params?: Record; + commission?: number; + rebalance_frequency?: string; +} + +// 백테스트 결과 통계 +export interface BacktestStats { + total_return_pct: number; + total_trades: number; + win_rate_pct: number; + max_drawdown_pct: number; + sharpe_ratio: number; + profit_factor: number; + volatility_pct?: number; + alpha?: number; + beta?: number; + calmar_ratio?: number; + sortino_ratio?: number; + var_95?: number; + cvar_95?: number; + // 벤치마크 관련 (선택적) + benchmark_ticker?: string; + benchmark_total_return_pct?: number; + alpha_vs_benchmark_pct?: number; + [key: string]: unknown; +} + +// 포트폴리오 통계 (포트폴리오 백테스트용) +export interface PortfolioStats { + total_return: number; + annual_return: number; + volatility: number; + sharpe_ratio: number; + max_drawdown: number; + calmar_ratio: number; + alpha: number; + beta: number; + var_95: number; + cvar_95: number; + win_rate: number; + profit_factor: number; + total_trades: number; +} + +// 차트 데이터 포인트 +export interface ChartDataPoint { + timestamp: string; + date: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + [key: string]: unknown; +} + +// 포트폴리오 차트 데이터 (필드명이 다름) +export interface PortfolioChartDataPoint { + timestamp: string; + date: string; + price: number; + cumulative_return: number; + daily_return: number; + volatility: number; +} + +export interface EquityPoint { + timestamp: string; + date: string; + equity: number; + return_pct: number; + drawdown_pct: number; + [key: string]: unknown; +} + +export interface TradeMarker { + timestamp: string; + date: string; + price: number; + type: 'entry' | 'exit'; + side: 'buy' | 'sell'; + size: number; + pnl_pct?: number; +} + +export interface IndicatorData { + name: string; + type: string; + color: string; + data: Array<{ + timestamp: string; + date: string; + value: number; + }>; + [key: string]: unknown; +} + +// 단일 종목 백테스트 응답 +export interface ChartDataResponse { + ticker: string; + strategy: string; + start_date: string; + end_date: string; + ohlc_data: ChartDataPoint[]; + equity_data: EquityPoint[]; + trade_markers: TradeMarker[]; + indicators: IndicatorData[]; + summary_stats: BacktestStats; +} + +// 포트폴리오 백테스트 응답 +export interface PortfolioBacktestResponse { + portfolio_composition: Array<{ + symbol: string; + weight: number; + amount: number; + asset_type: string; + }>; + chart_data: PortfolioChartDataPoint[]; + stats: PortfolioStats; + start_date: string; + end_date: string; + strategy: string; + sp500_benchmark?: Array<{ date: string; close: number; volume: number }>; + nasdaq_benchmark?: Array<{ date: string; close: number; volume: number }>; +} + +// 전략 관련 타입 +export interface Strategy { + name: string; + description: string; + parameters: StrategyParameter[]; + category: string; +} + +export interface StrategyParameter { + name: string; + type: 'int' | 'float' | 'str' | 'bool' | 'select'; + default: StrategyParamValue; + description: string; + min?: number; + max?: number; + options?: string[]; + required?: boolean; +} + +// HTTP 메서드 타입 +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +// 통합 백테스트 응답 (새로운 /execute 엔드포인트용) +export interface UnifiedBacktestResponse { + status: 'success' | 'error'; + backtest_type: 'single_stock' | 'portfolio'; + data: ChartDataResponse | PortfolioBacktestResponse; + message?: string; + warnings?: string[]; // 상장일 경고 메시지 (백테스트 실행 전 검증 실패 시 반환되지 않음) +} + +// 통합 백테스트 결과 (프론트엔드 처리용) +export interface UnifiedBacktestResult extends Partial, Partial { + backtest_type?: 'single_stock' | 'portfolio'; + api_version?: 'unified' | 'legacy'; +} + diff --git a/backtest_fe/src/features/backtest/model/types/index.ts b/backtest_fe/src/features/backtest/model/types/index.ts index fb13c172..a2ab09d7 100644 --- a/backtest_fe/src/features/backtest/model/types/index.ts +++ b/backtest_fe/src/features/backtest/model/types/index.ts @@ -1,77 +1,71 @@ -/** - * 타입 정의 통합 export - * 모든 타입 정의를 한 곳에서 import할 수 있도록 통합 - * - * Note: 중복 방지를 위해 각 모듈에서 특정 타입만 선택적으로 export - */ - -// API 관련 타입 (중복 제외) -export type { - StrategyParamValue, - ApiResponse, - PaginatedResponse, - ApiError, - PortfolioStock, - BacktestRequest, - BacktestStats, - PortfolioStats, - ChartDataPoint, - PortfolioChartDataPoint, - IndicatorData, - ChartDataResponse, - PortfolioBacktestResponse, - Strategy, - StrategyParameter, - NewsResponse, - VolatilityData, - ExchangeRateData, - SystemInfo, - HttpMethod, - UnifiedBacktestResponse, - UnifiedBacktestResult, - ApiEndpoint -} from './api-types'; - -// 폼 관련 타입 -export type { - Stock, - PortfolioInputMode, - BacktestFormState, - BacktestFormAction -} from './backtest-form-types'; - -export { initialBacktestFormState } from './backtest-form-types'; - -// 결과 관련 타입 (우선순위 타입들 - EquityPoint, TradeMarker, NewsItem 등) -export type { - IndividualReturn, - OhlcPoint, - EquityPoint, - TradeMarker, - IndicatorPoint, - ExchangeRatePoint, - ExchangeRateStats, - BenchmarkPoint, - BenchmarkSeriesPoint, - PortfolioStatistics, - ChartData, - NewsItem, - RebalanceTrade, - RebalanceEvent, - WeightHistoryPoint, - TickerInfo, - TradeLog, - StrategyStats, - PortfolioData, - BacktestResultsProps, - StockDataItem, - EquityChartDataItem -} from './backtest-result-types'; - -// 급등락 관련 타입 -export type { - VolatilityEvent, - StockVolatilityNewsProps -} from './volatility-news-types'; - -export { TICKER_TO_COMPANY_NAME, getCompanyName } from './volatility-news-types'; +/** + * 타입 정의 통합 export + * 모든 타입 정의를 한 곳에서 import할 수 있도록 통합 + * + * Note: 중복 방지를 위해 각 모듈에서 특정 타입만 선택적으로 export + */ + +// API 관련 타입 (중복 제외) +export type { + StrategyParamValue, + ApiResponse, + PaginatedResponse, + ApiError, + PortfolioStock, + BacktestRequest, + BacktestStats, + PortfolioStats, + ChartDataPoint, + PortfolioChartDataPoint, + IndicatorData, + ChartDataResponse, + PortfolioBacktestResponse, + Strategy, + StrategyParameter, + HttpMethod, + UnifiedBacktestResponse, + UnifiedBacktestResult +} from './api-types'; + +// 폼 관련 타입 +export type { + Stock, + PortfolioInputMode, + BacktestFormState, + BacktestFormAction +} from './backtest-form-types'; + +export { initialBacktestFormState } from './backtest-form-types'; + +// 결과 관련 타입 (우선순위 타입들 - EquityPoint, TradeMarker, NewsItem 등) +export type { + IndividualReturn, + OhlcPoint, + EquityPoint, + TradeMarker, + IndicatorPoint, + ExchangeRatePoint, + ExchangeRateStats, + BenchmarkPoint, + BenchmarkSeriesPoint, + PortfolioStatistics, + ChartData, + NewsItem, + RebalanceTrade, + RebalanceEvent, + WeightHistoryPoint, + TickerInfo, + TradeLog, + StrategyStats, + PortfolioData, + BacktestResultsProps, + StockDataItem, + EquityChartDataItem +} from './backtest-result-types'; + +// 급등락 관련 타입 +export type { + VolatilityEvent +} from './volatility-news-types'; + +export { TICKER_TO_COMPANY_NAME, getCompanyName } from './volatility-news-types'; diff --git a/backtest_fe/src/features/backtest/model/types/volatility-news-types.ts b/backtest_fe/src/features/backtest/model/types/volatility-news-types.ts index fafe1e99..ec995fb3 100644 --- a/backtest_fe/src/features/backtest/model/types/volatility-news-types.ts +++ b/backtest_fe/src/features/backtest/model/types/volatility-news-types.ts @@ -11,13 +11,6 @@ export interface VolatilityEvent { event_type: string; } -export interface StockVolatilityNewsProps { - symbols: string[]; - startDate: string; - endDate: string; - className?: string; -} - // 티커 심볼을 한국어 기업명으로 매핑 (백엔드와 동일한 매핑 사용) export const TICKER_TO_COMPANY_NAME: { [key: string]: string } = { // 미국 주요 종목 diff --git a/backtest_fe/src/features/backtest/services/__tests__/backtestService.integration.test.ts b/backtest_fe/src/features/backtest/services/__tests__/backtestService.integration.test.ts index 99499c10..c68a2fa2 100644 --- a/backtest_fe/src/features/backtest/services/__tests__/backtestService.integration.test.ts +++ b/backtest_fe/src/features/backtest/services/__tests__/backtestService.integration.test.ts @@ -6,7 +6,6 @@ import { apiClient } from '@/shared/api/client' import type { BacktestRequest, UnifiedBacktestResponse, - NewsResponse, } from '../../model/types/api-types' const baseRequest: BacktestRequest = { @@ -90,38 +89,4 @@ describe('BacktestService (integration)', () => { await expect(BacktestService.executeBacktest(baseRequest)).rejects.toThrow() }) - - it('sends query parameters when searching news', async () => { - const capturedQueries: Record = {} - - const mockNews: NewsResponse = { - lastBuildDate: '2024-01-01', - total: 1, - start: 1, - display: 5, - items: [ - { - title: 'Sample news', - link: 'https://example.com/news', - description: 'Example description', - pubDate: '2024-01-01', - }, - ], - } - - server.use( - http.get(`${TEST_BASE_URL}/api/v1/naver-news/search`, ({ request }) => { - const url = new URL(request.url) - url.searchParams.forEach((value, key) => { - capturedQueries[key] = value - }) - return HttpResponse.json(mockNews) - }) - ) - - const result = await BacktestService.searchNews('AAPL', 5) - - expect(result).toEqual(mockNews) - expect(capturedQueries).toEqual({ query: 'AAPL', display: '5' }) - }) }) diff --git a/backtest_fe/src/features/backtest/services/backtestService.ts b/backtest_fe/src/features/backtest/services/backtestService.ts index 2233ac8d..20a5d33b 100644 --- a/backtest_fe/src/features/backtest/services/backtestService.ts +++ b/backtest_fe/src/features/backtest/services/backtestService.ts @@ -1,16 +1,15 @@ /** - * 리팩터링된 백테스트 서비스 - * 타입 안전성과 에러 처리 개선 + * 백테스트 서비스 + * + * 백엔드는 통합 엔드포인트 POST /api/v1/backtest 하나만 제공한다. + * 주가/환율/뉴스/벤치마크 데이터는 모두 이 응답에 포함되어 오므로 + * 별도 조회 메서드를 두지 않는다. */ import { apiClient } from '@/shared/api/client'; import { BacktestRequest, UnifiedBacktestResponse, - Strategy, - NewsResponse, - ExchangeRateData, - VolatilityData, } from '../model/types/api-types'; export class BacktestService { @@ -21,58 +20,6 @@ export class BacktestService { const response = await apiClient.post('/api/v1/backtest', request); return response.data; } - - /** - * 이용 가능한 전략 목록 조회 - */ - static async getStrategies(): Promise { - const response = await apiClient.get('/api/v1/strategies'); - return response.data; - } - - /** - * 특정 전략 정보 조회 - */ - static async getStrategy(strategyName: string): Promise { - const response = await apiClient.get(`/api/v1/strategies/${strategyName}`); - return response.data; - } - - /** - * 뉴스 검색 - */ - static async searchNews(query: string, display = 10): Promise { - const response = await apiClient.get('/api/v1/naver-news/search', { - params: { query, display } - }); - return response.data; - } - - /** - * 환율 정보 조회 - */ - static async getExchangeRate(): Promise { - const response = await apiClient.get('/api/v1/yfinance/exchange-rate'); - return response.data; - } - - /** - * 변동성 데이터 조회 - */ - static async getVolatilityData(symbols: string[]): Promise { - const response = await apiClient.get('/api/v1/volatility', { - params: { symbols: symbols.join(',') } - }); - return response.data; - } - - /** - * 시스템 상태 조회 - */ - static async getSystemInfo() { - const response = await apiClient.get('/api/v1/system/info'); - return response.data; - } } -export default BacktestService; \ No newline at end of file +export default BacktestService; diff --git a/backtest_fe/src/shared/api/__tests__/client.test.ts b/backtest_fe/src/shared/api/__tests__/client.test.ts index e66be4e3..0dbbff94 100644 --- a/backtest_fe/src/shared/api/__tests__/client.test.ts +++ b/backtest_fe/src/shared/api/__tests__/client.test.ts @@ -1,7 +1,7 @@ /** * apiClient 경로 합성 회귀 테스트 * - * 서비스 레이어(backtestService.ts, backtestApi.ts)는 axios에 전체 경로 + * 서비스 레이어(backtestService.ts)는 axios에 전체 경로 * (`/api/v1/...`)를 그대로 넘긴다. 이때 VITE_API_BASE_URL이 `/api`로 설정되어 * 있으면 baseURL과 경로가 겹쳐 `/api/api/v1/backtest`가 만들어지고, * vite 프록시(`/api/v1/backtest`)에 매칭되지 않아 404가 난다. From 741732769c680ac035681c025c3a67da79b1a545 Mon Sep 17 00:00:00 2001 From: kyj0503 Date: Sat, 1 Aug 2026 04:12:02 +0900 Subject: [PATCH 8/8] =?UTF-8?q?docs(common):=20CLAUDE.md=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복되거나 오래된 항목 제거 (Stack, URLs, 네이티브 실행 커맨드, 하드코딩된 테스트 개수) - Sub-Agent 목록을 실제 사용 가능한 것으로 교체 (Explore / Plan / general-purpose) - DB 항목은 schema.sql 참조로 단순화 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6f294c50..e7fda29c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,32 +4,22 @@ **라고할때살걸** — Korean trading strategy backtesting platform (SMA, RSI, MACD, Bollinger, EMA, Buy&Hold). Supports portfolios, DCA, rebalancing. -**Stack:** Python 3.11 / FastAPI | TypeScript / React / Vite | MySQL 8.0 (prod) / SQLite (tests) | Docker Compose | Jenkins CI/CD - ## Commands ```bash -# BE tests (no DB) -pytest backtest_be_fast/tests/unit -v - -# FE -cd backtest_fe && npm run dev | npm run build | npm test - # Docker (full stack) docker compose -f compose.dev.yaml up -d --build docker compose -f compose.dev.yaml exec backtest-be-fast pytest tests/unit -v docker compose -f compose.dev.yaml exec backtest-fe npm test ``` -**URLs:** FE http://localhost:5173 | BE Docs http://localhost:8000/docs - ## Architecture **BE flow:** `FastAPI Endpoint → PortfolioManagerService → BacktestEngine / StrategyService / StockRepository → UnifiedDataService` **FE (Feature-Sliced Design):** `shared` ← `features` ← `pages` (no reverse imports). State: Zustand (global) + `useReducer` (forms). UI: shadcn/ui + Tailwind + Recharts. -**DB:** `stocks`, `daily_prices`, `stock_news` (see `database/schema.sql`) +**DB schema:** see `database/schema.sql` **API:** POST `/api/v1/backtest` — main endpoint. Errors: `@handle_portfolio_errors` decorator. @@ -49,13 +39,9 @@ docker compose -f compose.dev.yaml exec backtest-fe npm test ## Sub-Agent Usage -When working on this codebase, **actively use specialized sub-agents** for better quality: -- **`frontend-developer`** for React/TypeScript component changes -- **`python-pro` / `fastapi-pro`** for BE service/API changes -- **`test-automator`** for writing tests -- **`code-reviewer` / `architect-review`** for reviewing changes before finalizing -- **`debugger`** when encountering errors -- **`Explore`** agent for broad codebase research +- **`Explore`** for broad codebase research (where does X live, how is Y wired) +- **`Plan`** for designing multi-step changes before writing code +- **`general-purpose`** for multi-step research/edit tasks that need their own context Always verify changes in Docker containers (`docker compose exec`) before declaring work complete. @@ -63,7 +49,6 @@ Always verify changes in Docker containers (`docker compose exec`) before declar - **BE markers:** `@pytest.mark.unit` (no DB), `@pytest.mark.integration` (DB), `@pytest.mark.external` (real API) - **FE:** Vitest + React Testing Library; Playwright for E2E -- 141 BE unit tests, 94+ FE tests ## Commit Convention