From d443af1bb60bec16173f23d1252b3bbf6357e2f1 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 14:43:38 +0900 Subject: [PATCH 01/39] =?UTF-8?q?#24=20chore:=20Mui=20date=20pickers?= =?UTF-8?q?=EC=99=80=20dayjs=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 897 +++++++--------------------------------------- package.json | 2 + 2 files changed, 132 insertions(+), 767 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19f3dca..defcde5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ "@lukemorales/query-key-factory": "^1.3.4", "@mui/material": "^6.1.0", "@mui/material-nextjs": "^6.1.0", + "@mui/x-date-pickers": "^7.22.3", "@tanstack/react-query": "^5.55.4", "@tanstack/react-query-devtools": "^5.59.15", "chokidar": "^4.0.1", + "dayjs": "^1.11.13", "lightweight-charts": "^4.2.1", "next": "14.2.10", "react": "^18", @@ -27,26 +29,17 @@ "react-verification-input": "^4.1.2", "recharts": "^2.13.0", "styled-components": "^6.1.13", - "ts-node": "^10.9.2", "vite-node": "^2.1.3", "zustand": "^5.0.0" }, "devDependencies": { - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.8", - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.8", - "@types/babel__traverse": "^7.20.6", - "@types/chokidar": "^2.1.3", "@types/node": "^20.16.14", "@types/react": "^18", "@types/react-dom": "^18", "concurrently": "^9.0.1", "eslint": "^8", "eslint-config-next": "14.2.9", - "open": "^10.1.0", "prettier": "3.3.3", - "tsx": "^4.19.1", "typescript": "^5.6.3" } }, @@ -197,9 +190,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -258,26 +251,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -438,425 +411,41 @@ } }, "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", - "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/styled/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", + "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "dependencies": { + "@emotion/memoize": "^0.9.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node_modules/@emotion/styled/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "peerDependencies": { + "react": ">=16.8.0" } }, + "node_modules/@emotion/utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1295,6 +884,90 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/@mui/x-date-pickers": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.3.tgz", + "integrity": "sha512-shNp92IrST5BiVy2f4jbrmRaD32QhyUthjh1Oexvpcn0v6INyuWgxfodoTi5ZCnE5Ue5UVFSs4R9Xre0UbJ5DQ==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.21.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.21.0.tgz", + "integrity": "sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@next/env": { "version": "14.2.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.10.tgz", @@ -1774,45 +1447,6 @@ "react": "^18 || ^19" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/chokidar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-2.1.3.tgz", - "integrity": "sha512-6qK3xoLLAhQVTucQGHTySwOVA1crHRXnJeLwqK6KIFkkKa2aoMFXh+WEi8PotxDtvN6MQJLyYN9ag9P6NLV81w==", - "deprecated": "This is a stub types definition. chokidar provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "chokidar": "*" - } - }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -1888,6 +1522,7 @@ "version": "20.16.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.14.tgz", "integrity": "sha512-vtgGzjxLF7QT88qRHtXMzCWpAAmwonE7fwgVjFtXosUva2oSpnIEc3gNO9P7uIfOxKnii2f79/xtOnfreYtDaA==", + "devOptional": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2163,6 +1798,7 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2179,17 +1815,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2230,11 +1855,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2495,21 +2115,6 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2766,11 +2371,6 @@ "node": ">=10" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2975,6 +2575,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3034,34 +2639,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3079,18 +2656,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -3108,14 +2673,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3395,45 +2952,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4603,21 +4121,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4675,24 +4178,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4878,21 +4363,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5110,11 +4580,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5387,24 +4852,6 @@ "wrappy": "1" } }, - "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5974,18 +5421,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6615,48 +6050,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6674,25 +6067,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, - "node_modules/tsx": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", - "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", - "dev": true, - "dependencies": { - "esbuild": "~0.23.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6794,6 +6168,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6820,7 +6195,8 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "devOptional": true }, "node_modules/uri-js": { "version": "4.4.1", @@ -6836,11 +6212,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" - }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -7632,14 +7003,6 @@ "node": ">=8" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 7d4fd4f..3eec757 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,11 @@ "@lukemorales/query-key-factory": "^1.3.4", "@mui/material": "^6.1.0", "@mui/material-nextjs": "^6.1.0", + "@mui/x-date-pickers": "^7.22.3", "@tanstack/react-query": "^5.55.4", "@tanstack/react-query-devtools": "^5.59.15", "chokidar": "^4.0.1", + "dayjs": "^1.11.13", "lightweight-charts": "^4.2.1", "next": "14.2.10", "react": "^18", From 8e555ba9e4c0c9051bb24cd6ef31cf8dde0db46c Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 14:43:58 +0900 Subject: [PATCH 02/39] =?UTF-8?q?#24=20refactor:=20Fetcher=EC=97=90=20Dele?= =?UTF-8?q?te=20=EC=98=B5=EC=85=98=20data=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/fetcher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index a7468b0..b429ba0 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -132,6 +132,7 @@ const createFetcher = ( }), delete: ( url: string, + data?: Record | FormData, options?: FetcherOptions ): Promise> => requestWithoutData(`${baseURL}${url}`, "DELETE", { From 03cfe1533ea409e94cb69c003880488141c8a568 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:04:24 +0900 Subject: [PATCH 03/39] =?UTF-8?q?#24=20refactor:=20Background=20color=20?= =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC,=20=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=20=EA=B5=AC=EB=B6=84=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BasePage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/BasePage.tsx b/src/components/BasePage.tsx index 1d6506b..5ba4ec4 100644 --- a/src/components/BasePage.tsx +++ b/src/components/BasePage.tsx @@ -43,7 +43,10 @@ const StyledBasePage = styled.div<{ $isMobile: boolean; $isIOSPWA: boolean }>` ? "64px" : "0"}; - background-color: ${designSystem.color.neutral.white}; + background-color: ${({ $isMobile }) => + $isMobile + ? designSystem.color.neutral.white + : designSystem.color.neutral.gray50}; `; const Main = styled.main<{ $isMobile: boolean }>` From 9070e05b5de6dc781100ab6c185d578339bfb97c Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:04:41 +0900 Subject: [PATCH 04/39] =?UTF-8?q?#24=20feat:=20Breadcrumb=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Breadcrumb.tsx | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/components/Breadcrumb.tsx diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx new file mode 100644 index 0000000..b2acafa --- /dev/null +++ b/src/components/Breadcrumb.tsx @@ -0,0 +1,67 @@ +import designSystem from "@/styles/designSystem"; +import { useRouter } from "next/router"; +import styled from "styled-components"; +import { Icon } from "./Icon"; + +type Props = { + depthData: { + name: string; + url: string; + }[]; +}; + +export default function Breadcrumb({ depthData }: Props) { + const router = useRouter(); + + const lastIndex = depthData.length - 1; + + return ( + + {depthData.map((data, index) => { + const { name, url } = data; + return ( +
+ router.push(url)}> + {name} + + {index !== depthData.length - 1 && ( + + )} +
+ ); + })} +
+ ); +} + +const StyledBreadcrumb = styled.div` + display: flex; + + gap: 2.5px; + ${designSystem.font.title5}; + + > div { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 2.5px; + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray400}; + } +`; + +const DepthTitle = styled.span<{ $isLast: boolean }>` + text-decoration: ${({ $isLast }) => ($isLast ? "none" : "underline")}; + text-decoration-color: ${designSystem.color.neutral.gray600}; + color: ${({ $isLast }) => + $isLast + ? designSystem.color.neutral.gray800 + : designSystem.color.neutral.gray600}; + + &:hover { + color: ${designSystem.color.neutral.gray800}; + cursor: pointer; + } +`; From 4a7391e75283707d9a5974913e092101f1d6bb06 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:20:21 +0900 Subject: [PATCH 05/39] =?UTF-8?q?#24=20feat:=20DatePicker,=20AsyncButton?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Buttons/AsyncButton.tsx | 48 +++++++++++++++ src/components/DatePicker.tsx | 81 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/components/Buttons/AsyncButton.tsx create mode 100644 src/components/DatePicker.tsx diff --git a/src/components/Buttons/AsyncButton.tsx b/src/components/Buttons/AsyncButton.tsx new file mode 100644 index 0000000..c802b11 --- /dev/null +++ b/src/components/Buttons/AsyncButton.tsx @@ -0,0 +1,48 @@ +import designSystem from "@/styles/designSystem"; +import Spinner from "../Spinner"; +import Button, { ButtonProps } from "./Button"; + +type AsyncButtonProps = { + isPending: boolean; +} & ButtonProps; + +export default function AsyncButton({ + isPending, + children, + ...props +}: AsyncButtonProps) { + return ( + + ); +} + +const calcSpinnerSize = (size: ButtonProps["size"]) => { + switch (size) { + case "h24": + return 12; + case "h32": + return 15; + case "h44": + return 20; + } +}; + +const determineSpinnerColor = (variant: ButtonProps["variant"]) => { + switch (variant) { + case "primary": + return designSystem.color.neutral.white; + case "secondary": + return designSystem.color.primary.blue200; + case "tertiary": + return designSystem.color.neutral.gray400; + } +}; diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx new file mode 100644 index 0000000..149e2f8 --- /dev/null +++ b/src/components/DatePicker.tsx @@ -0,0 +1,81 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem, { parseFontString } from "@/styles/designSystem"; +import { ThemeProvider, createTheme } from "@mui/material"; +import { DesktopDatePicker } from "@mui/x-date-pickers"; +import { Dayjs } from "dayjs"; +import { Icon } from "./Icon"; + +type SizeType = "small" | "big"; + +type Props = { + size: SizeType; + disabled?: boolean; + value: Dayjs | null; + onChange: (newVal: Dayjs | null) => void; +}; + +export default function DatePicker({ + size, + disabled = false, + value, + onChange, +}: Props) { + const { isMobile } = useResponsiveLayout(); + + return ( + + { + return ; + }, + }} + /> + + ); +} + +const datePickerTheme = (size: SizeType, isMobile: boolean) => { + const WIDTH_SIZE = isMobile ? "100%" : size === "small" ? "127px" : "352px"; + const HEIGHT_SIZE = isMobile + ? size === "small" + ? "32px" + : "48px" + : size === "small" + ? "24px" + : "32px"; + + return createTheme({ + components: { + MuiInputBase: { + styleOverrides: { + root: { + "width": WIDTH_SIZE, + "height": HEIGHT_SIZE, + "padding": "0 0 0 8px", + ...parseFontString(designSystem.font.body3), + "&:hover, &:focus-within": { + fieldset: { + border: `1px solid ${designSystem.color.primary.blue500} !important`, + }, + }, + "fieldset": { + border: `1px solid ${designSystem.color.neutral.gray200}`, + }, + }, + input: { + padding: "0 !important", + }, + }, + }, + }, + }); +}; From e987e322b25059f17536b0b58a2a596678fb6c6a Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:20:58 +0900 Subject: [PATCH 06/39] =?UTF-8?q?#24=20feat:=20Pagination=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pagination/LabelRowsPerPage.tsx | 42 ++++++ src/components/Pagination/Pagination.tsx | 81 +++++++++++ .../Pagination/PaginationControl.tsx | 52 ++++++++ .../Pagination/PaginationSelect.tsx | 30 +++++ src/components/Pagination/TablePagination.tsx | 126 ++++++++++++++++++ .../utils/calculateStartAndEndRows.ts | 16 +++ 6 files changed, 347 insertions(+) create mode 100644 src/components/Pagination/LabelRowsPerPage.tsx create mode 100644 src/components/Pagination/Pagination.tsx create mode 100644 src/components/Pagination/PaginationControl.tsx create mode 100644 src/components/Pagination/PaginationSelect.tsx create mode 100644 src/components/Pagination/TablePagination.tsx create mode 100644 src/components/Pagination/utils/calculateStartAndEndRows.ts diff --git a/src/components/Pagination/LabelRowsPerPage.tsx b/src/components/Pagination/LabelRowsPerPage.tsx new file mode 100644 index 0000000..fb384ad --- /dev/null +++ b/src/components/Pagination/LabelRowsPerPage.tsx @@ -0,0 +1,42 @@ +import { Icon } from "@/components/Icon"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; + +type Props = { + count: number; + startRow: number | undefined; + endRow: number | undefined; +}; + +export function LabelRowsPerPage({ count, endRow, startRow }: Props) { + return ( + + + 전체 {count} 중{" "} + + {startRow && endRow + ? endRow === 1 + ? 1 + : `${startRow}-${endRow}` + : count} + + + + + ); +} + +const LabelRowsPerPageWrapper = styled.span` + display: flex; + align-items: center; +`; + +const StyledLabelRowsPerPage = styled.span` + margin-right: 8px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray600}; + + > span { + color: ${designSystem.color.neutral.gray900}; + } +`; diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx new file mode 100644 index 0000000..3c24789 --- /dev/null +++ b/src/components/Pagination/Pagination.tsx @@ -0,0 +1,81 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import { + Pagination as MuiPagination, + PaginationItem, + paginationClasses, +} from "@mui/material"; +import { ChangeEvent } from "react"; +import styled from "styled-components"; +import { Icon } from "../Icon"; + +type Props = { + count: number; + page: number; + onPageChange: (event: ChangeEvent, page: number) => void; +}; + +export default function Pagination({ count, page, onPageChange }: Props) { + const { isMobile } = useResponsiveLayout(); + + return ( + , newPage: number) => { + if (page === newPage) return; + + if (isMobile) { + window.scroll({ top: 0 }); + } + onPageChange(event, newPage - 1); + }} + shape="rounded" + renderItem={(item) => ( + ( + + ), + next: () => , + }} + {...item} + /> + )} + /> + ); +} + +const StyledPagination = styled(MuiPagination)<{ $isMobile: boolean }>` + display: flex; + justify-content: center; + height: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + + .${paginationClasses.ul} { + gap: 8px; + + > li { + width: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + height: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + + > button { + width: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + min-width: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + height: ${({ $isMobile }) => ($isMobile ? "32px" : "24px")}; + margin: 0; + padding: 0; + ${designSystem.font.body3}; + + &.Mui-selected { + background-color: ${designSystem.color.primary.blue50}; + color: ${designSystem.color.primary.blue500}; + } + + &:hover { + background-color: ${designSystem.color.neutral.gray50}; + } + } + } + } +`; diff --git a/src/components/Pagination/PaginationControl.tsx b/src/components/Pagination/PaginationControl.tsx new file mode 100644 index 0000000..abf10d8 --- /dev/null +++ b/src/components/Pagination/PaginationControl.tsx @@ -0,0 +1,52 @@ +import { LabelRowsPerPage } from "@/components/Pagination/LabelRowsPerPage"; +import { PaginationSelect } from "@/components/Pagination/PaginationSelect"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; + +type Props = { + count: number; + startRow: number | undefined; + endRow: number | undefined; + rowsPerPage: number; + rowsPerPageOptions: number[]; + onRowsPerPageChange: (value: string) => void; +}; + +export function PaginationControl({ + count, + startRow, + endRow, + rowsPerPage, + rowsPerPageOptions, + onRowsPerPageChange, +}: Props) { + return ( + + + + + + {rowsPerPage !== -1 && ( + 개 씩 보기 + )} + + ); +} + +const StyledPaginationControl = styled.div` + display: flex; + align-items: center; +`; + +const LabelDisplayedRows = styled.span` + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray600}; +`; + +const PaginationSelectWrapper = styled.div` + width: 66px; +`; diff --git a/src/components/Pagination/PaginationSelect.tsx b/src/components/Pagination/PaginationSelect.tsx new file mode 100644 index 0000000..771a2fa --- /dev/null +++ b/src/components/Pagination/PaginationSelect.tsx @@ -0,0 +1,30 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import { Select, SelectOption } from "../Select"; + +type Props = { + rowsPerPage: number; + rowsPerPageOptions: number[]; + onRowsPerPageChange: (value: string) => void; +}; + +export function PaginationSelect({ + rowsPerPage, + rowsPerPageOptions, + onRowsPerPageChange, +}: Props) { + const { isMobile } = useResponsiveLayout(); + + return ( + + ); +} diff --git a/src/components/Pagination/TablePagination.tsx b/src/components/Pagination/TablePagination.tsx new file mode 100644 index 0000000..e700bf1 --- /dev/null +++ b/src/components/Pagination/TablePagination.tsx @@ -0,0 +1,126 @@ +import designSystem from "@/styles/designSystem"; +import { + TablePagination as MUITablePagination, + styled, + tablePaginationClasses, +} from "@mui/material"; +import { memo } from "react"; +import { LabelRowsPerPage } from "./LabelRowsPerPage"; +import Pagination from "./Pagination"; +import { PaginationSelect } from "./PaginationSelect"; +import calculateStartAndEndRows from "./utils/calculateStartAndEndRows"; + +type Props = { + count: number; + page: number; + rowsPerPage: number; + rowsPerPageOptions: number[]; + onPageChange: (event: unknown, newPage: number) => void; + onRowsPerPageChange: (value: string) => void; +}; + +export default memo(function TablePagination({ + count, + page, + rowsPerPage, + rowsPerPageOptions, + onPageChange, + onRowsPerPageChange, +}: Props) { + const { startRow, endRow } = calculateStartAndEndRows( + count, + page + 1, + rowsPerPage + ); + + return ( + <> + + } + labelDisplayedRows={() => (rowsPerPage === -1 ? "" : "개 씩 보기")} + slotProps={{ + select: { + input: ( + + ), + }, + }} + ActionsComponent={() => ( + + )} + onPageChange={onPageChange} + /> + + ); +}); + +const StyledTablePagination = styled(MUITablePagination)` + margin-top: 16px; + height: 24px; + + & .${tablePaginationClasses.spacer} { + display: none; + } + + & .${tablePaginationClasses.toolbar} { + height: 100%; + min-height: auto; + padding: 0; + display: flex; + background-color: #fff; + } + + & .${tablePaginationClasses.input} { + width: 49px; + height: 24px; + min-height: 24px; + margin: 0; + } + + & .${tablePaginationClasses.selectLabel} { + margin-right: 8px; + } + + & .${tablePaginationClasses.select} { + width: 100%; + height: inherit; + margin: 0; + box-sizing: border-box; + display: flex; + align-items: center; + background-color: transparent; + border: 1px solid ${designSystem.color.neutral.gray200}; + border-radius: 2px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + + &:hover { + border: 1px solid ${designSystem.color.primary.blue500}; + } + + &:active { + border: 1px solid ${designSystem.color.primary.blue500}; + } + } + + & .${tablePaginationClasses.displayedRows} { + margin: 0 auto 0 8px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray600}; + } +`; diff --git a/src/components/Pagination/utils/calculateStartAndEndRows.ts b/src/components/Pagination/utils/calculateStartAndEndRows.ts new file mode 100644 index 0000000..b55c3ff --- /dev/null +++ b/src/components/Pagination/utils/calculateStartAndEndRows.ts @@ -0,0 +1,16 @@ +export default function calculateStartAndEndRows( + totalNumRows: number, + currentPage: number, + rowsPerPage: number +) { + if (currentPage < 1 || rowsPerPage < 1 || totalNumRows < 0) return {}; + + const startRow = (currentPage - 1) * rowsPerPage + 1; + const endRow = Math.min(currentPage * rowsPerPage, totalNumRows); + + // If the page number exceeds the available rows. + // Ex: totalNumRows = 100, currentPage = 11, rowsPerPage = 10 + if (startRow > totalNumRows) return {}; + + return { startRow, endRow }; +} From 1319975ff6860b35968361153ec1254c3f2d3b1f Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:21:38 +0900 Subject: [PATCH 07/39] =?UTF-8?q?#24=20refactor:=20PortfoliosDropdown=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortfoliosDropdown/PortfoliosDropdown.tsx | 25 +++++++++------- .../PortfoliosDropdownList.tsx | 30 +++++++++---------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx b/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx index fb327cb..ce5a0e0 100644 --- a/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx +++ b/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx @@ -1,16 +1,22 @@ import Routes from "@/constants/Routes"; +import useUserQuery from "@/features/user/api/queries/useUserQuery"; import { useDropdown } from "@/hooks/useDropdown"; import designSystem, { parseFontString } from "@/styles/designSystem"; import { useBoolean } from "@fineants/demolition"; import Link from "next/link"; +import { useRouter } from "next/router"; import { MouseEvent } from "react"; import styled from "styled-components"; +import { AsyncBoundary } from "../AsyncBoundary"; import { Icon } from "../Icon"; +import PortfoliosDropdownList from "./PortfoliosDropdownList"; +import PortfoliosDropdownListErrorFallback from "./errorFallback/PortfoliosDropdownListErrorFallback"; +import PortfoliosDropdownListSkeleton from "./skeletons/PortfoliosDropdownListSkeleton"; export function PortfoliosDropdown() { - // const router = useRouter(); + const router = useRouter(); - // const { user } = useContext(UserContext); + const { data: user } = useUserQuery(); const { isOpen, onOpen, DropdownMenu, DropdownItem } = useDropdown(); @@ -21,10 +27,10 @@ export function PortfoliosDropdown() { }; const onPortfolioAddClick = () => { - // if (!user) { - // router.push(Routes.SIGNIN); - // return; - // } + if (!user) { + router.push(Routes.SIGNIN); + return; + } portfolioDialogOpen(); }; @@ -40,15 +46,14 @@ export function PortfoliosDropdown() { /> - {/* {user && ( + {user && ( } ErrorFallback={PortfoliosDropdownListErrorFallback}> - )} */} - {/* */} - + )} + 포트폴리오로 이동 diff --git a/src/components/PortfoliosDropdown/PortfoliosDropdownList.tsx b/src/components/PortfoliosDropdown/PortfoliosDropdownList.tsx index 8c6146a..d05f1d8 100644 --- a/src/components/PortfoliosDropdown/PortfoliosDropdownList.tsx +++ b/src/components/PortfoliosDropdown/PortfoliosDropdownList.tsx @@ -1,4 +1,9 @@ +import Routes from "@/constants/Routes"; +import usePortfolioNameListQuery from "@/features/portfolio/api/queries/usePortfolioNameListQuery"; import { DropdownItemProps } from "@/hooks/useDropdown"; +import designSystem, { parseFontString } from "@/styles/designSystem"; +import { Divider } from "@mui/material"; +import Link from "next/link"; import { ComponentType } from "react"; @@ -6,30 +11,23 @@ type Props = { DropdownItem: ComponentType; }; -export default function PortfoliosDropdownList({}: Props) { - // const { data: portfolioList } = usePortfolioListQuery(); - - // const portfolioDropdownItems = portfolioList.map( - // (portfolio: PortfolioItem) => ({ - // name: portfolio.name, - // path: Routes.PORTFOLIO(portfolio.id), - // }) - // ); +export default function PortfoliosDropdownList({ DropdownItem }: Props) { + const { data: portfolioList } = usePortfolioNameListQuery(); return ( <> - {/* {portfolioDropdownItems?.map((item) => ( - + {portfolioList?.map((item) => ( + {item.name} ))} - {portfolioDropdownItems && } */} + {portfolioList && } ); } -// const portfolioDropdownItemSx = { -// font: designSystem.font.body2, -// color: designSystem.color.neutral.gray900, -// }; +const portfolioDropdownItemSx = { + ...parseFontString(designSystem.font.body2), + color: designSystem.color.neutral.gray900, +}; From 316c1e6fa52b004e4517b59c2d14e8ca47455cbb Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:21:56 +0900 Subject: [PATCH 08/39] =?UTF-8?q?#24=20feat:=20Select=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Select/Select.tsx | 116 +++++++++++++++++++++++++ src/components/Select/SelectOption.tsx | 21 +++++ src/components/Select/index.ts | 2 + 3 files changed, 139 insertions(+) create mode 100644 src/components/Select/Select.tsx create mode 100644 src/components/Select/SelectOption.tsx create mode 100644 src/components/Select/index.ts diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx new file mode 100644 index 0000000..b19dd57 --- /dev/null +++ b/src/components/Select/Select.tsx @@ -0,0 +1,116 @@ +import designSystem from "@/styles/designSystem"; +import { useBoolean } from "@fineants/demolition"; +import { + InputBase, + Select as MuiSelect, + SelectChangeEvent, +} from "@mui/material"; +import { ReactNode } from "react"; +import styled from "styled-components"; +import { Icon } from "../Icon"; + +export type Size = "h24" | "h32" | "h40" | "h48"; + +type Props = { + size: Size; + menuMinHeight?: string; + menuMaxHeight?: string; + selectedValue: string; + changeSelectedValue: (value: string) => void; + children: ReactNode; +}; + +/** + * @param items - A list of items to display in the select menu. Use "-1" for "All". + */ +export default function Select({ + size, + menuMinHeight, + menuMaxHeight, + selectedValue, + changeSelectedValue, + children, +}: Props) { + const { state: isOpen, setTrue: onOpen, setFalse: onClose } = useBoolean(); + + const onChange = (event: SelectChangeEvent) => { + changeSelectedValue(event.target.value as string); + }; + + return ( + } + inputProps={{ "aria-label": "포트폴리오 종목 테이블 행 개수 선택" }} + SelectDisplayProps={{ + style: { + minWidth: "66px", + paddingRight: "40px", + }, + }} + IconComponent={(props) => ( + + + + )} + MenuProps={{ sx: MenuSX(size, menuMinHeight, menuMaxHeight) }}> + {children} + + ); +} + +const BootstrapInput = styled(InputBase)<{ $size: Size; $isOpen: boolean }>` + & .MuiInputBase-input { + min-width: ${({ $size }) => ($size === "h24" ? 56 : 80)}px; + height: ${({ $size }) => $size.slice(1)}px; + padding: 0 28px 0 8px; + display: flex; + align-items: center; + gap: 4px; + box-sizing: border-box; + background-color: ${designSystem.color.neutral.white}; + border: 1px solid + ${({ $isOpen }) => + $isOpen + ? designSystem.color.primary.blue500 + : designSystem.color.neutral.gray200}; + border-radius: ${({ $size }) => ($size === "h24" ? 2 : 3)}px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + + &:hover { + border-color: ${designSystem.color.primary.blue500}; + } + } +`; + +const IconWrapper = styled.div` + width: 32px; + display: flex; + justify-content: center; + align-items: center; + top: auto; +`; + +const MenuSX = ( + size: Size, + menuMinHeight?: string, + menuMaxHeight?: string +) => ({ + "& .MuiPaper-root": { + "minHeight": menuMinHeight ? menuMinHeight : "160px", + "maxHeight": menuMaxHeight ? menuMaxHeight : "240px", + "minWidth": `${size === "h24" ? 56 : 80}px`, + + "marginTop": "2px", + "padding": "4px", + + ".MuiMenu-list": { + padding: "0", + }, + }, +}); diff --git a/src/components/Select/SelectOption.tsx b/src/components/Select/SelectOption.tsx new file mode 100644 index 0000000..7f9ef1b --- /dev/null +++ b/src/components/Select/SelectOption.tsx @@ -0,0 +1,21 @@ +import designSystem from "@/styles/designSystem"; +import { MenuItem as MuiMenuItem } from "@mui/material"; +import styled from "styled-components"; + +export default styled(MuiMenuItem)` + height: inherit; + padding: 0 4px; + gap: 4px; + background-color: ${designSystem.color.neutral.white}; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + + &:hover { + background-color: ${designSystem.color.neutral.gray50}; + } + + &.Mui-selected, + &.Mui-selected:hover { + background-color: ${designSystem.color.neutral.gray50} !important; + } +`; diff --git a/src/components/Select/index.ts b/src/components/Select/index.ts new file mode 100644 index 0000000..43a96dd --- /dev/null +++ b/src/components/Select/index.ts @@ -0,0 +1,2 @@ +export { default as Select } from "./Select"; +export { default as SelectOption } from "./SelectOption"; From efd2a230a48d56fb910370398e61dbadba63e9e8 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:22:16 +0900 Subject: [PATCH 09/39] =?UTF-8?q?#24=20feat:=20Table=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Table/CollapsibleSelectableTable.tsx | 196 ++++++++++++++++++ src/components/Table/CollapsibleTable.tsx | 123 +++++++++++ src/components/Table/PlainTable.tsx | 116 +++++++++++ src/components/Table/SelectableTable.tsx | 179 ++++++++++++++++ src/components/Table/TableSkeleton.tsx | 97 +++++++++ .../Table/hooks/useTableSelection.ts | 38 ++++ src/components/Table/types.ts | 1 + src/components/Table/utils/comparator.ts | 47 +++++ 8 files changed, 797 insertions(+) create mode 100644 src/components/Table/CollapsibleSelectableTable.tsx create mode 100644 src/components/Table/CollapsibleTable.tsx create mode 100644 src/components/Table/PlainTable.tsx create mode 100644 src/components/Table/SelectableTable.tsx create mode 100644 src/components/Table/TableSkeleton.tsx create mode 100644 src/components/Table/hooks/useTableSelection.ts create mode 100644 src/components/Table/types.ts create mode 100644 src/components/Table/utils/comparator.ts diff --git a/src/components/Table/CollapsibleSelectableTable.tsx b/src/components/Table/CollapsibleSelectableTable.tsx new file mode 100644 index 0000000..a0165d1 --- /dev/null +++ b/src/components/Table/CollapsibleSelectableTable.tsx @@ -0,0 +1,196 @@ +import { useBoolean } from "@fineants/demolition"; +import { + Box as MuiBox, + Table as MuiTable, + TableContainer as MuiTableContainer, +} from "@mui/material"; +import { + ChangeEvent, + ComponentType, + MouseEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import TablePagination from "../Pagination/TablePagination"; +import { Order } from "./types"; +import { getComparator } from "./utils/comparator"; + +type Props = { + tableTitle: string; + initialOrderBy: keyof Item; + rowsPerPageOptions?: number[]; + data: Item[]; + TableToolBar: ComponentType<{ + selected: readonly Item[]; + updateSelected: (newSelected: readonly Item[]) => void; + isAllDeleteOnLastPage: boolean; + moveToPrevTablePage: () => void; + }>; + TableHead: ComponentType<{ + order: Order; + orderBy: keyof Item; + isAllRowsSelectedInCurrentPage: boolean; + onSelectAllClick: (event: ChangeEvent) => void; + onRequestSort: (event: MouseEvent, property: keyof Item) => void; + isAllRowsOpen: boolean; + onExpandOrCollapseAllRows: (event: MouseEvent) => void; + }>; + TableBody: ComponentType<{ + numEmptyRows: number; + visibleRows: readonly Item[]; + selected: readonly Item[]; + updateSelected: (newSelected: readonly Item[]) => void; + isAllRowsOpen: boolean; + }>; + EmptyTable?: ComponentType; + enableTablePagination?: boolean; +}; + +const defaultRowsPerPageOptions = [5, 10, 15, 20, -1]; + +export default function CollapsibleSelectableTable< + Item extends { id: string | number }, +>({ + tableTitle, + initialOrderBy, + rowsPerPageOptions = defaultRowsPerPageOptions, + data: tableRows, + TableToolBar, + TableHead, + TableBody, + EmptyTable = () => <>, + enableTablePagination = true, +}: Props) { + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState(initialOrderBy); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); + const { state: isAllRowsOpen, setOpposite: handleExpandOrCollapseAllRows } = + useBoolean(); + + const handleRequestSort = useCallback( + (_: MouseEvent, property: keyof Item) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }, + [] + ); + + const handleChangePage = useCallback((_: unknown, newPage: number) => { + setPage(newPage); + }, []); + + const moveToPrevTablePage = useCallback(() => { + setPage((prev) => prev - 1); + }, []); + + const updateSelected = useCallback((newSelected: readonly Item[]) => { + setSelected(newSelected); + }, []); + + const numEmptyRows = enableTablePagination + ? Math.max(0, (1 + page) * Math.min(5, rowsPerPage) - tableRows.length) + : 0; + + const visibleRows = useMemo( + () => + rowsPerPage > 0 || rowsPerPage === -1 + ? tableRows + .sort(getComparator(order, orderBy)) + .slice( + page * rowsPerPage, + page * rowsPerPage + + (rowsPerPage === -1 ? tableRows.length : rowsPerPage) + ) + : tableRows, + [order, orderBy, page, rowsPerPage, tableRows] + ); + + // TODO : 다른 Table component도 고려 + const visibleRowsRef = useRef(visibleRows); + + useEffect(() => { + visibleRowsRef.current = visibleRows; + }, [visibleRows]); + + const handleSelectAllClick = useCallback( + (event: ChangeEvent) => { + if (event.target.checked) { + setSelected(visibleRowsRef.current); + } else { + setSelected([]); + } + }, + [] + ); + + const handleChangeRowsPerPage = useCallback((newValue: string) => { + setRowsPerPage(parseInt(newValue, 10)); + setPage(0); + }, []); + + const selectedSet = new Set(selected.map((item) => item.id)); + const isAllRowsSelectedInCurrentPage = + selected.length > 0 && + visibleRows.every((visibleRow) => selectedSet.has(visibleRow.id)); + const isAllDeleteOnLastPage = + page >= Math.ceil(tableRows.length / rowsPerPage) - 1 && + isAllRowsSelectedInCurrentPage; + + return ( + + {tableRows.length > 0 ? ( + <> + {TableToolBar && ( + + )} + + + + + + + + + + + ) : ( + + )} + + ); +} diff --git a/src/components/Table/CollapsibleTable.tsx b/src/components/Table/CollapsibleTable.tsx new file mode 100644 index 0000000..fd3dca1 --- /dev/null +++ b/src/components/Table/CollapsibleTable.tsx @@ -0,0 +1,123 @@ +import { useBoolean } from "@fineants/demolition"; +import { + Box as MuiBox, + Table as MuiTable, + TableContainer as MuiTableContainer, +} from "@mui/material"; +import { ComponentType, MouseEvent, useMemo, useState } from "react"; +import TablePagination from "../Pagination/TablePagination"; +import { Order } from "./types"; +import { getComparator } from "./utils/comparator"; + +const defaultRowsPerPageOptions = [5, 10, 15, 20, -1]; + +type Props = { + tableTitle: string; + initialOrderBy: keyof Item; + rowsPerPageOptions?: number[]; + data: Item[]; + TableHead: ComponentType<{ + order: Order; + orderBy: keyof Item; + onRequestSort: (event: MouseEvent, property: keyof Item) => void; + isAllRowsOpen: boolean; + onExpandOrCollapseAllRows: (event: MouseEvent) => void; + }>; + TableBody: ComponentType<{ + numEmptyRows: number; + visibleRows: readonly Item[]; + isAllRowsOpen: boolean; + }>; + EmptyTable?: ComponentType; + enableTablePagination?: boolean; +}; + +export default function CollapsibleTable({ + tableTitle, + initialOrderBy, + rowsPerPageOptions = defaultRowsPerPageOptions, + data: tableRows, + TableHead, + TableBody, + EmptyTable = () => <>, + enableTablePagination = true, +}: Props) { + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState(initialOrderBy); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); + const { state: isAllRowsOpen, setOpposite: handleExpandOrCollapseAllRows } = + useBoolean(); + + const handleRequestSort = (_: MouseEvent, property: keyof Item) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleChangePage = (_: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (newValue: string) => { + setRowsPerPage(parseInt(newValue, 10)); + setPage(0); + }; + + const numEmptyRows = enableTablePagination + ? Math.max(0, (1 + page) * Math.min(5, rowsPerPage) - tableRows.length) + : 0; + + const visibleRows = useMemo( + () => + rowsPerPage > 0 || rowsPerPage === -1 + ? tableRows + .sort(getComparator(order, orderBy)) + .slice( + page * rowsPerPage, + page * rowsPerPage + + (rowsPerPage === -1 ? tableRows.length : rowsPerPage) + ) + : tableRows, + [order, orderBy, page, rowsPerPage, tableRows] + ); + + return ( + + {tableRows.length > 0 ? ( + <> + + + + + + + + + + ) : ( + + )} + + ); +} diff --git a/src/components/Table/PlainTable.tsx b/src/components/Table/PlainTable.tsx new file mode 100644 index 0000000..34aa5e6 --- /dev/null +++ b/src/components/Table/PlainTable.tsx @@ -0,0 +1,116 @@ +import { + Box as MuiBox, + Table as MuiTable, + TableContainer as MuiTableContainer, +} from "@mui/material"; +import { ComponentType, MouseEvent, useMemo, useState } from "react"; +import TablePagination from "../Pagination/TablePagination"; +import { Order } from "./types"; +import { getComparator } from "./utils/comparator"; + +const defaultRowsPerPageOptions = [5, 10, 15, 20, -1]; + +type Props = { + tableTitle: string; + initialOrderBy: keyof Item; + rowsPerPageOptions?: number[]; + data: Item[]; + TableHead: ComponentType<{ + order: Order; + orderBy: keyof Item; + onRequestSort: (event: MouseEvent, property: keyof Item) => void; + }>; + TableBody: ComponentType<{ + numEmptyRows: number; + visibleRows: readonly Item[]; + }>; + EmptyTable?: ComponentType; + enableTablePagination?: boolean; +}; + +export default function PlainTable({ + tableTitle, + initialOrderBy, + rowsPerPageOptions = defaultRowsPerPageOptions, + data: tableRows, + TableHead, + TableBody, + EmptyTable = () => <>, + enableTablePagination = true, +}: Props) { + const [order, setOrder] = useState("desc"); + const [orderBy, setOrderBy] = useState(initialOrderBy); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); + + const handleRequestSort = (_: MouseEvent, property: keyof Item) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleChangePage = (_: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (newValue: string) => { + setRowsPerPage(parseInt(newValue, 10)); + setPage(0); + }; + + const numEmptyRows = enableTablePagination + ? Math.max(0, (1 + page) * Math.min(5, rowsPerPage) - tableRows.length) + : 0; + + const visibleRows = useMemo( + () => + rowsPerPage > 0 || rowsPerPage === -1 + ? tableRows + .sort(getComparator(order, orderBy)) + .slice( + page * rowsPerPage, + page * rowsPerPage + + (rowsPerPage === -1 ? tableRows.length : rowsPerPage) + ) + : tableRows, + [order, orderBy, page, rowsPerPage, tableRows] + ); + + return ( + + {tableRows.length > 0 ? ( + <> + + + + + + + + {enableTablePagination && ( + + )} + + ) : ( + + )} + + ); +} diff --git a/src/components/Table/SelectableTable.tsx b/src/components/Table/SelectableTable.tsx new file mode 100644 index 0000000..7f68c92 --- /dev/null +++ b/src/components/Table/SelectableTable.tsx @@ -0,0 +1,179 @@ +import { + Box as MuiBox, + Table as MuiTable, + TableContainer as MuiTableContainer, +} from "@mui/material"; +import { + ChangeEvent, + ComponentType, + MouseEvent, + useCallback, + useMemo, + useState, +} from "react"; +import { Order } from "./types"; +import { getComparator } from "./utils/comparator"; + +type Props = { + tableTitle: string; + initialOrderBy: keyof Item; + rowsPerPageOptions?: number[]; + data: Item[]; + TableToolBar: ComponentType<{ + selected: readonly Item[]; + updateSelected: (newSelected: readonly Item[]) => void; + isAllDeleteOnLastPage: boolean; + moveToPrevTablePage: () => void; + }>; + TableHead: ComponentType<{ + order: Order; + orderBy: keyof Item; + isAllRowsSelectedInCurrentPage: boolean; + onSelectAllClick: (event: ChangeEvent) => void; + onRequestSort: (event: MouseEvent, property: keyof Item) => void; + }>; + TableBody: ComponentType<{ + numEmptyRows: number; + visibleRows: readonly Item[]; + selected: readonly Item[]; + updateSelected: (newSelected: readonly Item[]) => void; + }>; + EmptyTable?: ComponentType; +}; + +const defaultRowsPerPageOptions = [5, 10, 15, 20, -1]; + +export default function SelectableTable({ + tableTitle, + initialOrderBy, + rowsPerPageOptions = defaultRowsPerPageOptions, + data: tableRows, + TableToolBar, + TableHead, + TableBody, + EmptyTable = () => <>, +}: Props) { + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState(initialOrderBy); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); + + const handleRequestSort = (_: MouseEvent, property: keyof Item) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleChangePage = (_: unknown, newPage: number) => { + setPage(newPage); + }; + + const moveToPrevTablePage = () => { + setPage((prev) => prev - 1); + }; + + const updateSelected = (newSelected: readonly Item[]) => { + setSelected(newSelected); + }; + + // Avoid a layout jump when reaching the last page with empty rows. + const numEmptyRows = + rowsPerPage === -1 + ? Math.max(0, 5 - tableRows.length) + : Math.max(0, (1 + page) * Math.min(5, rowsPerPage) - tableRows.length); + + const visibleRows = useMemo( + () => + rowsPerPage > 0 || rowsPerPage === -1 + ? tableRows + .sort(getComparator(order, orderBy)) + .slice( + page * rowsPerPage, + page * rowsPerPage + + (rowsPerPage === -1 ? tableRows.length : rowsPerPage) + ) + : tableRows, + [order, orderBy, page, rowsPerPage, tableRows] + ); + + const handleSelectAllClick = useCallback( + (event: ChangeEvent) => { + if (event.target.checked) { + setSelected((prev) => { + const set = new Set([...prev, ...visibleRows]); + return [...set]; + }); + return; + } + setSelected((prev) => { + const set = new Set(prev); + visibleRows.forEach((visibleRow) => set.delete(visibleRow)); + return [...set]; + }); + }, + [visibleRows] + ); + + const handleChangeRowsPerPage = (newValue: string) => { + setRowsPerPage(parseInt(newValue, 10)); + setPage(0); + }; + + const selectedSet = new Set(selected.map((item) => item.id)); + const isAllRowsSelectedInCurrentPage = + selected.length > 0 && + visibleRows.every((visibleRow) => selectedSet.has(visibleRow.id)); + const isAllDeleteOnLastPage = + page >= Math.ceil(tableRows.length / rowsPerPage) - 1 && + isAllRowsSelectedInCurrentPage; + + return ( + + {tableRows.length > 0 ? ( + <> + {TableToolBar && ( + + )} + + + + + + + + + + + ) : ( + + )} + + ); +} diff --git a/src/components/Table/TableSkeleton.tsx b/src/components/Table/TableSkeleton.tsx new file mode 100644 index 0000000..c326c75 --- /dev/null +++ b/src/components/Table/TableSkeleton.tsx @@ -0,0 +1,97 @@ +import designSystem from "@/styles/designSystem"; +import { Skeleton } from "@mui/material"; +import styled from "styled-components"; + +type Props = { + tableTitle?: boolean; + tableToolBar?: boolean; + tablePagination?: boolean; + tableTitleHeight?: number; + tableToolBarHeight?: number; + tableHeadHeight?: number; + tableRowHeight?: number; + tablePaginationHeight?: number; +}; + +export default function TableSkeleton({ + tableTitle = true, + tableToolBar = true, + tablePagination = true, + tableTitleHeight = 39, + tableToolBarHeight = 32, + tableHeadHeight = 48, + tableRowHeight = 64, + tablePaginationHeight = 24, +}: Props) { + return ( + + {tableTitle && ( + + )} + + {tableToolBar && ( + + )} + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + ))} + + {tablePagination && ( + + )} + + ); +} + +const StyledTableSkeleton = styled.div` + width: 100%; + height: 100%; +`; + +const TableTitleSkeleton = styled(Skeleton)<{ $tableTitleHeight: number }>` + width: 100%; + height: ${({ $tableTitleHeight }) => $tableTitleHeight}px; + margin-bottom: 40px; +`; + +const TableToolBarSkeleton = styled(Skeleton)<{ $tableToolBarHeight: number }>` + width: 100%; + height: ${({ $tableToolBarHeight }) => $tableToolBarHeight}px; + margin-bottom: 16px; +`; + +const TableHeadSkeleton = styled(Skeleton)<{ $tableHeadHeight: number }>` + width: 100%; + height: ${({ $tableHeadHeight }) => $tableHeadHeight}px; + margin-bottom: 8px; +`; + +const TableRowSkeleton = styled(Skeleton)<{ $tableRowHeight: number }>` + width: 100%; + height: ${({ $tableRowHeight }) => $tableRowHeight}px; + border-bottom: 1px solid ${designSystem.color.neutral.gray100}; +`; + +const TablePaginationSkeleton = styled(Skeleton)<{ + $tablePaginationHeight: number; +}>` + width: 100%; + height: ${({ $tablePaginationHeight }) => $tablePaginationHeight}px; + margin-top: 16px; +`; diff --git a/src/components/Table/hooks/useTableSelection.ts b/src/components/Table/hooks/useTableSelection.ts new file mode 100644 index 0000000..d5e99f5 --- /dev/null +++ b/src/components/Table/hooks/useTableSelection.ts @@ -0,0 +1,38 @@ +import { MouseEvent, useCallback } from "react"; + +type UseTableSelectionProps = { + selected: readonly T[]; + updateSelected: (selected: readonly T[]) => void; +}; + +export function useTableSelection({ + selected, + updateSelected, +}: UseTableSelectionProps) { + const toggleSelect = useCallback( + (_: MouseEvent, row: T) => { + const selectedItemIndex = selected.findIndex( + (item) => item.id === row.id + ); + let newSelected: readonly T[] = []; + + if (selectedItemIndex === -1) { + newSelected = [...selected, row]; + } else { + newSelected = selected.filter( + (_, index) => index !== selectedItemIndex + ); + } + + updateSelected(newSelected); + }, + [selected, updateSelected] + ); + + const isSelected = useCallback( + (id: number) => !!selected.find((item) => item.id === id), + [selected] + ); + + return { toggleSelect, isSelected }; +} diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts new file mode 100644 index 0000000..3b4327e --- /dev/null +++ b/src/components/Table/types.ts @@ -0,0 +1 @@ +export type Order = "asc" | "desc"; diff --git a/src/components/Table/utils/comparator.ts b/src/components/Table/utils/comparator.ts new file mode 100644 index 0000000..dfce99b --- /dev/null +++ b/src/components/Table/utils/comparator.ts @@ -0,0 +1,47 @@ +import { Order } from "../types"; + +// Natural sorting algorithm to account for numbers in strings +export function descendingComparator(a: T, b: T, orderBy: keyof T) { + const getValue = (item: T) => { + const value = item[orderBy]; + if (typeof value === "string") { + // Split the string into parts of digits and non-digits + const parsedValue: (string | number)[] = value + .split(/(\d+)/) + .map((part) => + isNaN(Number(part)) ? part.toLowerCase() : Number(part) + ); + return parsedValue; + } + return [value]; // Return a single-element array for non-string values + }; + + const valueA = getValue(a); + const valueB = getValue(b); + + let comparison = 0; + for (let i = 0; i < Math.min(valueA.length, valueB.length); i++) { + if (valueB[i] < valueA[i]) { + // Sort valueA before valueB + comparison = -1; + break; + } + if (valueB[i] > valueA[i]) { + // Sort valueB before valueA + comparison = 1; + break; + } + } + + return comparison; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getComparator( + order: Order, + orderBy: keyof Item +): (a: Item, b: Item) => number { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} From 7ae8eab989dbaec4f99efe999ebbce5a80dc6082 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:30:02 +0900 Subject: [PATCH 10/39] =?UTF-8?q?#24=20refactor:=20Cookies=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20Type=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/api/apiRoutes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/api/apiRoutes.ts b/src/features/auth/api/apiRoutes.ts index 5f74c8c..62ca61b 100644 --- a/src/features/auth/api/apiRoutes.ts +++ b/src/features/auth/api/apiRoutes.ts @@ -1,7 +1,7 @@ import { clientFetcher } from "@/api/fetcher"; import { Response } from "@/api/types"; -export const getAuthStatus = async (cookies?: Record) => { +export const getAuthStatus = async (cookies?: {}) => { const res = await clientFetcher.get>("/authStatus", { cookies, }); From 7fadcf7a48f055b3289c967cd48dc73d2df0390e Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:30:21 +0900 Subject: [PATCH 11/39] =?UTF-8?q?#24=20feat:=20=EC=A6=9D=EA=B6=8C=EC=82=AC?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A1=9C=EA=B3=A0=20object=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/securitiesFirm.ts | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/constants/securitiesFirm.ts diff --git a/src/constants/securitiesFirm.ts b/src/constants/securitiesFirm.ts new file mode 100644 index 0000000..1f2967e --- /dev/null +++ b/src/constants/securitiesFirm.ts @@ -0,0 +1,69 @@ +import bnk from "@/assets/images/securitiesFirmLogo/bnk.png"; +import bookook from "@/assets/images/securitiesFirmLogo/bookook.png"; +import cape from "@/assets/images/securitiesFirmLogo/cape.png"; +import daishin from "@/assets/images/securitiesFirmLogo/daishin.png"; +import daol from "@/assets/images/securitiesFirmLogo/daol.png"; +import db from "@/assets/images/securitiesFirmLogo/db.png"; +import ebest from "@/assets/images/securitiesFirmLogo/ebest.png"; +import eugene from "@/assets/images/securitiesFirmLogo/eugene.png"; +import FA from "@/assets/images/securitiesFirmLogo/fineants.png"; +import hana from "@/assets/images/securitiesFirmLogo/hana.png"; +import hanwha from "@/assets/images/securitiesFirmLogo/hanwha.png"; +import hi from "@/assets/images/securitiesFirmLogo/hi.png"; +import hyundai from "@/assets/images/securitiesFirmLogo/hyundai.png"; +import ibk from "@/assets/images/securitiesFirmLogo/ibk.png"; +import kakao from "@/assets/images/securitiesFirmLogo/kakao.png"; +import kb from "@/assets/images/securitiesFirmLogo/kb.png"; +import kiwoom from "@/assets/images/securitiesFirmLogo/kiwoom.png"; +import korea from "@/assets/images/securitiesFirmLogo/korea.png"; +import koreafoss from "@/assets/images/securitiesFirmLogo/koreafoss.png"; +import kyobo from "@/assets/images/securitiesFirmLogo/kyobo.png"; +import meritz from "@/assets/images/securitiesFirmLogo/meritz.png"; +import miraeasset from "@/assets/images/securitiesFirmLogo/miraeasset.png"; +import namuh from "@/assets/images/securitiesFirmLogo/namuh.png"; +import samsung from "@/assets/images/securitiesFirmLogo/samsung.png"; +import sangsangin from "@/assets/images/securitiesFirmLogo/sangsangin.png"; +import shinhan from "@/assets/images/securitiesFirmLogo/shinhan.png"; +import shinyoung from "@/assets/images/securitiesFirmLogo/shinyoung.png"; +import sk from "@/assets/images/securitiesFirmLogo/sk.png"; +import toss from "@/assets/images/securitiesFirmLogo/toss.png"; +import yuanta from "@/assets/images/securitiesFirmLogo/yuanta.png"; + +export const securitiesFirmLogos = { + FineAnts: FA, + BNK투자증권: bnk, + 부국증권: bookook, + 케이프투자증권: cape, + 대신증권: daishin, + 다올투자증권: daol, + DB금융투자: db, + 이베스트투자증권: ebest, + 유진투자증권: eugene, + 하나증권: hana, + 한화투자증권: hanwha, + 하이투자증권: hi, + 현대차증권: hyundai, + IBK투자증권: ibk, + 카카오페이증권: kakao, + KB증권: kb, + 키움증권: kiwoom, + 한국투자증권: korea, + 한국포스증권: koreafoss, + 교보증권: kyobo, + 메리츠증권: meritz, + 미래에셋증권: miraeasset, + 나무증권: namuh, + 삼성증권: samsung, + 상상인증권: sangsangin, + 신한투자증권: shinhan, + 신영증권: shinyoung, + SK증권: sk, + 토스증권: toss, + 유안타증권: yuanta, +}; + +export type SecuritiesFirm = keyof typeof securitiesFirmLogos; + +export const SECURITIES_FIRM = Object.keys( + securitiesFirmLogos +) as SecuritiesFirm[]; From 469302edfde5fe8bf4fe24094dd798f5a92ae8df Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:30:49 +0900 Subject: [PATCH 12/39] =?UTF-8?q?#24=20feat:=20Portfolio=20API=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/portfolio/api/index.ts | 181 ++++++++++++++++++ .../portfolio/api/queries/queryKeys.ts | 7 + .../api/queries/usePortfolioDetailsQuery.ts | 12 ++ .../queries/usePortfolioHoldingAddMutation.ts | 37 ++++ .../usePortfolioHoldingDeleteMutation.ts | 23 +++ .../usePortfolioHoldingPurchaseAddMutation.ts | 30 +++ ...ePortfolioHoldingPurchaseDeleteMutation.ts | 22 +++ ...usePortfolioHoldingPurchaseEditMutation.ts | 22 +++ .../api/queries/usePortfolioListQuery.ts | 12 ++ .../api/queries/usePortfolioNameListQuery.ts | 12 ++ src/features/portfolio/api/types.ts | 147 ++++++++++++++ src/features/portfolio/types.ts | 1 + 12 files changed, 506 insertions(+) create mode 100644 src/features/portfolio/api/index.ts create mode 100644 src/features/portfolio/api/queries/queryKeys.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioDetailsQuery.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingDeleteMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingPurchaseDeleteMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingPurchaseEditMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioListQuery.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioNameListQuery.ts create mode 100644 src/features/portfolio/api/types.ts create mode 100644 src/features/portfolio/types.ts diff --git a/src/features/portfolio/api/index.ts b/src/features/portfolio/api/index.ts new file mode 100644 index 0000000..cabfd2d --- /dev/null +++ b/src/features/portfolio/api/index.ts @@ -0,0 +1,181 @@ +import { fetcher } from "@/api/fetcher"; +import { Response } from "@/api/types"; +import { + Portfolio, + PortfolioPageCharts, + PortfolioReqBody, + PortfoliosList, + PortfoliosNameList, + PurchaseHistoryInput, +} from "./types"; + +export const getPortfoliosList = async () => { + const res = await fetcher.get>("/portfolios"); + return res.data; +}; + +export const getPortfoliosNameList = async () => { + const res = + await fetcher.get>("/portfolios/names"); + return res.data; +}; + +export const getPortfolioCharts = async (portfolioId: number, cookies?: {}) => { + const res = await fetcher.get>( + `/portfolio/${portfolioId}/charts`, + { cookies } + ); + return res.data; +}; + +export const getPortfolioDetails = async ( + portfolioId: number, + cookies?: {} +) => { + const res = await fetcher.get>( + `/portfolio/${portfolioId}/holdings`, + { cookies } + ); + return res.data; +}; + +export const postPortfolio = async (body: PortfolioReqBody) => { + const res = await fetcher.post>( + `/portfolios`, + body + ); + return res.data; +}; + +export const putPortfolio = async ({ + portfolioId, + body, +}: { + portfolioId: number; + body: PortfolioReqBody; +}) => { + const res = await fetcher.put>( + `/portfolios/${portfolioId}`, + body + ); + return res.data; +}; + +export const deletePortfolio = async (portfolioId: number) => { + const res = await fetcher.delete>( + `/portfolios/${portfolioId}` + ); + return res.data; +}; + +export const deletePortfolios = async (portfolioIds: number[]) => { + const res = await fetcher.delete>("/portfolios", { + data: { portfolioIds }, + }); + return res.data; +}; + +export const postPortfolioHolding = async ({ + portfolioId, + body, +}: { + portfolioId: number; + body: { + tickerSymbol: string; + purchaseHistory?: PurchaseHistoryInput; + }; +}) => { + const res = await fetcher.post>( + `/portfolio/${portfolioId}/holdings`, + body + ); + return res.data; +}; + +export const deletePortfolioHolding = async ({ + portfolioId, + portfolioHoldingId, +}: { + portfolioId: number; + portfolioHoldingId: number; +}) => { + const res = await fetcher.delete>( + `/portfolio/${portfolioId}/holdings/${portfolioHoldingId}` + ); + return res.data; +}; + +export const postPortfolioHoldingPurchase = async ({ + portfolioId, + portfolioHoldingId, + body, +}: { + portfolioId: number; + portfolioHoldingId: number; + body: { + purchaseDate: string; + numShares: number; + purchasePricePerShare: number; + memo: string; + }; +}) => { + const res = await fetcher.post>( + `/portfolio/${portfolioId}/holdings/${portfolioHoldingId}/purchaseHistory`, + body + ); + return res.data; +}; + +export const deletePortfolioHoldings = async ({ + portfolioId, + body, +}: { + portfolioId: number; + body: { + portfolioHoldingIds: readonly number[]; + }; +}) => { + const res = await fetcher.delete>( + `/portfolio/${portfolioId}/holdings`, + { data: body } + ); + return res.data; +}; + +export const putPortfolioHoldingPurchase = async ({ + portfolioId, + portfolioHoldingId, + purchaseHistoryId, + body, +}: { + portfolioId: number; + portfolioHoldingId: number; + purchaseHistoryId: number; + body: { + purchaseDate: string; + numShares: number; + purchasePricePerShare: number; + memo: string; + }; +}) => { + const res = await fetcher.put>( + `/portfolio/${portfolioId}/holdings/${portfolioHoldingId}/purchaseHistory/${purchaseHistoryId}`, + body + ); + return res.data; +}; + +export const deletePortfolioHoldingPurchase = async ({ + portfolioId, + portfolioHoldingId, + purchaseHistoryId, +}: { + portfolioId: number; + portfolioHoldingId: number; + purchaseHistoryId: number; +}) => { + const res = await fetcher.delete>( + `/portfolio/${portfolioId}/holdings/${portfolioHoldingId}/purchaseHistory/${purchaseHistoryId}` + ); + return res.data; +}; diff --git a/src/features/portfolio/api/queries/queryKeys.ts b/src/features/portfolio/api/queries/queryKeys.ts new file mode 100644 index 0000000..b07d8cd --- /dev/null +++ b/src/features/portfolio/api/queries/queryKeys.ts @@ -0,0 +1,7 @@ +import { createQueryKeys } from "@lukemorales/query-key-factory"; + +export const portfolioKeys = createQueryKeys("portfolio", { + list: null, + details: (portfolioId: number) => [portfolioId], + charts: (portfolioId: number) => [portfolioId], +}); diff --git a/src/features/portfolio/api/queries/usePortfolioDetailsQuery.ts b/src/features/portfolio/api/queries/usePortfolioDetailsQuery.ts new file mode 100644 index 0000000..c9c7fc0 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioDetailsQuery.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getPortfolioDetails } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioDetailsQuery(portfolioId: number) { + return useSuspenseQuery({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + queryFn: () => getPortfolioDetails(portfolioId), + retry: false, + select: (res) => res.data, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts new file mode 100644 index 0000000..9fb00e2 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts @@ -0,0 +1,37 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postPortfolioHolding } from ".."; +import { portfolioKeys } from "./queryKeys"; + +type Props = { + portfolioId: number; + onClose: () => void; +}; + +export default function usePortfolioHoldingAddMutation({ + portfolioId, + onClose, +}: Props) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: postPortfolioHolding, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: portfolioKeys.charts(portfolioId).queryKey, + }); + onClose(); + }, + onError: (error) => { + //TODO : toast 추가 + // const message = (error as AxiosError>).response?.data + // ?.message as string; + // toast.error(message); + }, + meta: { + toastSuccessMessage: "종목이 추가되었습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingDeleteMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingDeleteMutation.ts new file mode 100644 index 0000000..05b366a --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingDeleteMutation.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deletePortfolioHoldings } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioHoldingDeleteMutation(portfolioId: number) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deletePortfolioHoldings, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: portfolioKeys.charts(portfolioId).queryKey, + }); + }, + meta: { + toastSuccessMessage: "종목을 삭제했습니다", + toastErrorMessage: "종목 삭제를 실패했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts new file mode 100644 index 0000000..fff4b06 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postPortfolioHoldingPurchase } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioHoldingPurchaseAddMutation( + portfolioId: number +) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: postPortfolioHoldingPurchase, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: portfolioKeys.charts(portfolioId).queryKey, + }); + }, + onError: (error) => { + //TODO toast 추가 필요 + // const message = (error as AxiosError>).response?.data + // ?.message as string; + // toast.error(message); + }, + meta: { + toastSuccessMessage: "매입 이력을 추가했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseDeleteMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseDeleteMutation.ts new file mode 100644 index 0000000..1d4935b --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseDeleteMutation.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deletePortfolioHoldingPurchase } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioHoldingPurchaseDeleteMutation( + portfolioId: number +) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deletePortfolioHoldingPurchase, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + }, + meta: { + toastSuccessMessage: "매입 이력을 삭제했습니다", + toastErrorMessage: "매입 이력 삭제를 실패했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseEditMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseEditMutation.ts new file mode 100644 index 0000000..163ad2a --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseEditMutation.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { putPortfolioHoldingPurchase } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioHoldingPurchaseEditMutation( + portfolioId: number +) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: putPortfolioHoldingPurchase, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + }, + meta: { + toastSuccessMessage: "매입 이력을 수정했습니다", + toastErrorMessage: "매입 이력 수정을 실패했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioListQuery.ts b/src/features/portfolio/api/queries/usePortfolioListQuery.ts new file mode 100644 index 0000000..a3134d4 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioListQuery.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getPortfoliosList } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioListQuery() { + return useSuspenseQuery({ + queryKey: portfolioKeys.list.queryKey, + queryFn: getPortfoliosList, + retry: false, + select: (res) => res.data.portfolios, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioNameListQuery.ts b/src/features/portfolio/api/queries/usePortfolioNameListQuery.ts new file mode 100644 index 0000000..da25c39 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioNameListQuery.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getPortfoliosNameList } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioNameListQuery() { + return useSuspenseQuery({ + queryKey: portfolioKeys.list.queryKey, + queryFn: getPortfoliosNameList, + retry: false, + select: (res) => res.data.portfolios, + }); +} diff --git a/src/features/portfolio/api/types.ts b/src/features/portfolio/api/types.ts new file mode 100644 index 0000000..820f399 --- /dev/null +++ b/src/features/portfolio/api/types.ts @@ -0,0 +1,147 @@ +import { PieChartData } from "@/components/PieChart/PieChart"; +import { SecuritiesFirm } from "@/constants/securitiesFirm"; + +export type PortfoliosList = { + portfolios: PortfolioItem[]; +}; + +export type PortfoliosNameList = { + portfolios: PortfoliosName[]; +}; + +export type PortfolioItem = { + id: number; + securitiesFirm: SecuritiesFirm; + name: string; + currentValuation: number; + budget: number; + totalGain: number; + totalGainRate: number; + dailyGain: number; + dailyGainRate: number; + expectedMonthlyDividend: number; + numShares: number; + dateCreated: string; +}; + +export type PortfoliosName = { + id: number; + name: string; + dateCreated: string; +}; + +export type Portfolio = { + portfolioDetails: PortfolioDetails; + portfolioHoldings: PortfolioHolding[]; +}; + +export type PortfolioDetailsSSE = Pick< + PortfolioDetails, + | "currentValuation" + | "totalGain" + | "totalGainRate" + | "dailyGain" + | "dailyGainRate" + | "provisionalLossBalance" +>; + +export type PortfolioHoldingsSSE = Pick< + PortfolioHolding, + | "currentValuation" + | "currentPrice" + | "dailyChange" + | "dailyChangeRate" + | "totalGain" + | "totalReturnRate" +>; + +export type PortfolioSSE = { + portfolioDetails: PortfolioDetailsSSE; + portfolioHoldings: PortfolioHoldingsSSE[]; +}; + +export type PortfolioDetails = { + id: number; + securitiesFirm: SecuritiesFirm; + name: string; + budget: number; + targetGain: number; + targetReturnRate: number; + maximumLoss: number; + maximumLossRate: number; + currentValuation: number; + investedAmount: number; + totalGain: number; + totalGainRate: number; + dailyGain: number; + dailyGainRate: number; + balance: number; + annualDividend: number; + annualDividendYield: number; + annualInvestmentDividendYield: number; + provisionalLossBalance: number; + targetGainNotify: boolean; + maxLossNotify: boolean; +}; + +export type PortfolioHolding = { + id: number; + companyName: string; + tickerSymbol: string; + currentValuation: number; + currentPrice: number; + averageCostPerShare: number; + numShares: number; + dailyChange: number; + dailyChangeRate: number; + totalGain: number; + totalReturnRate: number; + annualDividend: number; + annualDividendYield: number; + purchaseHistory: PurchaseHistory[]; + dateAdded: string; +}; + +export type PurchaseHistory = { + purchaseHistoryId: number; + purchaseDate: string; + numShares: number; + purchasePricePerShare: number; + memo: string; +}; + +export type PurchaseHistoryInput = { + purchaseDate: string; + numShares: number; + purchasePricePerShare: number; + memo: string; +}; + +export type PortfolioReqBody = { + name: string; + securitiesFirm: SecuritiesFirm; + budget: number; + targetGain: number; + maximumLoss: number; +}; + +export type PortfolioHoldingsDividendChartItem = { + month: number; + amount: number; +}; + +export type PortfolioHoldingsSectorBarItem = { + sector: string; + sectorWeight: number; +}; + +export type PortfolioPageCharts = { + portfolioDetails: { + id: number; + name: string; + securitiesFirm: SecuritiesFirm; + }; + pieChart: PieChartData[]; + dividendChart: PortfolioHoldingsDividendChartItem[]; + sectorChart: PortfolioHoldingsSectorBarItem[]; +}; diff --git a/src/features/portfolio/types.ts b/src/features/portfolio/types.ts new file mode 100644 index 0000000..312db27 --- /dev/null +++ b/src/features/portfolio/types.ts @@ -0,0 +1 @@ +export type PortfolioPageTab = "portfolio" | "chart"; From 6540c6fe52dc812571e5cfc6ae52765f2c46ac5d Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:31:23 +0900 Subject: [PATCH 13/39] =?UTF-8?q?#24=20feat:=20Portfolio=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=B4=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20Chart=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chart/Dividend/DividendBarChart.tsx | 79 ++++++++ .../Dividend/DividendBarChartContainer.tsx | 32 ++++ .../Chart/Dividend/DividendBarTooltip.tsx | 47 +++++ .../Chart/Dividend/RoundedBarShape.tsx | 41 ++++ .../Chart/PieChart/HoldingsPieChart.tsx | 181 ++++++++++++++++++ .../Chart/PieChart/PieChartContainer.tsx | 87 +++++++++ .../components/Chart/Sector/SectorBar.tsx | 52 +++++ .../Chart/Sector/SectorBarChartContainer.tsx | 64 +++++++ .../components/Chart/Sector/SectorBarItem.tsx | 72 +++++++ .../components/Chart/Sector/SectorTooltip.tsx | 30 +++ 10 files changed, 685 insertions(+) create mode 100644 src/features/portfolio/components/Chart/Dividend/DividendBarChart.tsx create mode 100644 src/features/portfolio/components/Chart/Dividend/DividendBarChartContainer.tsx create mode 100644 src/features/portfolio/components/Chart/Dividend/DividendBarTooltip.tsx create mode 100644 src/features/portfolio/components/Chart/Dividend/RoundedBarShape.tsx create mode 100644 src/features/portfolio/components/Chart/PieChart/HoldingsPieChart.tsx create mode 100644 src/features/portfolio/components/Chart/PieChart/PieChartContainer.tsx create mode 100644 src/features/portfolio/components/Chart/Sector/SectorBar.tsx create mode 100644 src/features/portfolio/components/Chart/Sector/SectorBarChartContainer.tsx create mode 100644 src/features/portfolio/components/Chart/Sector/SectorBarItem.tsx create mode 100644 src/features/portfolio/components/Chart/Sector/SectorTooltip.tsx diff --git a/src/features/portfolio/components/Chart/Dividend/DividendBarChart.tsx b/src/features/portfolio/components/Chart/Dividend/DividendBarChart.tsx new file mode 100644 index 0000000..0d2e689 --- /dev/null +++ b/src/features/portfolio/components/Chart/Dividend/DividendBarChart.tsx @@ -0,0 +1,79 @@ +import { PortfolioHoldingsDividendChartItem } from "@/features/portfolio/api/types"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import { useState } from "react"; +import { Bar, BarChart, Cell, Tooltip, XAxis } from "recharts"; +import DividendBarTooltip from "./DividendBarTooltip"; +import RoundedBarShape from "./RoundedBarShape"; + +type Props = { + data: PortfolioHoldingsDividendChartItem[]; +}; + +export default function DividendBarChart({ data }: Props) { + const { isMobile } = useResponsiveLayout(); + + const [currentMonthIndex, setCurrentMonthIndex] = useState( + new Date().getMonth() + ); + + const selectBar = (index: number) => { + setCurrentMonthIndex(index); + }; + + const hasNoDividendData = data.length === 0; + + const emptyDividendData = Array.from({ length: 12 }, (_, index) => ({ + month: index + 1, + amount: 0, + })); + + const barChartWidth = isMobile ? window.innerWidth - 32 : 400; + + return ( + + tickItem} + unit="월" + interval={0} + axisLine={{ + stroke: designSystem.color.neutral.gray400, + strokeWidth: 0.5, + }} + fontSize={12} + fontWeight={isMobile ? 350 : 400} + tick={{ fill: designSystem.color.neutral.gray400 }} + tickMargin={8} + /> + } + activeBar={}> + {(hasNoDividendData ? emptyDividendData : data).map((data, index) => ( + selectBar(index)} + /> + ))} + + {currentMonthIndex !== null && ( + } /> + )} + + ); +} diff --git a/src/features/portfolio/components/Chart/Dividend/DividendBarChartContainer.tsx b/src/features/portfolio/components/Chart/Dividend/DividendBarChartContainer.tsx new file mode 100644 index 0000000..4a912c8 --- /dev/null +++ b/src/features/portfolio/components/Chart/Dividend/DividendBarChartContainer.tsx @@ -0,0 +1,32 @@ +import { PortfolioHoldingsDividendChartItem } from "@/features/portfolio/api/types"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; +import DividendBarChart from "./DividendBarChart"; + +type Props = { + dividendChart: PortfolioHoldingsDividendChartItem[]; +}; + +export default function DividendBarChartContainer({ dividendChart }: Props) { + const { isMobile } = useResponsiveLayout(); + + return ( + + 예상 월 배당금 + + + ); +} + +const StyledDividendBarChartContainer = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +const ChartLabel = styled.h1<{ $isMobile: boolean }>` + ${({ $isMobile }) => + $isMobile ? designSystem.font.heading4 : designSystem.font.heading3}; + color: ${designSystem.color.neutral.gray900}; +`; diff --git a/src/features/portfolio/components/Chart/Dividend/DividendBarTooltip.tsx b/src/features/portfolio/components/Chart/Dividend/DividendBarTooltip.tsx new file mode 100644 index 0000000..cfa2134 --- /dev/null +++ b/src/features/portfolio/components/Chart/Dividend/DividendBarTooltip.tsx @@ -0,0 +1,47 @@ +import designSystem from "@/styles/designSystem"; +import { thousandsDelimiter } from "@fineants/demolition"; +import styled from "styled-components"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default function DividendBarTooltip({ active, payload, label }: any) { + if (active && payload.length > 0) { + const currentYear = new Date().getFullYear(); + + const month = parseInt(label, 10); + const paddedMonth = month.toString().padStart(2, "0"); + + const dividendAmount = payload[0].value; + + return ( + + + ₩{thousandsDelimiter(dividendAmount)} + + ); + } + + return null; +} + +const StyledDividendBarTooltip = styled.div` + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; + background-color: ${designSystem.color.neutral.white}; + border-radius: 4px; + border: 1px solid ${designSystem.color.neutral.gray100}; + box-shadow: 0px 0px 12px 0px #00000014; + + > label { + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray600}; + } + + > span { + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray800}; + } +`; diff --git a/src/features/portfolio/components/Chart/Dividend/RoundedBarShape.tsx b/src/features/portfolio/components/Chart/Dividend/RoundedBarShape.tsx new file mode 100644 index 0000000..643f84b --- /dev/null +++ b/src/features/portfolio/components/Chart/Dividend/RoundedBarShape.tsx @@ -0,0 +1,41 @@ +import designSystem from "@/styles/designSystem"; +import { useMemo } from "react"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default function RoundedBarShape(props: any) { + const { amount, fill, x, y, width, height, index, onClick, radius, isHover } = + props; + + const adjustedY = y - 8; + + const path = useMemo(() => { + return ` + M${x + radius},${adjustedY} + L${x + width - radius},${adjustedY} + Q${x + width},${adjustedY} ${x + width},${adjustedY + radius} + L${x + width},${adjustedY + height - radius} + Q${x + width},${adjustedY + height} ${x + width - radius},${ + adjustedY + height + } + L${x + radius},${adjustedY + height} + Q${x},${adjustedY + height} ${x},${adjustedY + height - radius} + L${x},${adjustedY + radius} + Q${x},${adjustedY} ${x + radius},${adjustedY} + Z`; + }, [x, adjustedY, width, height, radius]); + + return ( + onClick(index)} + /> + ); +} diff --git a/src/features/portfolio/components/Chart/PieChart/HoldingsPieChart.tsx b/src/features/portfolio/components/Chart/PieChart/HoldingsPieChart.tsx new file mode 100644 index 0000000..5af2c77 --- /dev/null +++ b/src/features/portfolio/components/Chart/PieChart/HoldingsPieChart.tsx @@ -0,0 +1,181 @@ +import WideLegend from "@/components/Legend/WideLegend"; +import { PieChartData } from "@/components/PieChart/PieChart"; +import { thousandsDelimiter } from "@fineants/demolition"; +import { useCallback, useState } from "react"; +import { Pie, PieChart, Sector } from "recharts"; +import styled from "styled-components"; + +type PieEntry = { + percent: number; + cornerRadius?: number; + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tooltipPayload: any[]; + midAngle: number; + cx: number; + cy: number; + endAngle: number; + fill: string; + innerRadius: number; + maxRadius: number; + outerRadius: number; + paddingAngle: number; + startAngle: number; + stroke: string; + tooltipPosition: { + x: number; + y: number; + }; + value: number; +}; + +type Props = { + data: PieChartData[]; +}; + +const DEFAULT_ACTIVE_INDEX = -1; + +export default function HoldingsPieChart({ data }: Props) { + const [activeIndex, setActiveIndex] = useState(DEFAULT_ACTIVE_INDEX); + + const totalValuation = data.reduce((acc, cur) => acc + cur.valuation, 0); + + const pieChartLegendList = data.map((item) => ({ + title: item.name, + percent: item.weight, + color: item.fill, + })); + + const onPieEnter = useCallback( + (_: PieEntry, index: number) => { + setActiveIndex(index); + }, + [setActiveIndex] + ); + + const onPieLeave = useCallback(() => { + setActiveIndex(DEFAULT_ACTIVE_INDEX); + }, [setActiveIndex]); + + return ( + + {activeIndex === DEFAULT_ACTIVE_INDEX && ( + +

총 자산 현황

+
{thousandsDelimiter(totalValuation)}
+
+ )} + + + + + + + {/* TODO */} + +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const renderActiveShape = (props: any) => { + const { + cx, + cy, + innerRadius, + outerRadius, + startAngle, + endAngle, + fill, + payload, + } = props; + + return ( + + + {payload.name} + + + {/* TODO: FIX! undefined */} + {/* {thousandsDelimiter(payload.value)} */} + + + + ); +}; + +const StyledHoldingsPieChart = styled.div` + width: 600px; + height: 318px; + display: flex; + justify-content: center; + align-items: center; + position: relative; + border-radius: 8px; + border: 1px solid #e0e0e0; + background-color: #ffffff; + box-shadow: 0px 0px 12px 0px #00000014; +`; + +const TotalValue = styled.div` + display: flex; + flex-direction: column; + + position: absolute; + top: 39%; + left: 45%; + z-index: 3; + > p { + font-size: 15px; + font-weight: bold; + color: #000000; + } + + > div { + display: flex; + justify-content: center; + font-size: 14px; + font-weight: bold; + color: #000000; + } +`; + +const PieChartWrapper = styled.div` + top: 10px; + width: 250px; + height: 250px; + position: absolute; +`; diff --git a/src/features/portfolio/components/Chart/PieChart/PieChartContainer.tsx b/src/features/portfolio/components/Chart/PieChart/PieChartContainer.tsx new file mode 100644 index 0000000..c64c432 --- /dev/null +++ b/src/features/portfolio/components/Chart/PieChart/PieChartContainer.tsx @@ -0,0 +1,87 @@ +import emptyHoldingsPieChartImg from "@/assets/images/no_holdings_pie_chart.png"; +import TallLegend from "@/components/Legend/TallLegend"; +import WideLegend from "@/components/Legend/WideLegend"; +import PieChart from "@/components/PieChart/PieChart"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; + +type Props = { + coloredPieChart: { + fill: string; + id: number; + name: string; + valuation: number; + totalGain: number; + totalGainRate: number; + weight: number; + }[]; + pieChartLegendList: { + title: string; + percent: number; + color: string; + }[]; +}; + +export function PieChartContainer({ + coloredPieChart, + pieChartLegendList, +}: Props) { + const { isMobile } = useResponsiveLayout(); + + const hasNoHoldings = + coloredPieChart.length === 1 && coloredPieChart[0].name === "현금"; + + return ( + + 종목 구성 + + {hasNoHoldings ? ( + // TODO : Image 태그 + 비어있는 파이차트 이미지 + ) : ( + + )} + {!hasNoHoldings && + (isMobile ? ( + + ) : ( + + ))} + + + ); +} + +const StyledPieChartContainer = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +const PieChartWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +`; + +const ChartLabel = styled.h1<{ $isMobile: boolean }>` + ${({ $isMobile }) => + $isMobile ? designSystem.font.heading4 : designSystem.font.heading3}; + color: ${designSystem.color.neutral.gray900}; +`; diff --git a/src/features/portfolio/components/Chart/Sector/SectorBar.tsx b/src/features/portfolio/components/Chart/Sector/SectorBar.tsx new file mode 100644 index 0000000..4087f1b --- /dev/null +++ b/src/features/portfolio/components/Chart/Sector/SectorBar.tsx @@ -0,0 +1,52 @@ +import { PortfolioHoldingsSectorBarItem } from "@/features/portfolio/api/types"; +import { chartColorPalette } from "@/styles/chartColorPalette"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; +import SectorBarItem from "./SectorBarItem"; + +type Props = { + data: PortfolioHoldingsSectorBarItem[]; + sectorBarWidth: number; + hasNoHoldings: boolean; +}; + +export default function SectorBar({ + data, + sectorBarWidth, + hasNoHoldings, +}: Props) { + const coloredData = data.map((item, index) => ({ + ...item, + fill: chartColorPalette[index], + })); + + return ( + + {hasNoHoldings ? ( + + ) : ( + coloredData.map((d, index) => ( + + )) + )} + + ); +} + +const StyledSectorBar = styled.div<{ $sectorBarWidth: number }>` + display: flex; + gap: 2px; + width: ${({ $sectorBarWidth }) => $sectorBarWidth}px; + overflow: hidden; +`; diff --git a/src/features/portfolio/components/Chart/Sector/SectorBarChartContainer.tsx b/src/features/portfolio/components/Chart/Sector/SectorBarChartContainer.tsx new file mode 100644 index 0000000..40dd018 --- /dev/null +++ b/src/features/portfolio/components/Chart/Sector/SectorBarChartContainer.tsx @@ -0,0 +1,64 @@ +import TallLegend from "@/components/Legend/TallLegend"; +import WideLegend from "@/components/Legend/WideLegend"; +import { PortfolioHoldingsSectorBarItem } from "@/features/portfolio/api/types"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import styled from "styled-components"; +import SectorBar from "./SectorBar"; + +type Props = { + sectorChart: PortfolioHoldingsSectorBarItem[]; + sectorLegendList: { + title: string; + percent: number; + color: string; + }[]; +}; +export default function SectorBarChartContainer({ + sectorChart, + sectorLegendList, +}: Props) { + const { isMobile } = useResponsiveLayout(); + const hasNoHoldings = + sectorChart.length === 1 && sectorChart[0].sector === "현금"; + + const sectorBarWidth = isMobile ? window.innerWidth - 32 : 400; + + return ( + + 섹터 구성 + + + {!hasNoHoldings && + (isMobile ? ( + + ) : ( + + ))} + + ); +} + +const StyledSectorBarChartContainer = styled.div` + display: flex; + flex-direction: column; + position: relative; + gap: 24px; +`; + +const ChartLabel = styled.h1<{ $isMobile: boolean }>` + ${({ $isMobile }) => + $isMobile ? designSystem.font.heading4 : designSystem.font.heading3}; + color: ${designSystem.color.neutral.gray900}; +`; diff --git a/src/features/portfolio/components/Chart/Sector/SectorBarItem.tsx b/src/features/portfolio/components/Chart/Sector/SectorBarItem.tsx new file mode 100644 index 0000000..8282f6d --- /dev/null +++ b/src/features/portfolio/components/Chart/Sector/SectorBarItem.tsx @@ -0,0 +1,72 @@ +import designSystem, { parseFontString } from "@/styles/designSystem"; +import Typography from "@mui/material/Typography"; +import { styled } from "@mui/material/styles"; +import { SectorTooltip } from "./SectorTooltip"; + +type Props = { + fill: string; + weight: number; + title: string; + sectorBarWidth: number; +}; +export default function SectorBarItem({ + title, + fill, + weight, + sectorBarWidth, +}: Props) { + return ( +
+ + + + {title} + + {weight}% + + }> + + +
+ ); +} + +const StyledSectorBarItem = styled("div")<{ $fill: string; $width: number }>( + ({ $fill, $width }) => ({ + width: `${$width}px`, + height: "24px", + display: "flex", + justifyContent: "center", + borderRadius: "4px", + backgroundColor: $fill, + }) +); + +const Color = styled("div")<{ $color: string }>(({ $color }) => ({ + width: "10px", + height: "10px", + borderRadius: "50%", + backgroundColor: $color, +})); + +const TitleWrapper = styled("div")({ + display: "flex", + alignItems: "center", + gap: "3.5px", +}); + +const SectorTitle = styled(Typography)({ + ...parseFontString(designSystem.font.title5), + color: designSystem.color.neutral.gray600, +}); + +const Percent = styled(Typography)({ + ...parseFontString(designSystem.font.title5), + color: designSystem.color.primary.blue500, +}); diff --git a/src/features/portfolio/components/Chart/Sector/SectorTooltip.tsx b/src/features/portfolio/components/Chart/Sector/SectorTooltip.tsx new file mode 100644 index 0000000..7d8cf06 --- /dev/null +++ b/src/features/portfolio/components/Chart/Sector/SectorTooltip.tsx @@ -0,0 +1,30 @@ +import designSystem from "@/styles/designSystem"; +import { Tooltip, TooltipProps, styled, tooltipClasses } from "@mui/material"; + +export const SectorTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + display: "flex", + alignItems: "center", + backgroundColor: designSystem.color.neutral.white, + gap: "8px", + color: designSystem.color.neutral.gray600, + fontSize: theme.typography.pxToRem(12), + border: `1px solid ${designSystem.color.neutral.gray100}`, + boxShadow: "0px 4px 8px 0px rgba(0, 0, 0, 0.08)", + }, +})); From 256bd2f342101eb91fd1bfec41d28a1b3e85ce45 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:34:19 +0900 Subject: [PATCH 14/39] =?UTF-8?q?#24=20feat:=20RealtimeValue=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portfolio/components/RealtimeValue.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/features/portfolio/components/RealtimeValue.tsx diff --git a/src/features/portfolio/components/RealtimeValue.tsx b/src/features/portfolio/components/RealtimeValue.tsx new file mode 100644 index 0000000..8808d53 --- /dev/null +++ b/src/features/portfolio/components/RealtimeValue.tsx @@ -0,0 +1,63 @@ +import designSystem from "@/styles/designSystem"; +import { thousandsDelimiter } from "@fineants/demolition"; +import { memo, useEffect, useRef, useState } from "react"; +import styled from "styled-components"; + +type Props = { + value: number; +}; + +type ValueStatus = "gain" | "loss" | "none"; + +export default memo(function RealtimeValue({ value }: Props) { + const prevValue = useRef(value); + const timerRef = useRef | null>(null); + + const [status, setStatus] = useState("none"); + + useEffect(() => { + if (value > prevValue.current) { + setStatus("gain"); + } else if (value < prevValue.current) { + setStatus("loss"); + } else { + setStatus("none"); + } + + prevValue.current = value; + }, [value]); + + useEffect(() => { + if (status !== "none") { + timerRef.current = setTimeout(() => { + setStatus("none"); + }, 2500); + } else if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [status]); + + return ( + {thousandsDelimiter(value)} + ); +}); + +const StyledPrice = styled.span<{ $status: ValueStatus }>` + color: ${({ $status }) => { + switch ($status) { + case "gain": + return designSystem.color.state.green500; + case "loss": + return designSystem.color.state.red500; + case "none": + return designSystem.color.neutral.gray900; + } + }}; +`; From 871d1093c62ef7b7ddeaed55269c918d3475ad01 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:34:38 +0900 Subject: [PATCH 15/39] =?UTF-8?q?#24=20feat:=20utils=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/date.ts | 16 ++++++++++++++++ src/utils/openPopUpWindow.ts | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/utils/date.ts create mode 100644 src/utils/openPopUpWindow.ts diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..9bf6623 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,16 @@ +// Convert YYYY-MM-DDTHH:MM:SS to YYYY-MM-DD +export function formatDate(dateStr: string): string { + const dateStrRegex = new RegExp( + /\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-4]):([0-5][0-9]):([0-5][0-9])/ + ); + if (!dateStrRegex.test(dateStr)) { + throw Error("날짜 형식은 YYYY-MM-DDTHH:MM:SS 입니다"); + } + + const date = new Date(dateStr); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + return `${year}-${month}-${day}`; +} diff --git a/src/utils/openPopUpWindow.ts b/src/utils/openPopUpWindow.ts new file mode 100644 index 0000000..b830c6b --- /dev/null +++ b/src/utils/openPopUpWindow.ts @@ -0,0 +1,14 @@ +export default function openPopUpWindow( + url: string, + target: string, + width: number, + height: number +) { + const y = window.outerHeight / 2 + window.screenY - height / 2; + const x = window.outerWidth / 2 + window.screenX - width / 2; + return window.open( + url, + target, + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${y}, left=${x}` + ); +} From fa35a374861c762ee32718fecdfcd49297247942 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:35:07 +0900 Subject: [PATCH 16/39] =?UTF-8?q?#24=20feat:=20Portfolio=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=B4=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B2=8C=EB=90=A0=20customHook=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/portfolio/hook/usePortfolioId.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/features/portfolio/hook/usePortfolioId.ts diff --git a/src/features/portfolio/hook/usePortfolioId.ts b/src/features/portfolio/hook/usePortfolioId.ts new file mode 100644 index 0000000..95c5447 --- /dev/null +++ b/src/features/portfolio/hook/usePortfolioId.ts @@ -0,0 +1,7 @@ +import { useRouter } from "next/router"; + +export function usePortfolioId() { + const { query } = useRouter(); + const portfolioId = query.portfolioId; + return Array.isArray(portfolioId) ? portfolioId[0] : portfolioId || ""; +} From fe8f99a29bed12148e60e6583f8b75593410bffb Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:35:38 +0900 Subject: [PATCH 17/39] =?UTF-8?q?#24=20feat:=20Portfolio=20Holding=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EmptyPortfolioHoldingTable.tsx | 77 ++++ .../PortfolioHoldingAddDialog.tsx | 18 + .../PortfolioHoldingDeleteConfirm.tsx | 23 ++ .../PortfolioHoldingSelectedDeleteConfirm.tsx | 26 ++ .../desktop/PortfolioHoldingAddDialogD.tsx | 361 ++++++++++++++++++ .../PortfolioHoldingLotAddRowD.tsx | 198 ++++++++++ .../PortfolioHoldingLotRowD.tsx | 282 ++++++++++++++ .../PortfolioHoldingLotsTableD.tsx | 159 ++++++++ .../desktop/PortfolioHoldingRow.tsx | 285 ++++++++++++++ .../desktop/PortfolioHoldingTable.tsx | 25 ++ .../desktop/PortfolioHoldingTableBody.tsx | 58 +++ .../desktop/PortfolioHoldingTableHead.tsx | 242 ++++++++++++ .../desktop/PortfolioHoldingTableToolBar.tsx | 120 ++++++ 13 files changed, 1874 insertions(+) create mode 100644 src/features/portfolio/components/PortfolioHolding/EmptyPortfolioHoldingTable.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/PortfolioHoldingDeleteConfirm.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/PortfolioHoldingSelectedDeleteConfirm.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingAddDialogD.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotAddRowD.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotRowD.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotsTableD.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTable.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableBody.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableHead.tsx create mode 100644 src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableToolBar.tsx diff --git a/src/features/portfolio/components/PortfolioHolding/EmptyPortfolioHoldingTable.tsx b/src/features/portfolio/components/PortfolioHolding/EmptyPortfolioHoldingTable.tsx new file mode 100644 index 0000000..bc64c7b --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/EmptyPortfolioHoldingTable.tsx @@ -0,0 +1,77 @@ +import noHoldingStockImg from "@/assets/images/no_holdings_stock.png"; +import Button from "@/components/Buttons/Button"; +import { Icon } from "@/components/Icon"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import designSystem from "@/styles/designSystem"; +import { useBoolean } from "@fineants/demolition"; +import styled from "styled-components"; +import PortfolioHoldingAddDialog from "./PortfolioHoldingAddDialog"; + +export default function EmptyPortfolioHoldingTable() { + const { isMobile } = useResponsiveLayout(); + + const { + state: isAddHoldingDialogOpen, + setTrue: onDialogOpen, + setFalse: onDialogClose, + } = useBoolean(); + + return ( + <> + + 보유 종목 없음 + + +
종목을 추가하세요
+ 보유한 종목을 추가하여 포트폴리오 관리를 시작하세요 +
+ +
+ + {isAddHoldingDialogOpen && ( + + )} + + ); +} + +const StyledEmptyPortfolioHoldingTable = styled.div<{ $isMobile: boolean }>` + width: 100%; + height: 318px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + border-radius: 8px; + border: ${({ $isMobile }) => + $isMobile ? "none" : `1px dashed ${designSystem.color.primary.blue100}`}; + ${designSystem.font.title3}; + color: ${designSystem.color.neutral.gray600}; +`; + +const TextBox = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + text-align: center; + + > div { + ${designSystem.font.title3}; + color: ${designSystem.color.neutral.gray600}; + } + + > span { + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray500}; + } +`; diff --git a/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx new file mode 100644 index 0000000..ab8abe4 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx @@ -0,0 +1,18 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import PortfolioHoldingAddDialogD from "./desktop/PortfolioHoldingAddDialogD"; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export default function PortfolioHoldingAddDialog(props: Props) { + const { isDesktop, isMobile } = useResponsiveLayout(); + + return ( + <> + {isDesktop && } + {/* {isMobile && } */} + + ); +} diff --git a/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingDeleteConfirm.tsx b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingDeleteConfirm.tsx new file mode 100644 index 0000000..4acc3f6 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingDeleteConfirm.tsx @@ -0,0 +1,23 @@ +import ConfirmAlert from "@/components/ConfirmAlert"; + +type Props = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; +}; + +export default function PortfolioHoldingDeleteConfirm({ + isOpen, + onClose, + onConfirm, +}: Props) { + return ( + + 매입 이력을 삭제하시겠습니까? + + ); +} diff --git a/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingSelectedDeleteConfirm.tsx b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingSelectedDeleteConfirm.tsx new file mode 100644 index 0000000..7ac83b7 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingSelectedDeleteConfirm.tsx @@ -0,0 +1,26 @@ +import ConfirmAlert from "@/components/ConfirmAlert"; + +type Props = { + isOpen: boolean; + selected: readonly Item[]; + onClose: () => void; + onConfirm: () => void; +}; + +export default function PortfolioHoldingSelectedDeleteConfirm< + Item extends { companyName: string }, +>({ isOpen, selected, onClose, onConfirm }: Props) { + return ( + + + {`'${selected.length !== 0 && selected[0].companyName}'${ + selected.length > 1 ? ` 외 ${selected.length - 1}개` : "" + } 종목을 삭제하시겠습니까?`} + + + ); +} diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingAddDialogD.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingAddDialogD.tsx new file mode 100644 index 0000000..b53ef6a --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingAddDialogD.tsx @@ -0,0 +1,361 @@ +import BaseDialog from "@/components/BaseDialog"; +import AsyncButton from "@/components/Buttons/AsyncButton"; +import { IconButton } from "@/components/Buttons/IconButton"; +import DatePicker from "@/components/DatePicker"; +import SearchBarD from "@/components/SearchBar/desktop/SearchBarD"; +import usePortfolioHoldingAddMutation from "@/features/portfolio/api/queries/usePortfolioHoldingAddMutation"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import { StockSearchItem } from "@/features/stock/api/types"; +import designSystem from "@/styles/designSystem"; +import { + executeCbIfNumeric, + removeThousandsDelimiter, + useText, +} from "@fineants/demolition"; +import dayjs, { Dayjs } from "dayjs"; +import { ChangeEvent, FormEvent, memo, useState } from "react"; +import styled from "styled-components"; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export default memo(function PortfolioHoldingAddDialogD({ + isOpen, + onClose, +}: Props) { + const portfolioId = usePortfolioId(); + + const { + mutateAsync: portfolioHoldingAddMutateAsync, + isPending: isPortfolioHoldingAddMutatePending, + } = usePortfolioHoldingAddMutation({ + portfolioId: Number(portfolioId), + onClose, + }); + + const [selectedStock, setSelectedStock] = useState( + null + ); + + const [newPurchaseDate, setNewPurchaseDate] = useState( + dayjs(new Date()) + ); + + const { + value: purchasePricePerShare, + onChange: onPurchasePricePerShareChange, + } = useText(); + const purchasePricePerShareHandler = (e: ChangeEvent) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onPurchasePricePerShareChange, + }); + }; + + const { value: numShares, onChange: onNumSharesChange } = useText(); + const numSharesHandler = (e: ChangeEvent) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onNumSharesChange, + }); + }; + + const { value: memo, onChange: onMemoChange } = useText(); + + const onSelectOption = (stock: StockSearchItem) => { + setSelectedStock(stock); + }; + + const addStockToPortfolio = async (stock: StockSearchItem) => { + const purchaseHistory = { + purchaseDate: newPurchaseDate + ? newPurchaseDate.format("YYYY-MM-DDTHH:mm:ss") + : "", + numShares: Number(removeThousandsDelimiter(numShares)), + purchasePricePerShare: Number( + removeThousandsDelimiter(purchasePricePerShare) + ), + memo, + }; + + const hasPurchaseHistory = + newPurchaseDate?.toString() !== "" && + numShares !== "" && + purchasePricePerShare !== ""; + + await portfolioHoldingAddMutateAsync({ + portfolioId: Number(portfolioId), + body: { + tickerSymbol: stock.tickerSymbol, + purchaseHistory: hasPurchaseHistory ? purchaseHistory : undefined, + }, + }); + + setSelectedStock(null); + }; + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!selectedStock) return; + addStockToPortfolio(selectedStock); + }; + + const onDialogClose = () => { + onClose(); + setSelectedStock(null); + }; + + const onDeleteHoldingBoxClick = () => { + setSelectedStock(null); + }; + + return ( + +
+ 종목 추가 + +
+ +
+ 종목 검색 * +
+ +
+
+ {selectedStock && ( + + + + {selectedStock.tickerSymbol} + + + + )} + + + + + setNewPurchaseDate(newVal)} + /> + + + + + + +
+
+
+ + + + + + + + + + + onMemoChange(e.target.value.trim())} + /> + +
+ + + 추가 + +
+
+ ); +}); + +const portfolioHoldingAddDialogStyle = { + height: "auto", + padding: "32px", +}; + +const Header = styled.header` + margin-bottom: 32px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Title = styled.div` + ${designSystem.font.heading3}; + color: ${designSystem.color.neutral.gray800}; +`; + +const SearchWrapper = styled.div` + width: 100%; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 8px; + ${designSystem.font.title5}; + + > div { + color: ${designSystem.color.neutral.gray800}; + + > span { + color: ${designSystem.color.state.red500}; + } + } +`; + +const Form = styled.form` + width: 100%; +`; + +const HoldingBox = styled.div` + width: 100%; + height: 64px; + padding: 0 16px; + display: flex; + align-items: center; + justify-content: space-between; + background-color: ${designSystem.color.neutral.gray50}; + border: 1px solid ${designSystem.color.primary.blue50}; + border-radius: 8px; + ${designSystem.font.title5}; + color: ${designSystem.color.primary.blue500}; +`; + +const TitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; + + > span { + ${designSystem.font.body4}; + color: ${designSystem.color.neutral.gray400}; + } +`; + +const InputContainer = styled.div` + margin-top: 24px; + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 16px; +`; + +const InputBox = styled.div` + display: flex; + gap: 8px; + + > label { + width: 120px; + height: 24px; + margin-top: 4px; + display: flex; + align-items: center; + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray800}; + } +`; + +const InputWrapper = styled.div` + height: 32px; + padding: 4px 8px; + display: flex; + justify-content: space-between; + align-items: center; + flex: 1; + box-sizing: border-box; + border: 1px solid ${designSystem.color.neutral.gray200}; + border-radius: 3px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray400}; + + &:hover, + &:focus-within { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; + +const InputTextArea = styled.textarea` + height: 54px; + padding: 4px 8px; + flex: 1; + display: flex; + align-items: center; + box-sizing: border-box; + border: 1px solid ${designSystem.color.neutral.gray200}; + border-radius: 3px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray800}; + + &&::placeholder { + color: ${designSystem.color.neutral.gray400}; + } + + &:hover, + &:focus { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; + +const Input = styled.input` + height: 100%; + flex: 1; + border: none; + outline: none; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray800}; + + &&::placeholder { + color: ${designSystem.color.neutral.gray400}; + } +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotAddRowD.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotAddRowD.tsx new file mode 100644 index 0000000..ea9fbe0 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotAddRowD.tsx @@ -0,0 +1,198 @@ +import { IconButton } from "@/components/Buttons/IconButton"; +import DatePicker from "@/components/DatePicker"; +import usePortfolioHoldingPurchaseAddMutation from "@/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation"; +import designSystem from "@/styles/designSystem"; +import { + executeCbIfNumeric, + removeThousandsDelimiter, + useText, +} from "@fineants/demolition"; +import { + TableCell as MuiTableCell, + TableRow as MuiTableRow, +} from "@mui/material"; +import dayjs, { Dayjs } from "dayjs"; +import { ChangeEvent, useState } from "react"; +import styled from "styled-components"; + +type Props = { + portfolioId: number; + portfolioHoldingId: number; + onDeleteButtonClick: () => void; +}; + +export default function PortfolioHoldingLotAddRowD({ + portfolioId, + portfolioHoldingId, + onDeleteButtonClick, +}: Props) { + const [newPurchaseDate, setNewPurchaseDate] = useState( + dayjs(new Date()) + ); + + const { + value: newPurchasePricePerShare, + onChange: onNewPurchasePricePerShareChange, + } = useText(); + const newPurchasePricePerShareHandler = ( + e: ChangeEvent + ) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onNewPurchasePricePerShareChange, + }); + }; + + const { value: newNumShares, onChange: onNewNumSharesChange } = useText(); + const newNumSharesHandler = (e: ChangeEvent) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onNewNumSharesChange, + }); + }; + + const [newMemo, setNewMemo] = useState(""); + + const { mutate: portfolioHoldingPurchaseAddMutate } = + usePortfolioHoldingPurchaseAddMutation(portfolioId); + + const onSaveClick = () => { + portfolioHoldingPurchaseAddMutate({ + portfolioId, + portfolioHoldingId, + body: { + purchaseDate: newPurchaseDate?.toISOString() ?? "", + purchasePricePerShare: Number( + removeThousandsDelimiter(newPurchasePricePerShare) + ), + numShares: Number(removeThousandsDelimiter(newNumShares)), + memo: newMemo.trim(), + }, + }); + onPurchaseValuesRemove(); + }; + + const onPurchaseValuesRemove = () => { + setNewPurchaseDate(null); + onNewPurchasePricePerShareChange(""); + onNewNumSharesChange(""); + setNewMemo(""); + onDeleteButtonClick(); + }; + + const isValid = newPurchaseDate && newPurchasePricePerShare && newNumShares; + + return ( + + + setNewPurchaseDate(newVal)} + /> + + + + + + + + + + + setNewMemo(e.target.value)} + /> + + + + + + + + + + + ); +} + +const StyledTableCell = styled(MuiTableCell)` + height: 40px; + padding: 0 8px; + ${designSystem.font.body3}; + + color: ${designSystem.color.neutral.gray900}; + text-align: center; +`; + +const Input = styled.input` + width: 100%; + height: 24px; + padding: 0 8px; + box-sizing: border-box; + background-color: ${designSystem.color.neutral.white}; + border: 1px solid ${designSystem.color.neutral.gray200}; + border-radius: 2px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + + &::placeholder { + color: ${designSystem.color.neutral.gray400}; + } + + &:focus { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; + +const StyledTextArea = styled.textarea` + width: 100%; + height: 24px; + margin-top: 7px; + padding: 0 8px; + box-sizing: border-box; + background-color: ${designSystem.color.neutral.white}; + border: 1px solid ${designSystem.color.neutral.gray200}; + border-radius: 2px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + text-align: left; + + &::placeholder { + color: ${designSystem.color.neutral.gray400}; + } + + &:focus { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotRowD.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotRowD.tsx new file mode 100644 index 0000000..fe4e0f9 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotRowD.tsx @@ -0,0 +1,282 @@ +import { IconButton } from "@/components/Buttons/IconButton"; +import DatePicker from "@/components/DatePicker"; +import usePortfolioHoldingPurchaseDeleteMutation from "@/features/portfolio/api/queries/usePortfolioHoldingPurchaseDeleteMutation"; +import usePortfolioHoldingPurchaseEditMutation from "@/features/portfolio/api/queries/usePortfolioHoldingPurchaseEditMutation"; +import { PurchaseHistory } from "@/features/portfolio/api/types"; +import designSystem from "@/styles/designSystem"; +import { formatDate } from "@/utils/date"; +import { + executeCbIfNumeric, + removeThousandsDelimiter, + thousandsDelimiter, + useBoolean, + useText, +} from "@fineants/demolition"; +import { + TableCell as MuiTableCell, + TableRow as MuiTableRow, +} from "@mui/material"; +import dayjs, { Dayjs } from "dayjs"; +import { ChangeEvent, useState } from "react"; +import styled from "styled-components"; +import PortfolioHoldingDeleteConfirm from "../../PortfolioHoldingDeleteConfirm"; + +type Props = { + portfolioId: number; + portfolioHoldingId: number; + lot: PurchaseHistory; +}; + +export default function PortfolioHoldingLotRowD({ + portfolioId, + portfolioHoldingId, + lot: { + purchaseHistoryId, + purchaseDate, + purchasePricePerShare, + numShares, + memo, + }, +}: Props) { + const { mutate: portfolioHoldingPurchaseEditMutate } = + usePortfolioHoldingPurchaseEditMutation(portfolioId); + + const { mutate: portfolioHoldingPurchaseDeleteMutate } = + usePortfolioHoldingPurchaseDeleteMutation(portfolioId); + + const { + state: isEditing, + setTrue: onEdit, + setFalse: onEditCancel, + } = useBoolean(); + const { + state: isDeleteConfirmAlertOpen, + setTrue: onOpenDeleteConfirmAlert, + setFalse: onCloseDeleteConfirmAlert, + } = useBoolean(); + + const [newPurchaseDate, setNewPurchaseDate] = useState( + dayjs(purchaseDate) + ); + + const { + value: newPurchasePricePerShare, + onChange: onNewPurchasePricePerShareChange, + } = useText({ + initialValue: thousandsDelimiter(purchasePricePerShare), + }); + const newPurchasePricePerShareHandler = ( + e: ChangeEvent + ) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onNewPurchasePricePerShareChange, + }); + }; + + const { value: newNumShares, onChange: onNewNumSharesChange } = useText({ + initialValue: thousandsDelimiter(numShares), + }); + const newNumSharesHandler = (e: ChangeEvent) => { + executeCbIfNumeric({ + value: e.target.value.trim(), + callback: onNewNumSharesChange, + }); + }; + + const [newMemo, setNewMemo] = useState(memo ?? ""); + + const onSaveClick = () => { + portfolioHoldingPurchaseEditMutate({ + portfolioId, + portfolioHoldingId, + purchaseHistoryId, + body: { + purchaseDate: newPurchaseDate?.toISOString() ?? "", + purchasePricePerShare: Number( + removeThousandsDelimiter(newPurchasePricePerShare) + ), + numShares: Number(removeThousandsDelimiter(newNumShares)), + memo: newMemo.trim(), + }, + }); + + onEditCancel(); + }; + + const onDeleteConfirm = () => { + portfolioHoldingPurchaseDeleteMutate({ + portfolioId, + portfolioHoldingId, + purchaseHistoryId, + }); + }; + + return ( + + {isEditing ? ( + <> + + setNewPurchaseDate(newVal)} + /> + + + newPurchasePricePerShareHandler(e)} + /> + + + + newNumSharesHandler(e)} + /> + + + + setNewMemo(e.target.value)} + /> + + + + + + + + + + + ) : ( + <> + + {formatDate(purchaseDate)} + + + + {thousandsDelimiter(purchasePricePerShare)} + + + + {numShares} + + + {memo} + + + + + + + + + + + )} + + ); +} + +const StyledTableRow = styled(MuiTableRow)` + width: 856px; + height: 40px; + padding: 8px 16px; + box-sizing: border-box; + + & > .MuiTableCell-root { + border-bottom: 1px solid ${designSystem.color.neutral.gray100}; + } + + & > :first-child { + padding-left: 16px; + } + + & > :last-child { + padding-right: 16px; + } +`; + +const StyledTableCell = styled(MuiTableCell)` + padding: 4px 8px; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; +`; + +const Input = styled.input` + width: 100%; + height: 24px; + padding: 0 8px; + box-sizing: border-box; + border: 1px solid ${designSystem.color.neutral.gray200}; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + background-color: ${designSystem.color.neutral.white}; + border-radius: 2px; + + &::placeholder { + color: ${designSystem.color.neutral.gray400}; + } + + &:focus { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; + +const StyledTextArea = styled.textarea` + margin-top: 7px; + width: 100%; + height: 24px; + padding: 0 8px; + border: 1px solid ${designSystem.color.neutral.gray200}; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + background-color: ${designSystem.color.neutral.white}; + border-radius: 2px; + box-sizing: border-box; + + &::placeholder { + color: ${designSystem.color.neutral.gray400}; + } + + &:focus { + border: 1px solid ${designSystem.color.primary.blue500}; + } +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotsTableD.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotsTableD.tsx new file mode 100644 index 0000000..04dfdf1 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingLots/PortfolioHoldingLotsTableD.tsx @@ -0,0 +1,159 @@ +import { TextButton } from "@/components/Buttons/TextButton"; +import { Icon } from "@/components/Icon"; +import { PurchaseHistory } from "@/features/portfolio/api/types"; +import designSystem from "@/styles/designSystem"; +import { useBoolean } from "@fineants/demolition"; +import { + Table as MuiTable, + TableBody as MuiTableBody, + TableCell as MuiTableCell, + TableFooter as MuiTableFooter, + TableHead as MuiTableHead, + TableRow as MuiTableRow, +} from "@mui/material"; +import styled from "styled-components"; +import PortfolioHoldingLotAddRowD from "./PortfolioHoldingLotAddRowD"; +import PortfolioHoldingLotRowD from "./PortfolioHoldingLotRowD"; + +type Props = { + portfolioId: number; + portfolioHoldingId: number; + purchaseHistory: PurchaseHistory[]; +}; + +// TODO: PlainTable을 사용하도록 수정 +export default function PortfolioHoldingLotsTableD({ + portfolioId, + portfolioHoldingId, + purchaseHistory, +}: Props) { + const { + state: isAddLotMode, + setTrue: onAddLotButtonClick, + setFalse: onDeleteLotButtonClick, + } = useBoolean(); + + return ( + + + + + + + 매입 날짜 + + + 매입가 + + + 개수 + + + 메모 + + + + + + + + {purchaseHistory.map((lot) => ( + + ))} + {isAddLotMode && ( + + )} + + + + + + + + 매입 이력 추가 + + + + + + + + ); +} + +const StyledPortfolioHoldingLotsTable = styled.div` + width: 896px; + padding-left: 24px; + display: flex; + justify-content: flex-end; + margin-top: 8px; + padding-bottom: 8px; + border-bottom: 1px solid ${designSystem.color.neutral.gray100}; +`; + +const Wrapper = styled.div` + width: inherit; + padding-left: 16px; + border-left: 1px solid ${designSystem.color.primary.blue100}; +`; + +const StyledTable = styled(MuiTable)` + display: flex; + flex-direction: column; + gap: 8px; +`; + +const StyledTableHead = styled(MuiTableHead)` + width: 856px; + margin-left: auto; + + & > tr:last-child { + td { + padding-bottom: 8px; + } + } +`; + +const StyledTableHeadRow = styled(MuiTableRow)` + width: 856px; + background-color: ${designSystem.color.neutral.gray50}; + border-radius: 8px; + + & > * { + border: none; + } + + & > .MuiTableCell-root:first-child { + padding-left: 16px; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + + & > .MuiTableCell-root:last-child { + padding-right: 16px; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + } +`; + +const StyledTableHeadCell = styled(MuiTableCell)` + height: 40px; + padding: 4px 8px; + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray600}; +`; + +const StyledTableBody = styled(MuiTableBody)` + width: 100%; +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx new file mode 100644 index 0000000..04bcaf5 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx @@ -0,0 +1,285 @@ +import RateBadge from "@/components/Badges/RateBadge"; +import { IconButton } from "@/components/Buttons/IconButton"; +import CheckBox from "@/components/Checkbox"; +import { EllipsisTextTooltip } from "@/components/Tooltips/EllipsisTextTooltip"; +import Routes from "@/constants/Routes"; +import { PortfolioHolding } from "@/features/portfolio/api/types"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import designSystem from "@/styles/designSystem"; +import { thousandsDelimiter } from "@fineants/demolition"; +import { Collapse, TableCell, TableRow, Typography } from "@mui/material"; +import Link from "next/link"; +import { MouseEvent, memo, useCallback, useEffect, useState } from "react"; +import styled from "styled-components"; +import RealtimeValue from "../../RealtimeValue"; +import PortfolioHoldingLotsTableD from "./PortfolioHoldingLots/PortfolioHoldingLotsTableD"; + +type Props = { + labelId: string; + isItemSelected: boolean; + isAllRowsOpen: boolean; + toggleSelect: (event: MouseEvent, row: PortfolioHolding) => void; +} & PortfolioHolding; + +export default memo(function PortfolioHoldingRow({ + labelId, + isItemSelected, + isAllRowsOpen, + toggleSelect, + ...row +}: Props) { + const portfolioId = usePortfolioId(); + + const { + id, + companyName, + tickerSymbol, + currentValuation, + currentPrice, + averageCostPerShare, + numShares, + dailyChange, + dailyChangeRate, + totalGain, + totalReturnRate, + annualDividend, + annualDividendYield, + purchaseHistory, + } = row; + + const [isRowOpen, setIsRowOpen] = useState(false); + + const onExpandRowClick = useCallback( + (event: MouseEvent) => { + event.stopPropagation(); + setIsRowOpen((prev) => !prev); + }, + [] + ); + + // TODO: Reduce rendering (currently renders twice) + useEffect(() => { + setIsRowOpen(isAllRowsOpen); + }, [isAllRowsOpen]); + + return ( + <> + toggleSelect(event, row)} + aria-selected={isItemSelected}> + + + + + + + + + + + + + + + + + + + + + + + + ); +}); + +const MemoizedHoldingTableCell = memo( + ({ + isRowOpen, + onExpandRowClick, + }: { + isRowOpen: boolean; + onExpandRowClick: (event: MouseEvent) => void; + }) => ( + + + + ) +); + +const MemoizedCheckBoxCell = memo( + ({ + isItemSelected, + labelId, + }: { + isItemSelected: boolean; + labelId: string; + }) => ( + + + + ) +); + +const MemoizedCompanyInfoCell = memo( + ({ + companyName, + tickerSymbol, + }: { + companyName: string; + tickerSymbol: string; + }) => ( + + + + + {companyName} + + + + + + {tickerSymbol} + + + ) +); + +const MemoizedTableCell = memo( + ({ + value, + width, + align = "right", + }: { + value: number; + width: string; + align?: "left" | "right"; + }) => ( + + + + ) +); + +const MemoizedTableCellWithRateBadge = memo( + ({ value, rate, width }: { value: number; rate: number; width: string }) => ( + + +
+ +
+
+ ) +); + +const MemoizedAmountCell = memo( + ({ value, width }: { value: number; width: string }) => ( + + {thousandsDelimiter(value)} + + ) +); + +const MemoizedTextCell = memo( + ({ text, width }: { text: number; width: string }) => ( + + {text} + + ) +); + +const StyledHoldingTableRow = styled(TableRow)` + &.Mui-selected { + background-color: ${({ theme: { color } }) => color.neutral.gray50}; + border-bottom: 1px solid ${({ theme: { color } }) => color.neutral.white}; + } + + &.Mui-selected:hover { + background-color: ${({ theme: { color } }) => color.neutral.gray100}; + } + + &:hover { + background-color: ${({ theme: { color } }) => color.neutral.gray100}; + } + + & > * { + border-bottom: 1px solid ${({ theme: { color } }) => color.neutral.gray100}; + } +`; + +const HoldingTableCell = styled(TableCell)` + padding: 0px 8px; + height: 48px; + + > button { + padding: 0; + } +`; + +const HoldingTypography = styled(Typography)` + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; +`; + +const Amount = styled(HoldingTypography)` + display: inline; +`; + +const StyledHoldingLotRow = styled(TableRow)` + width: 856px; +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTable.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTable.tsx new file mode 100644 index 0000000..59ac165 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTable.tsx @@ -0,0 +1,25 @@ +import CollapsibleSelectableTable from "@/components/Table/CollapsibleSelectableTable"; +import { PortfolioHolding } from "@/features/portfolio/api/types"; +import EmptyPortfolioHoldingTable from "../EmptyPortfolioHoldingTable"; +import PortfolioHoldingTableBody from "./PortfolioHoldingTableBody"; +import PortfolioHoldingTableHead from "./PortfolioHoldingTableHead"; +import PortfolioHoldingTableToolBar from "./PortfolioHoldingTableToolBar"; + +type Props = { + data: PortfolioHolding[]; +}; + +export default function PortfolioHoldingTable({ data }: Props) { + PortfolioHoldingTableHead; + return ( + + ); +} diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableBody.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableBody.tsx new file mode 100644 index 0000000..307af9f --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableBody.tsx @@ -0,0 +1,58 @@ +import { useTableSelection } from "@/components/Table/hooks/useTableSelection"; +import { PortfolioHolding } from "@/features/portfolio/api/types"; +import { TableBody, TableCell, TableRow } from "@mui/material"; +import { memo } from "react"; +import PortfolioHoldingRow from "./PortfolioHoldingRow"; + +type Props = { + numEmptyRows: number; + visibleRows: readonly PortfolioHolding[]; + selected: readonly PortfolioHolding[]; + isAllRowsOpen: boolean; + updateSelected: (selected: readonly PortfolioHolding[]) => void; +}; + +export default memo(function PortfolioHoldingTableBody({ + numEmptyRows, + visibleRows, + selected, + isAllRowsOpen, + updateSelected, +}: Props) { + const { isSelected, toggleSelect } = useTableSelection({ + selected, + updateSelected, + }); + + return ( + + + + + + {visibleRows.map((row, index) => { + const isItemSelected = isSelected(row.id); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + + ); + })} + {numEmptyRows > 0 && ( + + + + )} + + ); +}); diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableHead.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableHead.tsx new file mode 100644 index 0000000..f71ae3b --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableHead.tsx @@ -0,0 +1,242 @@ +import { IconButton } from "@/components/Buttons/IconButton"; +import CheckBox from "@/components/Checkbox"; +import { Icon } from "@/components/Icon"; +import { Order } from "@/components/Table/types"; +import { CustomTooltip } from "@/components/Tooltips/CustomTooltip"; +import { PortfolioHolding } from "@/features/portfolio/api/types"; +import designSystem from "@/styles/designSystem"; +import { + Box, + TableCell, + TableHead, + TableRow, + TableSortLabel, +} from "@mui/material"; +import { visuallyHidden } from "@mui/utils"; +import { ChangeEvent, MouseEvent, memo } from "react"; +import styled from "styled-components"; + +type Props = { + order: Order; + // TODO: 임시로 해결하기 위해서 타입을 지정. + // 추후에 공용 테이블에서 orderBy type 수정필요 + orderBy: string | number | symbol; + isAllRowsSelectedInCurrentPage: boolean; + onRequestSort: ( + event: MouseEvent, + property: keyof PortfolioHolding + ) => void; + onSelectAllClick: (event: ChangeEvent) => void; + isAllRowsOpen: boolean; + onExpandOrCollapseAllRows: (event: MouseEvent) => void; +}; + +type HeadCell = { + id: keyof PortfolioHolding; + label: string; + numeric: boolean; + size: string; +}; + +const headCells: readonly HeadCell[] = [ + { + id: "companyName", + numeric: false, + label: "종목명", + size: "132px", + }, + { + id: "currentValuation", + numeric: true, + label: "평가 금액", + size: "108px", + }, + { + id: "currentPrice", + numeric: true, + label: "현재가", + size: "108px", + }, + { + id: "averageCostPerShare", + numeric: true, + label: "평균 매입가", + size: "108px", + }, + { + id: "numShares", + numeric: true, + label: "개수", + size: "64px", + }, + { + id: "dailyChangeRate", + numeric: true, + label: "변동률", + size: "80px", + }, + { + id: "totalGain", + numeric: true, + label: "총 손익", + size: "108px", + }, + { + id: "annualDividend", + numeric: true, + label: "연 배당금", + size: "140px", + }, +]; + +export default memo(function PortfolioHoldingTableHead({ + order, + orderBy, + isAllRowsSelectedInCurrentPage, + onRequestSort, + onSelectAllClick, + isAllRowsOpen, + onExpandOrCollapseAllRows, +}: Props) { + const createSortHandler = + (property: keyof PortfolioHolding) => (event: MouseEvent) => { + onRequestSort(event, property); + }; + + return ( + + + + onExpandOrCollapseAllRows(event)} + aria-label="포트폴리오 종목 테이블 모두 펼치기 버튼" + /> + + + + + + + {headCells.map((headCell, index) => ( + + { + const isOrderBy = orderBy === headCell.id; + + return isOrderBy ? ( + + ) : ( + + ); + }}> + {headCell.label === "연 배당금" ? ( + + + {headCell.label} + + + + ) : ( + <>{headCell.label} + )} + + {orderBy === headCell.id ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + + ))} + + + ); +}); + +const StyledTableHead = styled(TableHead)` + height: 48px; + width: 100%; + background-color: ${designSystem.color.neutral.gray50}; + border-radius: 8px; + + & .MuiTableCell-root { + border-bottom: none; + color: ${designSystem.color.neutral.gray600}; + ${designSystem.font.title5}; + } +`; + +const StyledTableRow = styled(TableRow)` + width: 100%; + background-color: #f6f6f8; + + & > last-child { + border-radius: 0 8px 8px 0; + padding: 0 16px 0 8px; + } +`; + +const StyledTableCell = styled(TableCell)` + height: 48px; + padding: 4px 8px; + + &:first-of-type { + padding-left: 16px; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + + > span { + width: auto; + } +`; + +const StyledTableSortLabel = styled(TableSortLabel)` + flex-direction: row; + gap: 4px; + color: ${designSystem.color.neutral.gray600}; +`; + +const StyledTooltipContainer = styled.div` + display: flex; + gap: 4px; +`; diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableToolBar.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableToolBar.tsx new file mode 100644 index 0000000..2c1c2d3 --- /dev/null +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingTableToolBar.tsx @@ -0,0 +1,120 @@ +import Button from "@/components/Buttons/Button"; +import { Icon } from "@/components/Icon"; +import { PortfolioHolding } from "@/features/portfolio/api/types"; +import designSystem from "@/styles/designSystem"; +import { useBoolean } from "@fineants/demolition"; +import { Toolbar, Typography } from "@mui/material"; +import { memo } from "react"; + +import usePortfolioHoldingDeleteMutation from "@/features/portfolio/api/queries/usePortfolioHoldingDeleteMutation"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import styled from "styled-components"; +import PortfolioHoldingAddDialog from "../PortfolioHoldingAddDialog"; +import PortfolioHoldingSelectedDeleteConfirm from "../PortfolioHoldingSelectedDeleteConfirm"; + +type Props = { + selected: readonly PortfolioHolding[]; + isAllDeleteOnLastPage: boolean; + updateSelected: (newSelected: readonly PortfolioHolding[]) => void; + moveToPrevTablePage: () => void; +}; + +export default memo(function PortfolioHoldingTableToolBar({ + selected, + isAllDeleteOnLastPage, + updateSelected, + moveToPrevTablePage, +}: Props) { + const portfolioId = usePortfolioId(); + + const { mutateAsync: portfolioHoldingDeleteMutateAsync } = + usePortfolioHoldingDeleteMutation(Number(portfolioId)); + + const { + state: isAddHoldingDialogOpen, + setTrue: onAddPortfolioButtonClick, + setFalse: onAddHoldingDialogClose, + } = useBoolean(); + const { + state: isConfirmOpen, + setTrue: onDeleteHoldingsButtonClick, + setFalse: onDeleteHoldingsAlertClose, + } = useBoolean(); + + const onConfirmAction = async () => { + const selectedHoldingIds = selected.map((item) => item.id); + await portfolioHoldingDeleteMutateAsync({ + portfolioId: Number(portfolioId), + body: { portfolioHoldingIds: selectedHoldingIds }, + }); + + updateSelected([]); + + if (isAllDeleteOnLastPage) { + moveToPrevTablePage(); + } + }; + + return ( + + + {selected.length > 0 && ( + <> + + {selected.length} + + 개 선택됨 + + + + + + + + )} + + + + + + + + + ); +}); + +const StyledToolbar = styled(Toolbar)` + height: 32px; + min-height: 32px; + padding: 0; + margin-bottom: 16px; + display: flex; + justify-content: space-between; +`; + +const SelectedInfoContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; From cf1354c2f37f03409ae0d0a068e76a11ba6bd174 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:35:58 +0900 Subject: [PATCH 18/39] =?UTF-8?q?#24=20feat:=20Portfolio=20Overview=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../desktop/PortfolioOverviewBodyD.tsx | 375 ++++++++++++++++++ .../desktop/PortfolioOverviewD.tsx | 206 ++++++++++ 2 files changed, 581 insertions(+) create mode 100644 src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx create mode 100644 src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx diff --git a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx new file mode 100644 index 0000000..abd2473 --- /dev/null +++ b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx @@ -0,0 +1,375 @@ +import RateBadge from "@/components/Badges/RateBadge"; +import { IconButton } from "@/components/Buttons/IconButton"; +import { Icon } from "@/components/Icon"; +import ConditionalTooltip from "@/components/Tooltips/ConditionalTooltip"; +import { CustomTooltip } from "@/components/Tooltips/CustomTooltip"; +import { PortfolioDetails } from "@/features/portfolio/api/types"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import designSystem from "@/styles/designSystem"; +import { thousandsDelimiter } from "@fineants/demolition"; +import { debounce } from "@mui/material"; +import { memo, useCallback } from "react"; +import styled from "styled-components"; +import RealtimeValue from "../../RealtimeValue"; +import { TargetGainToolTip } from "../TargetGainToolTip"; + +type Props = { + data: Omit< + PortfolioDetails, + "id" | "name" | "securitiesFirm" | "currentValuation" + >; +}; + +export default memo(function PortfolioOverviewBodyD({ data }: Props) { + const { + budget, + investedAmount, + balance, + provisionalLossBalance, + targetGain, + targetReturnRate, + targetGainNotify, + maximumLoss, + maximumLossRate, + maxLossNotify, + totalGain, + totalGainRate, + dailyGain, + dailyGainRate, + annualDividend, + annualDividendYield, + annualInvestmentDividendYield, + } = data; + + const portfolioId = usePortfolioId(); + + // TODO: 알림 추가하면서 추가하기 + // const { mutate } = usePortfolioNotificationSettingsMutation( + // Number(portfolioId) + // ); + + const onTargetGainNotifyButtonClick = useCallback( + debounce(() => { + // mutate({ + // notificationType: "targetGain", + // body: { isActive: !targetGainNotify }, + // }); + }, 250), + [] + ); + + const onMaxLossNotifyButtonClick = useCallback( + debounce(() => { + // mutate({ + // notificationType: "maxLoss", + // body: { isActive: !maxLossNotify }, + // }); + }, 250), + [] + ); + + return ( + + + + + + + ); +}); + +const BudgetSection = memo(function ({ + budget, + investedAmount, + balance, + provisionalLossBalance, +}: Pick< + PortfolioDetails, + "budget" | "investedAmount" | "balance" | "provisionalLossBalance" +>) { + return ( + + +
예산
+ {thousandsDelimiter(budget)} +
+ +
투자금액
+ {thousandsDelimiter(investedAmount)} +
+ +
잔고
+ {thousandsDelimiter(balance)} +
+ + + 손실 종목을 매도 시 현재 잔고의 감당력을 표현하는 것으로 매도 후 + 실제 잔고를 나타내는 것이 아닙니다 +
+ 잔고 - 손실 종목의 손실 합 +

+ }> +
+ 잠정 손실 잔고 +
+
+ {thousandsDelimiter(provisionalLossBalance)} +
+
+ ); +}); + +const TargetAndLossSection = memo(function ({ + targetGain, + targetReturnRate, + targetGainNotify, + maximumLoss, + maximumLossRate, + maxLossNotify, + onTargetGainNotifyButtonClick, + onMaxLossNotifyButtonClick, +}: Pick< + PortfolioDetails, + | "targetGain" + | "targetReturnRate" + | "targetGainNotify" + | "maximumLoss" + | "maximumLossRate" + | "maxLossNotify" +> & { + onTargetGainNotifyButtonClick: () => void; + onMaxLossNotifyButtonClick: () => void; +}) { + return ( + + + + 목표 수익률 + + + {targetGain === 0 ? "-" : thousandsDelimiter(targetGain)} + +
+ {targetGain === 0 ? ( + "-" + ) : ( + + )} +
+ + + 최대 손실률 + +
+ +
+
+
+ {maximumLoss === 0 ? "-" : thousandsDelimiter(maximumLoss)} +
+
+ {maximumLoss === 0 ? ( + "-" + ) : ( + + )} +
+
+ ); +}); + +const GainAndLossSection = memo(function ({ + totalGain, + totalGainRate, + dailyGain, + dailyGainRate, +}: Pick< + PortfolioDetails, + "totalGain" | "totalGainRate" | "dailyGain" | "dailyGainRate" +>) { + return ( + + +
총 손익
+ +
+
+ +
+ +
당일 손익
+ +
+
+ +
+
+ ); +}); + +const DividendSection = memo(function ({ + annualDividend, + annualDividendYield, + annualInvestmentDividendYield, +}: Pick< + PortfolioDetails, + "annualDividend" | "annualDividendYield" | "annualInvestmentDividendYield" +>) { + return ( + + +
총 연배당금
+ {thousandsDelimiter(annualDividend)} +
+
+ +
+ + + 총 투자금액 대비 연배당률 +
연 배당금 / 투자금액 * 100% +

+ }> +
+ 투자대비 연 배당률 + +
+
+ +
+
+ ); +}); + +const StyledOverviewBody = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 1px; + border: 1px solid ${designSystem.color.neutral.gray100}; + border-radius: 8px; + overflow: hidden; + + & > div { + display: flex; + flex-direction: column; + padding: 16px; + } + + & > div:nth-child(odd) { + border-right: 1px solid ${designSystem.color.neutral.gray100}; + } + + & > div:nth-child(-n + 2) { + border-bottom: 1px solid ${designSystem.color.neutral.gray100}; + } +`; + +const OverviewBodySection = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + width: 444px; + height: 140px; + padding: 16px; + + &:first-child { + border-right: 1px solid ${designSystem.color.neutral.gray100}; + } +`; + +const OverviewBodyData = styled.div` + height: 24px; + display: flex; + justify-content: space-between; + align-items: center; + + > span { + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + } +`; + +const NotificationLabel = styled.div` + display: flex; + align-items: center; + gap: 4px; +`; diff --git a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx new file mode 100644 index 0000000..fa3916f --- /dev/null +++ b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx @@ -0,0 +1,206 @@ +import LabelBadge from "@/components/Badges/LabelBadge"; +import Breadcrumb from "@/components/Breadcrumb"; +import Button from "@/components/Buttons/Button"; +import { Icon } from "@/components/Icon"; +import Routes from "@/constants/Routes"; +import { securitiesFirmLogos } from "@/constants/securitiesFirm"; +import { PortfolioDetails } from "@/features/portfolio/api/types"; +import designSystem from "@/styles/designSystem"; +import { thousandsDelimiter, useBoolean } from "@fineants/demolition"; +import { useRouter } from "next/router"; +import { memo } from "react"; +import styled from "styled-components"; +import PortfolioOverviewBodyD from "./PortfolioOverviewBodyD"; + +type Props = { + data: PortfolioDetails; +}; + +export default memo(function PortfolioOverviewD({ data }: Props) { + // const navigate = useNavigate(); + const router = useRouter(); + // const { portfolioId } = useParams(); + // const { mutate: portfolioDeleteMutate } = usePortfolioDeleteMutation(); + + const { id, name, securitiesFirm, currentValuation, ...overViewData } = data; + + const { + state: isDialogOpen, + setTrue: onPortfolioEdit, + setFalse: onDialogClose, + } = useBoolean(); + const { + state: isConfirmOpen, + setTrue: onPortfolioRemove, + setFalse: onConfirmAlertClose, + } = useBoolean(); + + const onConfirmAction = () => { + // portfolioDeleteMutate(Number(portfolioId)); + router.push(Routes.PORTFOLIOS); + }; + + return ( + +
+ + + + + + {/* {isDialogOpen && ( + + )} + {isConfirmOpen && ( + + )} */} + + ); +}); + +type HeaderProps = Pick & { + onPortfolioRemove: () => void; + onPortfolioEdit: () => void; +}; + +const Header = memo(function ({ + name, + id, + securitiesFirm, + onPortfolioRemove, + onPortfolioEdit, +}: HeaderProps) { + return ( + + + + + + {name} + + + + + + + + + ); +}); + +const CurrentValue = memo(function ({ + currentValuation, +}: Pick) { + return ( + +
평가금액
+ + ₩{thousandsDelimiter(currentValuation)} + +
+ ); +}); + +const StyledPortfolioOverview = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +`; + +const PortfolioOverviewHead = styled.div` + height: 73px; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const TitleContent = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const TitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const FirmImage = styled.img` + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; +`; + +const Title = styled.span` + ${designSystem.font.heading3}; +`; + +const ButtonsWrapper = styled.div` + display: flex; + gap: 8px; +`; + +const ValuationContainer = styled.div` + height: 64px; + padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; + background-color: ${designSystem.color.neutral.gray800}; + border-radius: 8px; + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray400}; +`; + +const CurrentValuation = styled.div` + display: flex; + gap: 4px; + align-items: center; + ${designSystem.font.title3}; + color: ${designSystem.color.neutral.gray600}; + + > span { + ${designSystem.font.title2}; + color: ${designSystem.color.neutral.white}; + } +`; From c66b6dc4fe036473e181f1b9222195a3a15fc3c4 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:36:27 +0900 Subject: [PATCH 19/39] =?UTF-8?q?#24=20feat:=20Skeletons=EC=99=80=20ErrorF?= =?UTF-8?q?allback=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Portfolio/skeletons/MainPanelSkeleton.tsx | 64 +++++++++++++++++++ .../errorFallback/MainPanelErrorFallback.tsx | 32 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/features/portfolio/components/Portfolio/skeletons/MainPanelSkeleton.tsx create mode 100644 src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx diff --git a/src/features/portfolio/components/Portfolio/skeletons/MainPanelSkeleton.tsx b/src/features/portfolio/components/Portfolio/skeletons/MainPanelSkeleton.tsx new file mode 100644 index 0000000..3e4c3a3 --- /dev/null +++ b/src/features/portfolio/components/Portfolio/skeletons/MainPanelSkeleton.tsx @@ -0,0 +1,64 @@ +import designSystem from "@/styles/designSystem"; +import { Box, Skeleton } from "@mui/material"; +import styled from "styled-components"; + +export default function MainPanelSkeleton() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +const StyledMainPanelSkeleton = styled.div` + width: 960px; + display: flex; + flex-direction: column; + gap: 40px; + padding: 32px; + background-color: #ffffff; + border-radius: 8px; +`; + +const PortfolioOverviewContainer = styled.div` + width: 100%; +`; + +const PortfolioHoldingsContainer = styled(Box)` + display: flex; + flex-direction: column; + gap: 16px; + width: 896px; +`; + +const StyledPortfolioOverview = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +`; + +const TitleContainer = styled.div` + height: 73px; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const StyledSkeleton = styled(Skeleton)` + background-color: ${designSystem.color.neutral.gray100}; +`; diff --git a/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx b/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx new file mode 100644 index 0000000..259d164 --- /dev/null +++ b/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx @@ -0,0 +1,32 @@ +import { ErrorFallbackContent } from "@/components/ErrorFallbackContent"; +import { FallbackProps } from "react-error-boundary"; +import styled from "styled-components"; + +export default function MainPanelErrorFallback({ + error, + resetErrorBoundary, +}: FallbackProps) { + return ( + + + + ); +} + +const StyledMainPanelErrorFallback = styled.div` + width: 960px; + height: 1060px; + padding: 48px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + position: relative; + border-radius: 8px; + background-color: ${({ theme: { color } }) => color.neutral.white}; + color: ${({ theme: { color } }) => color.neutral.gray900}; +`; From 42b1fc525fe167bdea7637b13c630a8204c812d1 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 15:37:36 +0900 Subject: [PATCH 20/39] =?UTF-8?q?#24=20feat:=20Portfolio=20page=EC=99=80?= =?UTF-8?q?=20MainPanel,=20ChartPanel=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Portfolio/ChartPanel.tsx | 88 +++++++++++++++++ .../components/Portfolio/MainPanel.tsx | 95 +++++++++++++++++++ .../Portfolio/desktop/MainPanelD.tsx | 51 ++++++++++ .../portfolioOverview/TargetGainToolTip.tsx | 38 ++++++++ src/pages/portfolio/[portfolioId].tsx | 89 +++++++++++++++++ 5 files changed, 361 insertions(+) create mode 100644 src/features/portfolio/components/Portfolio/ChartPanel.tsx create mode 100644 src/features/portfolio/components/Portfolio/MainPanel.tsx create mode 100644 src/features/portfolio/components/Portfolio/desktop/MainPanelD.tsx create mode 100644 src/features/portfolio/components/portfolioOverview/TargetGainToolTip.tsx create mode 100644 src/pages/portfolio/[portfolioId].tsx diff --git a/src/features/portfolio/components/Portfolio/ChartPanel.tsx b/src/features/portfolio/components/Portfolio/ChartPanel.tsx new file mode 100644 index 0000000..d2b0737 --- /dev/null +++ b/src/features/portfolio/components/Portfolio/ChartPanel.tsx @@ -0,0 +1,88 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import { chartColorPalette } from "@/styles/chartColorPalette"; +import designSystem from "@/styles/designSystem"; +import { useParams } from "next/navigation"; +import styled from "styled-components"; +import { PortfolioPageCharts } from "../../api/types"; +import { PortfolioPageTab } from "../../types"; +import DividendBarChartContainer from "../Chart/Dividend/DividendBarChartContainer"; +import { PieChartContainer } from "../Chart/PieChart/PieChartContainer"; +import SectorBarChartContainer from "../Chart/Sector/SectorBarChartContainer"; + +type Props = { + tab: PortfolioPageTab; + portfolioHoldingCharts: PortfolioPageCharts; + onChangeTab: (tab: PortfolioPageTab) => void; +}; + +export default function ChartsPanel({ + tab, + portfolioHoldingCharts, + onChangeTab, +}: Props) { + const { portfolioId } = useParams(); + + const { isMobile } = useResponsiveLayout(); + + // const { data: portfolioHoldingCharts } = usePortfolioHoldingChartsQuery( + // Number(portfolioId) + // ); + + const { name, securitiesFirm } = portfolioHoldingCharts.portfolioDetails; + + const { pieChart, dividendChart, sectorChart } = portfolioHoldingCharts; + + const coloredPieChart = pieChart.map((item, index) => ({ + ...item, + fill: chartColorPalette[index], + })); + + const pieChartLegendList = coloredPieChart.map((item) => ({ + title: item.name, + percent: item.weight, + color: item.fill, + })); + + const sectorLegendList = sectorChart.map((item, index) => ({ + title: item.sector, + percent: item.sectorWeight, + color: chartColorPalette[index], + })); + + return ( + <> + {/* {isMobile && ( + + )} */} + + + + + + + + + + ); +} + +const StyledChartsPanel = styled.div<{ $isMobile: boolean }>` + width: ${({ $isMobile }) => ($isMobile ? "100%" : "464px")}; + display: flex; + flex-direction: column; + gap: 48px; + padding: ${({ $isMobile }) => ($isMobile ? "32px 16px" : "32px")}; + border-radius: 8px; + background-color: ${designSystem.color.neutral.white}; +`; diff --git a/src/features/portfolio/components/Portfolio/MainPanel.tsx b/src/features/portfolio/components/Portfolio/MainPanel.tsx new file mode 100644 index 0000000..a1cc6b6 --- /dev/null +++ b/src/features/portfolio/components/Portfolio/MainPanel.tsx @@ -0,0 +1,95 @@ +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import { useState } from "react"; +import { Portfolio, PortfolioDetails, PortfolioHolding } from "../../api/types"; +import { PortfolioPageTab } from "../../types"; +import MainPanelD from "./desktop/MainPanelD"; + +type Props = { + tab: PortfolioPageTab; + portfolio: Portfolio; + onChangeTab: (tab: PortfolioPageTab) => void; +}; + +export default function MainPanel({ tab, portfolio, onChangeTab }: Props) { + // const { portfolioId } = useParams(); + + const { isDesktop, isMobile } = useResponsiveLayout(); + + // const { data: portfolio } = usePortfolioDetailsQuery(Number(portfolioId)); + + // const { + // data: portfolioSSE, + // //TODO: SSE 에러일때 핸들링처리 + // } = useSSE({ + // url: `/api/portfolio/${portfolioId}/holdings/realtime`, + // eventTypeName: "portfolioDetails", + // }); + + // // Static Data + // const { portfolioDetails, portfolioHoldings } = portfolio; + // // Realtime Data + // const { + // portfolioDetails: portfolioDetailsSSE, + // portfolioHoldings: portfolioHoldingsSSE, + // } = portfolioSSE ?? { portfolioDetails: null, portfolioHoldings: [] }; + + const [freshPortfolioDetailsData, setFreshPortfolioDetailsData] = + useState(portfolio.portfolioDetails); + + const [freshPortfolioHoldingsData, setFreshPortfolioHoldingsData] = useState< + PortfolioHolding[] + >(portfolio.portfolioHoldings); + + // useEffect(() => { + // setFreshPortfolioDetailsData({ + // ...portfolioDetails, + // ...portfolioDetailsSSE, + // }); + + // setFreshPortfolioHoldingsData( + // portfolioHoldings.map((holding, index) => ({ + // ...holding, + // ...portfolioHoldingsSSE[index], + // })) + // ); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [portfolioSSE]); + + // useEffect(() => { + // setFreshPortfolioDetailsData({ + // ...portfolioDetailsSSE, + // ...portfolioDetails, + // }); + + // setFreshPortfolioHoldingsData( + // portfolioHoldings.map((holding, index) => ({ + // ...portfolioHoldingsSSE[index], + // ...holding, + // })) + // ); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [portfolio]); + + // const hasNoHoldings = portfolioHoldings.length === 0; + const hasNoHoldings = false; + + return ( + <> + {isDesktop && ( + + )} + {/* {isMobile && ( + + )} */} + + ); +} diff --git a/src/features/portfolio/components/Portfolio/desktop/MainPanelD.tsx b/src/features/portfolio/components/Portfolio/desktop/MainPanelD.tsx new file mode 100644 index 0000000..5d40d8d --- /dev/null +++ b/src/features/portfolio/components/Portfolio/desktop/MainPanelD.tsx @@ -0,0 +1,51 @@ +import designSystem from "@/styles/designSystem"; +import { Box } from "@mui/material"; +import { memo } from "react"; +import styled from "styled-components"; +import { PortfolioDetails, PortfolioHolding } from "../../../api/types"; +import EmptyPortfolioHoldingTable from "../../PortfolioHolding/EmptyPortfolioHoldingTable"; +import PortfolioHoldingTable from "../../PortfolioHolding/desktop/PortfolioHoldingTable"; +import PortfolioOverviewD from "../../portfolioOverview/desktop/PortfolioOverviewD"; + +type Props = { + freshPortfolioDetailsData: PortfolioDetails; + freshPortfolioHoldingsData: PortfolioHolding[]; + hasNoHoldings: boolean; +}; + +export default memo(function MainPanelD({ + freshPortfolioDetailsData, + freshPortfolioHoldingsData, + hasNoHoldings, +}: Props) { + return ( + + + + {hasNoHoldings ? ( + + ) : ( + + + + )} + + ); +}); + +const StyledMainPanel = styled.div` + width: 960px; + display: flex; + flex-direction: column; + gap: 40px; + padding: 32px; + background-color: ${designSystem.color.neutral.white}; + border-radius: 8px; +`; + +const PortfolioHoldingsContainer = styled(Box)` + display: flex; + flex-direction: column; + gap: 16px; + width: 896px; +`; diff --git a/src/features/portfolio/components/portfolioOverview/TargetGainToolTip.tsx b/src/features/portfolio/components/portfolioOverview/TargetGainToolTip.tsx new file mode 100644 index 0000000..bab4af3 --- /dev/null +++ b/src/features/portfolio/components/portfolioOverview/TargetGainToolTip.tsx @@ -0,0 +1,38 @@ +import { IconButton } from "@/components/Buttons/IconButton"; +import ConditionalTooltip from "@/components/Tooltips/ConditionalTooltip"; + +type Props = { + targetGain: number; + targetGainNotify: boolean; + disabled: boolean; + onClick: () => void; +}; + +export function TargetGainToolTip({ + targetGain, + targetGainNotify, + disabled, + onClick, +}: Props) { + return ( + +
+ +
+
+ ); +} diff --git a/src/pages/portfolio/[portfolioId].tsx b/src/pages/portfolio/[portfolioId].tsx new file mode 100644 index 0000000..708383d --- /dev/null +++ b/src/pages/portfolio/[portfolioId].tsx @@ -0,0 +1,89 @@ +import BasePage from "@/components/BasePage"; +import { + getPortfolioCharts, + getPortfolioDetails, +} from "@/features/portfolio/api"; +import ChartsPanel from "@/features/portfolio/components/Portfolio/ChartPanel"; +import MainPanel from "@/features/portfolio/components/Portfolio/Mainpanel"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import { PortfolioPageTab } from "@/features/portfolio/types"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { useState } from "react"; +import styled from "styled-components"; + +export const getServerSideProps = async ( + context: GetServerSidePropsContext +) => { + const portfolioId = context.params?.portfolioId; + const cookies = context.req.cookies; + + const { data: portfolio } = await getPortfolioDetails( + Number(portfolioId), + cookies + ); + + const { data: portfolioHoldingCharts } = await getPortfolioCharts( + Number(portfolioId), + cookies + ); + + return { props: { portfolio, portfolioHoldingCharts } }; +}; + +export default function PortfolioPage({ + portfolio, + portfolioHoldingCharts, +}: InferGetServerSidePropsType) { + const { isMobile } = useResponsiveLayout(); + const portfolioId = usePortfolioId(); + + const [tab, setTab] = useState("portfolio"); + + const onChangeTab = (tab: PortfolioPageTab) => { + setTab(tab); + }; + + return ( + + + + + + + + + + + + ); +} + +const Container = styled.div<{ $isMobile: boolean }>` + width: 100%; + padding: ${({ $isMobile }) => ($isMobile ? "16px 0 32px 0px" : "40px 150px")}; + display: flex; + flex-direction: ${({ $isMobile }) => ($isMobile ? "column" : "row")}; + align-items: flex-start; + justify-content: center; + flex: 1; + gap: ${({ $isMobile }) => ($isMobile ? "0px" : "32px")}; +`; + +const PanelWrapper = styled.div<{ $isMobile: boolean; $isVisible: boolean }>` + width: ${({ $isMobile }) => ($isMobile ? "100%" : "auto")}; + display: ${({ $isVisible }) => ($isVisible ? "block" : "none")}; +`; From 9f5d0ed7e702d94a70c7fc3c0f55bbeeb29f8c6a Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 27 Nov 2024 19:07:34 +0900 Subject: [PATCH 21/39] =?UTF-8?q?#24=20refactor:=20Fetcher=EC=99=80=20Gene?= =?UTF-8?q?rator=EC=97=90=EC=84=9C=20delete=EA=B0=80=20body=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=9B=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EB=A5=BC=20=EA=B3=A0?= =?UTF-8?q?=EB=A0=A4=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/utils/api-router-generator-utils.ts | 2 +- src/api/fetcher.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/utils/api-router-generator-utils.ts b/scripts/utils/api-router-generator-utils.ts index df759d0..2702383 100644 --- a/scripts/utils/api-router-generator-utils.ts +++ b/scripts/utils/api-router-generator-utils.ts @@ -53,7 +53,7 @@ const createApiFiles = (methodDetails: MethodDetails[]) => { // Method 별 처리 const methodHandlers = methods .map((method) => { - const hasBody = ["POST", "PUT", "PATCH"].includes(method); + const hasBody = ["POST", "PUT", "PATCH", "DELETE"].includes(method); const bodyContent = hasBody ? ", req.body" : ""; return ` diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index b429ba0..34038c9 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -58,7 +58,7 @@ const addCookiesToHeaders = ( }; }; -// 데이터를 받지 않는 요청 (GET, DELETE) +// 데이터를 받지 않는 요청 (GET) const requestWithoutData = async ( url: string, method: string, @@ -135,7 +135,7 @@ const createFetcher = ( data?: Record | FormData, options?: FetcherOptions ): Promise> => - requestWithoutData(`${baseURL}${url}`, "DELETE", { + requestWithData(`${baseURL}${url}`, "DELETE", data, { ...defaultOptions, ...options, }), From 8487b77af1dc633363479083db51c0c6de06fc27 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:44:34 +0900 Subject: [PATCH 22/39] =?UTF-8?q?#24=20feat:=20Drawer=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20Transition=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SlideUpTransition.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/components/SlideUpTransition.tsx diff --git a/src/components/SlideUpTransition.tsx b/src/components/SlideUpTransition.tsx new file mode 100644 index 0000000..23cca87 --- /dev/null +++ b/src/components/SlideUpTransition.tsx @@ -0,0 +1,10 @@ +import { Slide } from "@mui/material"; +import { TransitionProps } from "@mui/material/transitions"; +import { forwardRef } from "react"; + +export default forwardRef(function Transition( + props: TransitionProps & { children: React.ReactElement }, + ref: React.Ref +) { + return ; +}); From 8a5233937a181f5c3fe4a82ebfaa468868e84431 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:46:03 +0900 Subject: [PATCH 23/39] =?UTF-8?q?#24=20refactor:=20Zustand=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=EC=97=85=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EB=AC=B8=EB=B2=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useZIndex.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/hooks/useZIndex.ts b/src/hooks/useZIndex.ts index b44a739..80007e4 100644 --- a/src/hooks/useZIndex.ts +++ b/src/hooks/useZIndex.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { create } from "zustand"; +import { useShallow } from "zustand/shallow"; type ZIndexState = { defaultZIndex: number; @@ -29,11 +30,13 @@ export const useZIndexStore = create((set, get) => ({ })); export const useZIndex = (isOpen: boolean = true) => { - const { pushStack, popStack, getCurrentZIndex } = useZIndexStore((state) => ({ - pushStack: state.pushStack, - popStack: state.popStack, - getCurrentZIndex: state.getCurrentZIndex, - })); + const [pushStack, popStack, getCurrentZIndex] = useZIndexStore( + useShallow((state) => [ + state.pushStack, + state.popStack, + state.getCurrentZIndex, + ]) + ); const [layoutIndex, setLayoutIndex] = useState(0); const zIndex = getCurrentZIndex(layoutIndex); @@ -43,7 +46,6 @@ export const useZIndex = (isOpen: boolean = true) => { const index = pushStack(); setLayoutIndex(index); } - return () => { if (isOpen) { popStack(); From bcfbc488e6792b003cfc1b383fc8ea6a11ab45ca Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:46:50 +0900 Subject: [PATCH 24/39] =?UTF-8?q?#24=20refactor:=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=EC=97=90=EC=84=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/portfolio/[portfolioId].tsx | 72 ++++++++++++--------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/src/pages/portfolio/[portfolioId].tsx b/src/pages/portfolio/[portfolioId].tsx index 708383d..9beca6e 100644 --- a/src/pages/portfolio/[portfolioId].tsx +++ b/src/pages/portfolio/[portfolioId].tsx @@ -1,40 +1,17 @@ +import { AsyncBoundary } from "@/components/AsyncBoundary"; import BasePage from "@/components/BasePage"; -import { - getPortfolioCharts, - getPortfolioDetails, -} from "@/features/portfolio/api"; -import ChartsPanel from "@/features/portfolio/components/Portfolio/ChartPanel"; -import MainPanel from "@/features/portfolio/components/Portfolio/Mainpanel"; +import ChartsPanelErrorFallback from "@/features/portfolio/components/Chart/errorFallback/ChartsPanelErrorFallback"; +import ChartsPanelSkeleton from "@/features/portfolio/components/Portfolio/skeletons/ChartsPanelSkeleton"; +import MainPanelSkeleton from "@/features/portfolio/components/Portfolio/skeletons/MainPanelSkeleton"; +import MainPanelErrorFallback from "@/features/portfolio/components/errorFallback/MainPanelErrorFallback"; import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; import { PortfolioPageTab } from "@/features/portfolio/types"; import useResponsiveLayout from "@/hooks/useResponsiveLayout"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import dynamic from "next/dynamic"; import { useState } from "react"; import styled from "styled-components"; -export const getServerSideProps = async ( - context: GetServerSidePropsContext -) => { - const portfolioId = context.params?.portfolioId; - const cookies = context.req.cookies; - - const { data: portfolio } = await getPortfolioDetails( - Number(portfolioId), - cookies - ); - - const { data: portfolioHoldingCharts } = await getPortfolioCharts( - Number(portfolioId), - cookies - ); - - return { props: { portfolio, portfolioHoldingCharts } }; -}; - -export default function PortfolioPage({ - portfolio, - portfolioHoldingCharts, -}: InferGetServerSidePropsType) { +export default function PortfolioPage() { const { isMobile } = useResponsiveLayout(); const portfolioId = usePortfolioId(); @@ -50,28 +27,41 @@ export default function PortfolioPage({ - + }> + + - + }> + + ); } +const MainPanel = dynamic( + import("@/features/portfolio/components/Portfolio/MainPanel"), + { + ssr: false, + } +); + +const ChartPanel = dynamic( + import("@/features/portfolio/components/Portfolio/ChartPanel"), + { + ssr: false, + } +); + const Container = styled.div<{ $isMobile: boolean }>` width: 100%; padding: ${({ $isMobile }) => ($isMobile ? "16px 0 32px 0px" : "40px 150px")}; From 388eff7a6f76af792e95128d2471ca747d45dc48 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:47:14 +0900 Subject: [PATCH 25/39] =?UTF-8?q?#24=20feat:=20Portfolio=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=8B=9C=EC=97=90=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=9C=A0=ED=8B=B8=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/portfolio/utils/calculations.ts | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/features/portfolio/utils/calculations.ts diff --git a/src/features/portfolio/utils/calculations.ts b/src/features/portfolio/utils/calculations.ts new file mode 100644 index 0000000..80156e9 --- /dev/null +++ b/src/features/portfolio/utils/calculations.ts @@ -0,0 +1,26 @@ +export const calculateRate = (val1: number, val2: number) => { + const rate = ((val1 - val2) / val2) * 100; + return rate; +}; + +export const calculateLossRate = (val1: number, val2: number) => { + const rate = ((val1 - val2) / val1) * 100; + return rate; +}; + +export const calculateValueFromRate = (rate: number, base: number) => { + const value = base + (rate / 100) * base; + return value; +}; + +export function applyDecimals(value: number, decimalPlaces: number = 2) { + if (value % 1 === 0) { + return value; + } else { + return parseFloat(value.toFixed(decimalPlaces)); + } +} + +export function removeNegativeSign(value: string) { + return value.replace("-", ""); +} From 680bdf1c4382eb0b3b548029d0d5a5c0ac74164d Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:49:01 +0900 Subject: [PATCH 26/39] =?UTF-8?q?#24=20refactor:=20Portfolio=20Dropdown?= =?UTF-8?q?=EC=97=90=20=ED=8F=AC=ED=8A=B8=ED=8F=B4=EB=A6=AC=EC=98=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20Dialog=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortfoliosDropdown/PortfoliosDropdown.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx b/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx index ce5a0e0..d2af29c 100644 --- a/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx +++ b/src/components/PortfoliosDropdown/PortfoliosDropdown.tsx @@ -1,4 +1,5 @@ import Routes from "@/constants/Routes"; +import PortfolioAddOrEditDialog from "@/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog"; import useUserQuery from "@/features/user/api/queries/useUserQuery"; import { useDropdown } from "@/hooks/useDropdown"; import designSystem, { parseFontString } from "@/styles/designSystem"; @@ -20,7 +21,11 @@ export function PortfoliosDropdown() { const { isOpen, onOpen, DropdownMenu, DropdownItem } = useDropdown(); - const { setTrue: portfolioDialogOpen } = useBoolean(); + const { + state: isPortfolioAddDialogOpen, + setTrue: portfolioDialogOpen, + setFalse: portfolioDialogClose, + } = useBoolean(); const onDropdownButtonClick = (e: MouseEvent) => { onOpen(e); @@ -63,12 +68,12 @@ export function PortfoliosDropdown() { - {/* {isPortfolioAddDialogOpen && ( - - )} */} + )} ); } From a762a7072230176625c2a20a7ba49cd544488854 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:49:43 +0900 Subject: [PATCH 27/39] =?UTF-8?q?#24=20feat:=20Portfolio=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20query=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/queries/usePortfolioAddMutation.ts | 32 +++++++++++++++++++ .../api/queries/usePortfolioDeleteMutation.ts | 23 +++++++++++++ .../api/queries/usePortfolioEditMutation.ts | 30 +++++++++++++++++ .../queries/usePortfolioHoldingChartsQuery.ts | 12 +++++++ 4 files changed, 97 insertions(+) create mode 100644 src/features/portfolio/api/queries/usePortfolioAddMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioDeleteMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioEditMutation.ts create mode 100644 src/features/portfolio/api/queries/usePortfolioHoldingChartsQuery.ts diff --git a/src/features/portfolio/api/queries/usePortfolioAddMutation.ts b/src/features/portfolio/api/queries/usePortfolioAddMutation.ts new file mode 100644 index 0000000..226c823 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioAddMutation.ts @@ -0,0 +1,32 @@ +import Routes from "@/constants/Routes"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useRouter } from "next/router"; +import { postPortfolio } from ".."; +import { portfolioKeys } from "./queryKeys"; + +type Props = { + onSuccessCb: () => void; +}; + +export default function usePortfolioAddMutation({ onSuccessCb }: Props) { + const router = useRouter(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: postPortfolio, + onSuccess: ({ data }) => { + onSuccessCb(); + + queryClient.invalidateQueries({ + queryKey: portfolioKeys.list.queryKey, + }); + + router.push(Routes.PORTFOLIO(data.portfolioId)); + }, + meta: { + toastSuccessMessage: "포트폴리오 추가를 성공했습니다", + toastErrorMessage: "포트폴리오 추가를 실패했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioDeleteMutation.ts b/src/features/portfolio/api/queries/usePortfolioDeleteMutation.ts new file mode 100644 index 0000000..1626112 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioDeleteMutation.ts @@ -0,0 +1,23 @@ +import Routes from "@/constants/Routes"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/router"; +import { deletePortfolio } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioDeleteMutation() { + const queryClient = useQueryClient(); + const router = useRouter(); + + return useMutation({ + mutationFn: deletePortfolio, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: portfolioKeys.list.queryKey, + }); + router.push(Routes.PORTFOLIOS); + }, + meta: { + toastSuccessMessage: "포트폴리오 삭제를 성공했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioEditMutation.ts b/src/features/portfolio/api/queries/usePortfolioEditMutation.ts new file mode 100644 index 0000000..ad4af7c --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioEditMutation.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { putPortfolio } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioEditMutation( + portfolioId: number, + onSuccessCb: () => void +) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: putPortfolio, + onSuccess: () => { + onSuccessCb(); + + queryClient.refetchQueries({ + queryKey: portfolioKeys.details(portfolioId).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: portfolioKeys.list.queryKey, + }); + queryClient.invalidateQueries({ + queryKey: portfolioKeys.charts(portfolioId).queryKey, + }); + }, + meta: { + toastSuccessMessage: "포트폴리오 수정을 성공했습니다", + toastErrorMessage: "포트폴리오 수정을 실패했습니다", + }, + }); +} diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingChartsQuery.ts b/src/features/portfolio/api/queries/usePortfolioHoldingChartsQuery.ts new file mode 100644 index 0000000..0cdd9c7 --- /dev/null +++ b/src/features/portfolio/api/queries/usePortfolioHoldingChartsQuery.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getPortfolioCharts } from ".."; +import { portfolioKeys } from "./queryKeys"; + +export default function usePortfolioHoldingChartsQuery(portfolioId: number) { + return useSuspenseQuery({ + queryKey: portfolioKeys.charts(portfolioId).queryKey, + queryFn: () => getPortfolioCharts(portfolioId), + retry: false, + select: (res) => res.data, + }); +} From 2b2957d9e83b9335786828fd160c0d93c33d78f4 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:50:13 +0900 Subject: [PATCH 28/39] =?UTF-8?q?#24=20refactor:=20query=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Delete=EC=9D=BC=20=EB=95=8C=20body=20=EA=B0=92=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/portfolio/api/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/portfolio/api/index.ts b/src/features/portfolio/api/index.ts index cabfd2d..07a0c2a 100644 --- a/src/features/portfolio/api/index.ts +++ b/src/features/portfolio/api/index.ts @@ -70,7 +70,7 @@ export const deletePortfolio = async (portfolioId: number) => { export const deletePortfolios = async (portfolioIds: number[]) => { const res = await fetcher.delete>("/portfolios", { - data: { portfolioIds }, + portfolioIds, }); return res.data; }; @@ -137,7 +137,7 @@ export const deletePortfolioHoldings = async ({ }) => { const res = await fetcher.delete>( `/portfolio/${portfolioId}/holdings`, - { data: body } + body ); return res.data; }; From ed6e1dc72b85093f6f940363a91333d7475fcf0c Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:50:55 +0900 Subject: [PATCH 29/39] =?UTF-8?q?#24=20feat:=20Portfolio=20Dialog=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EB=93=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortfolioAddOrEditDialog.tsx | 20 + .../desktop/PortfolioAddOrEditDialogD.tsx | 412 ++++++++++++++++++ .../usePortfolioAddOrEditDialogInputs.ts | 267 ++++++++++++ .../components/PortfolioDeleteConfirm.tsx | 25 ++ 4 files changed, 724 insertions(+) create mode 100644 src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx create mode 100644 src/features/portfolio/components/PortfolioAddOrEditDialog/desktop/PortfolioAddOrEditDialogD.tsx create mode 100644 src/features/portfolio/components/PortfolioAddOrEditDialog/hooks/usePortfolioAddOrEditDialogInputs.ts create mode 100644 src/features/portfolio/components/PortfolioDeleteConfirm.tsx diff --git a/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx b/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx new file mode 100644 index 0000000..3355209 --- /dev/null +++ b/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx @@ -0,0 +1,20 @@ +import { PortfolioDetails } from "@/features/portfolio/api/types"; +import useResponsiveLayout from "@/hooks/useResponsiveLayout"; +import PortfolioAddOrEditDialogD from "./desktop/PortfolioAddOrEditDialogD"; + +type Props = { + isOpen: boolean; + onClose: () => void; + portfolioDetails?: PortfolioDetails; +}; + +export default function PortfolioAddOrEditDialog(props: Props) { + const { isMobile, isDesktop } = useResponsiveLayout(); + + return ( + <> + {isDesktop && } + {/* {isMobile && } */} + + ); +} diff --git a/src/features/portfolio/components/PortfolioAddOrEditDialog/desktop/PortfolioAddOrEditDialogD.tsx b/src/features/portfolio/components/PortfolioAddOrEditDialog/desktop/PortfolioAddOrEditDialogD.tsx new file mode 100644 index 0000000..d111a85 --- /dev/null +++ b/src/features/portfolio/components/PortfolioAddOrEditDialog/desktop/PortfolioAddOrEditDialogD.tsx @@ -0,0 +1,412 @@ +import BaseDialog from "@/components/BaseDialog"; +import Button from "@/components/Buttons/Button"; +import { IconButton } from "@/components/Buttons/IconButton"; +import { Select, SelectOption } from "@/components/Select"; +import { TextField } from "@/components/TextField/TextField"; +import { + SECURITIES_FIRM, + SecuritiesFirm, + securitiesFirmLogos, +} from "@/constants/securitiesFirm"; +import usePortfolioAddMutation from "@/features/portfolio/api/queries/usePortfolioAddMutation"; +import usePortfolioEditMutation from "@/features/portfolio/api/queries/usePortfolioEditMutation"; +import { PortfolioDetails } from "@/features/portfolio/api/types"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; +import { applyDecimals } from "@/features/portfolio/utils/calculations"; +import designSystem from "@/styles/designSystem"; +import { + removeThousandsDelimiter, + thousandsDelimiter, + useText, +} from "@fineants/demolition"; +import { FormControl } from "@mui/material"; +import { FormEvent } from "react"; +import styled from "styled-components"; +import usePortfolioAddOrEditDialogInputs from "../hooks/usePortfolioAddOrEditDialogInputs"; + +type Props = { + isOpen: boolean; + onClose: () => void; + portfolioDetails?: PortfolioDetails; +}; + +export default function PortfolioAddOrEditDialogD({ + isOpen, + onClose, + portfolioDetails, +}: Props) { + const portfolioId = usePortfolioId(); + + const { mutate: addMutate } = usePortfolioAddMutation({ + onSuccessCb: onClose, + }); + + const { mutate: editMutate } = usePortfolioEditMutation( + Number(portfolioId), + onClose + ); + + // Securities Firm + const { value: securitiesFirm, onChange: onChangeSecuritiesFirm } = useText({ + initialValue: portfolioDetails + ? portfolioDetails.securitiesFirm + : "FineAnts", + }); + + // Portfolio Name + const { value: name, onChange: onNameChange } = useText({ + initialValue: portfolioDetails ? portfolioDetails.name : "", + }); + + // Number Inputs + const budgetInitialValue = portfolioDetails + ? thousandsDelimiter(portfolioDetails.budget) + : ""; + const targetGainInitialValue = portfolioDetails + ? thousandsDelimiter(portfolioDetails.targetGain) + : ""; + const targetReturnRateInitialValue = portfolioDetails + ? targetGainInitialValue === "" + ? "" + : thousandsDelimiter(applyDecimals(portfolioDetails.targetReturnRate)) + : ""; + const maximumLossInitialValue = portfolioDetails + ? thousandsDelimiter(portfolioDetails.maximumLoss) + : ""; + const maximumLossRateInitialValue = portfolioDetails + ? maximumLossInitialValue === "" + ? "" + : thousandsDelimiter(applyDecimals(portfolioDetails.maximumLossRate)) + : ""; + + const { + budget, + onBudgetChange, + budgetError, + isBudgetError, + targetGain, + onTargetGainChange: targetGainHandler, + targetGainError, + isTargetGainError, + targetReturnRate, + onTargetReturnRateChange: targetReturnRateHandler, + targetReturnRateError, + isTargetReturnRateError, + maximumLoss, + onMaximumLossChange: maximumLossHandler, + maximumLossError, + isMaximumLossError, + maximumLossRate, + onMaximumLossRateChange: maximumLossRateHandler, + maximumLossRateError, + isMaximumLossRateError, + isBudgetEmpty, + } = usePortfolioAddOrEditDialogInputs({ + budgetInitialValue, + targetGainInitialValue, + targetReturnRateInitialValue, + maximumLossInitialValue, + maximumLossRateInitialValue, + }); + + const isEditMode = !!portfolioDetails; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const body = { + name, + securitiesFirm: securitiesFirm as SecuritiesFirm, + budget: Number(removeThousandsDelimiter(budget)), + targetGain: Number(removeThousandsDelimiter(targetGain)), + maximumLoss: Number(removeThousandsDelimiter(maximumLoss)), + }; + + if (isEditMode) { + editMutate({ portfolioId: Number(portfolioId), body }); + } else { + addMutate(body); + } + }; + + const isFormValid = () => { + if ( + isTargetGainError || + isTargetReturnRateError || + isMaximumLossError || + isMaximumLossRateError + ) + return false; + + if (isEditMode) { + return ( + portfolioDetails?.securitiesFirm !== securitiesFirm || + portfolioDetails?.name !== name || + portfolioDetails?.budget !== Number(removeThousandsDelimiter(budget)) || + portfolioDetails?.targetGain !== + Number(removeThousandsDelimiter(targetGain)) || + portfolioDetails?.targetReturnRate !== + Number(removeThousandsDelimiter(targetReturnRate)) || + portfolioDetails?.maximumLoss !== + Number(removeThousandsDelimiter(maximumLoss)) || + portfolioDetails?.maximumLossRate !== + Number(removeThousandsDelimiter(maximumLossRate)) + ); + } + + if (!name) { + return false; + } + + return true; + }; + + return ( + +
+ +
포트폴리오 {isEditMode ? `수정` : `추가`}
+ +
+ + + + 이름 * + + + onNameChange(e.target.value)} + /> + + + + + 증권사 * + + + + + + + 예산 + onBudgetChange(e.target.value.trim())} + endAdornment={} + /> + + + 목표 수익률 + + targetReturnRateHandler(e.target.value.trim())} + endAdornment={%} + /> + targetGainHandler(e.target.value.trim())} + endAdornment={} + /> + + + + 최대 손실률 + + maximumLossRateHandler(e.target.value.trim())} + startAdornment={ + - + } + endAdornment={%} + /> + maximumLossHandler(e.target.value.trim())} + endAdornment={} + /> + + + + + + +
+
+ ); +} + +const PortfolioAddDialogStyle = { + height: "437px", + padding: "32px", +}; + +const Form = styled.form` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; + +const HeaderWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Header = styled.div` + ${designSystem.font.heading3}; + color: ${designSystem.color.neutral.gray800}; +`; + +const Body = styled.div` + margin-top: 32px; + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; + flex: 1; +`; + +const Row = styled.div` + width: auto; + display: flex; + gap: 8px; +`; + +const StyledSpan = styled.span` + width: 120px; + flex-shrink: 0; + ${designSystem.font.title5}; + color: ${designSystem.color.neutral.gray800}; + + > span { + color: ${designSystem.color.state.red500}; + } +`; + +const InputsWrapper = styled.div` + width: 100%; + display: flex; + gap: 8px; +`; + +const TextFieldEndAdornment = styled.span` + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray400}; +`; + +const StyledInput = styled.div` + display: flex; + min-width: auto; + flex: 1; + height: 32px; + box-sizing: border-box; + padding: 4px 8px; + border: 1px solid ${designSystem.color.neutral.gray300}; + border-radius: 3px; + + > span { + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray400}; + } +`; + +const Input = styled.input` + width: 100%; + height: 100%; + border: none; + outline: none; + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; + + &::placeholder { + color: ${designSystem.color.neutral.gray400}; + } +`; + +const ButtonWrapper = styled.div` + width: 100%; + margin-top: 24px; + display: flex; + justify-content: flex-end; +`; + +const SecuritiesFirmLogo = styled.img` + width: 24px; + height: 24px; +`; + +const SecuritiesFirmTitle = styled.span` + ${designSystem.font.body3}; + color: ${designSystem.color.neutral.gray900}; +`; diff --git a/src/features/portfolio/components/PortfolioAddOrEditDialog/hooks/usePortfolioAddOrEditDialogInputs.ts b/src/features/portfolio/components/PortfolioAddOrEditDialog/hooks/usePortfolioAddOrEditDialogInputs.ts new file mode 100644 index 0000000..aa9e6c8 --- /dev/null +++ b/src/features/portfolio/components/PortfolioAddOrEditDialog/hooks/usePortfolioAddOrEditDialogInputs.ts @@ -0,0 +1,267 @@ +import { + calculateLossRate, + calculateRate, + calculateValueFromRate, + removeNegativeSign, +} from "@/features/portfolio/utils/calculations"; +import { removeThousandsDelimiter, useNumber } from "@fineants/demolition"; +import { useCallback, useEffect } from "react"; + +type Props = { + budgetInitialValue: string; + targetGainInitialValue: string; + targetReturnRateInitialValue: string; + maximumLossInitialValue: string; + maximumLossRateInitialValue: string; +}; + +export default function usePortfolioAddOrEditDialogInputs({ + budgetInitialValue, + targetGainInitialValue, + targetReturnRateInitialValue, + maximumLossInitialValue, + maximumLossRateInitialValue, +}: Props) { + // Budget + const budgetValidator = (value: number) => { + if (value < 0) { + throw Error("0 이상이어야 합니다"); + } + }; + const { + value: budget, + onChange: onBudgetChange, + error: budgetError, + isError: isBudgetError, + } = useNumber({ + initialValue: + budgetInitialValue === "0" ? "" : budgetInitialValue?.toString(), + validators: [budgetValidator], + }); + + // Value and Rate Calculations + const calcNewValueBasedOnRate = useCallback( + (val: string) => { + return val === "" + ? val + : calculateValueFromRate( + Number(removeThousandsDelimiter(val)), + Number(removeThousandsDelimiter(budget)) + ); + }, + [budget] + ); + const calcNewTargetReturnRateBasedOnValue = useCallback( + (val: string) => { + return val === "" + ? val + : calculateRate( + Number(removeThousandsDelimiter(val)), + Number(removeThousandsDelimiter(budget)) + ); + }, + [budget] + ); + const calcNewMaxLossRateBasedOnValue = useCallback( + (val: string) => { + return val === "" + ? val + : calculateLossRate( + Number(removeThousandsDelimiter(budget)), + Number(removeThousandsDelimiter(val)) + ); + }, + [budget] + ); + + // Target Gain states + const targetGainValidator1 = (value: number) => { + if (value < 0) { + throw Error("0 이상이어야 합니다"); + } + }; + const targetGainValidator2 = (value: number) => { + if (value < Number(removeThousandsDelimiter(budget))) { + throw Error("예산 이상이어야 합니다"); + } + }; + const { + value: targetGain, + onChange: onTargetGainChange, + error: targetGainError, + isError: isTargetGainError, + } = useNumber({ + initialValue: + targetGainInitialValue === "0" ? "" : targetGainInitialValue?.toString(), + validators: [targetGainValidator1, targetGainValidator2], + }); + + const targetReturnRateValidator = (value: number) => { + if (value < 0) { + throw Error("0% 이상이어야 합니다"); + } + }; + const { + value: targetReturnRate, + onChange: onTargetReturnRateChange, + error: targetReturnRateError, + isError: isTargetReturnRateError, + } = useNumber({ + initialValue: + targetReturnRateInitialValue === "0" + ? "" + : targetReturnRateInitialValue?.toString(), + validators: [targetReturnRateValidator], + }); + + const targetGainHandler = useCallback( + (value: string) => { + onTargetGainChange(value); + + const newRate = calcNewTargetReturnRateBasedOnValue(value); + onTargetReturnRateChange(newRate.toString()); + }, + [ + calcNewTargetReturnRateBasedOnValue, + onTargetGainChange, + onTargetReturnRateChange, + ] + ); + const targetReturnRateHandler = useCallback( + (value: string) => { + onTargetReturnRateChange(value); + onTargetGainChange(calcNewValueBasedOnRate(value).toString()); + }, + [calcNewValueBasedOnRate, onTargetGainChange, onTargetReturnRateChange] + ); + + // Maximum Loss states + const maximumLossValidator1 = (value: number) => { + if (value < 0) { + throw Error("0 이상이어야 합니다"); + } + }; + const maximumLossValidator2 = (value: number) => { + if (value > Number(removeThousandsDelimiter(budget))) { + throw Error("예산을 초과할 수 없습니다"); + } + }; + const { + value: maximumLoss, + onChange: onMaximumLossChange, + error: maximumLossError, + isError: isMaximumLossError, + } = useNumber({ + initialValue: + maximumLossInitialValue === "0" + ? "" + : maximumLossInitialValue?.toString(), + validators: [maximumLossValidator1, maximumLossValidator2], + }); + + const maximumLossRateValidator1 = (value: number) => { + if (value > 100) { + throw Error("100% 이하이어야 합니다"); + } + }; + const { + value: maximumLossRate, + onChange: onMaximumLossRateChange, + error: maximumLossRateError, + isError: isMaximumLossRateError, + } = useNumber({ + initialValue: + maximumLossRateInitialValue === "0" + ? "" + : maximumLossRateInitialValue?.toString(), + validators: [maximumLossRateValidator1], + }); + + const maximumLossHandler = useCallback( + (value: string) => { + const isOnlyNegativeSign = value === "-"; + + onMaximumLossChange(isOnlyNegativeSign ? "" : value); + + const newRate = isOnlyNegativeSign + ? "" + : removeNegativeSign(calcNewMaxLossRateBasedOnValue(value).toString()); + onMaximumLossRateChange(newRate); + }, + [ + calcNewMaxLossRateBasedOnValue, + onMaximumLossChange, + onMaximumLossRateChange, + ] + ); + const maximumLossRateHandler = useCallback( + (value: string) => { + onMaximumLossRateChange(value); + onMaximumLossChange( + calcNewValueBasedOnRate(value === "" ? "" : `-${value}`).toString() + ); + }, + [calcNewValueBasedOnRate, onMaximumLossChange, onMaximumLossRateChange] + ); + + const clearInputs = useCallback(() => { + onTargetGainChange(""); + onTargetReturnRateChange(""); + onMaximumLossChange(""); + onMaximumLossRateChange(""); + }, [ + onMaximumLossChange, + onMaximumLossRateChange, + onTargetGainChange, + onTargetReturnRateChange, + ]); + + const isBudgetEmpty = budget === "0" || budget === ""; + + // 예산이 변경되었을 때 + useEffect(() => { + if (isBudgetEmpty) { + clearInputs(); + } else { + if (targetReturnRate) { + onTargetGainChange( + calcNewValueBasedOnRate(targetReturnRate).toString() + ); + } + if (maximumLossRate) { + onMaximumLossChange( + removeNegativeSign( + calcNewValueBasedOnRate( + maximumLossRate === "" ? "" : `-${maximumLossRate}` + ).toString() + ) + ); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [budget, isBudgetEmpty]); + + return { + budget, + onBudgetChange, + budgetError, + isBudgetError, + targetGain, + onTargetGainChange: targetGainHandler, + targetGainError, + isTargetGainError, + targetReturnRate, + onTargetReturnRateChange: targetReturnRateHandler, + targetReturnRateError, + isTargetReturnRateError, + maximumLoss, + onMaximumLossChange: maximumLossHandler, + maximumLossError, + isMaximumLossError, + maximumLossRate, + onMaximumLossRateChange: maximumLossRateHandler, + maximumLossRateError, + isMaximumLossRateError, + isBudgetEmpty, + }; +} diff --git a/src/features/portfolio/components/PortfolioDeleteConfirm.tsx b/src/features/portfolio/components/PortfolioDeleteConfirm.tsx new file mode 100644 index 0000000..2659242 --- /dev/null +++ b/src/features/portfolio/components/PortfolioDeleteConfirm.tsx @@ -0,0 +1,25 @@ +import ConfirmAlert from "@/components/ConfirmAlert"; + +type Props = { + isOpen: boolean; + portfolioName: string; + onClose: () => void; + onConfirm: () => void; +}; + +export default function PortfolioDeleteConfirm({ + isOpen, + portfolioName, + onClose, + onConfirm, +}: Props) { + return ( + + '{portfolioName}' 포트폴리오를 삭제하시겠습니까? + + ); +} From 06703d66896f5a982f56059138a08ed9ace2ce10 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:51:21 +0900 Subject: [PATCH 30/39] =?UTF-8?q?#24=20feat:=20Portfolio=20Page=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20Skeleton,=20errorFallback=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChartsPanelErrorFallback.tsx | 33 +++++++++++ .../skeletons/DividendBarChartSkeleton.tsx | 51 ++++++++++++++++ .../skeletons/HoldingsPieChartSkeleton.tsx | 18 ++++++ .../skeletons/ChartsPanelSkeleton.tsx | 59 +++++++++++++++++++ .../errorFallback/MainPanelErrorFallback.tsx | 5 +- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/features/portfolio/components/Chart/errorFallback/ChartsPanelErrorFallback.tsx create mode 100644 src/features/portfolio/components/Chart/skeletons/DividendBarChartSkeleton.tsx create mode 100644 src/features/portfolio/components/Chart/skeletons/HoldingsPieChartSkeleton.tsx create mode 100644 src/features/portfolio/components/Portfolio/skeletons/ChartsPanelSkeleton.tsx diff --git a/src/features/portfolio/components/Chart/errorFallback/ChartsPanelErrorFallback.tsx b/src/features/portfolio/components/Chart/errorFallback/ChartsPanelErrorFallback.tsx new file mode 100644 index 0000000..6bf7265 --- /dev/null +++ b/src/features/portfolio/components/Chart/errorFallback/ChartsPanelErrorFallback.tsx @@ -0,0 +1,33 @@ +import { ErrorFallbackContent } from "@/components/ErrorFallbackContent"; +import designSystem from "@/styles/designSystem"; +import { FallbackProps } from "react-error-boundary"; +import styled from "styled-components"; + +export default function ChartsPanelErrorFallback({ + error, + resetErrorBoundary, +}: FallbackProps) { + return ( + + + + ); +} + +const StyledChartsPanelErrorFallback = styled.div` + width: 464px; + height: 1060px; + padding: 48px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + border-radius: 8px; + position: relative; + background-color: ${designSystem.color.neutral.white}; + color: ${designSystem.color.neutral.gray900}; +`; diff --git a/src/features/portfolio/components/Chart/skeletons/DividendBarChartSkeleton.tsx b/src/features/portfolio/components/Chart/skeletons/DividendBarChartSkeleton.tsx new file mode 100644 index 0000000..f8ecc91 --- /dev/null +++ b/src/features/portfolio/components/Chart/skeletons/DividendBarChartSkeleton.tsx @@ -0,0 +1,51 @@ +import designSystem from "@/styles/designSystem"; +import { Skeleton } from "@mui/material"; +import styled from "styled-components"; + +export default function DividendBarChartSkeleton() { + return ( + + + {Array.from({ length: 12 }).map((_, index) => ( + + ))} + + + + {Array.from({ length: 12 }).map((_, index) => ( + + ))} + +
+ + ); +} + +const StyledDividendBarChartSkeleton = styled.div` + width: 400px; + height: 234px; + display: flex; + flex-direction: column; + background-color: ${designSystem.color.neutral.white}; + padding: 0 5px; +`; + +const BarsContainer = styled.div` + display: flex; + justify-content: space-around; + padding: 6px; + margin-bottom: 2px; +`; + +const MonthContainer = styled.div` + display: flex; + justify-content: space-around; + padding-top: 10px; +`; + +const BarSkeleton = styled(Skeleton)` + width: 16px; + height: 184px; + border-radius: 4px; + background-color: ${designSystem.color.primary.blue50}; +`; diff --git a/src/features/portfolio/components/Chart/skeletons/HoldingsPieChartSkeleton.tsx b/src/features/portfolio/components/Chart/skeletons/HoldingsPieChartSkeleton.tsx new file mode 100644 index 0000000..d1e631c --- /dev/null +++ b/src/features/portfolio/components/Chart/skeletons/HoldingsPieChartSkeleton.tsx @@ -0,0 +1,18 @@ +import { PieChartSkeleton } from "@/components/PieChart/skeletons/PieChartSkeleton"; +import styled from "styled-components"; + +export default function HoldingsPieChartSkeleton() { + return ( + + + + ); +} + +const PieChartWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 256px; + height: 256px; +`; diff --git a/src/features/portfolio/components/Portfolio/skeletons/ChartsPanelSkeleton.tsx b/src/features/portfolio/components/Portfolio/skeletons/ChartsPanelSkeleton.tsx new file mode 100644 index 0000000..4018a24 --- /dev/null +++ b/src/features/portfolio/components/Portfolio/skeletons/ChartsPanelSkeleton.tsx @@ -0,0 +1,59 @@ +import { WideLegendSkeleton } from "@/components/Legend/skeletons/WideLegendSkeleton"; + +import designSystem from "@/styles/designSystem"; +import { Skeleton } from "@mui/material"; +import styled from "styled-components"; +import DividendBarChartSkeleton from "../../Chart/skeletons/DividendBarChartSkeleton"; +import HoldingsPieChartSkeleton from "../../Chart/skeletons/HoldingsPieChartSkeleton"; + +export default function ChartsPanelSkeleton() { + return ( + + + 종목 구성 + + + + + + + 예상 월 배당금 + + + + 섹터 구성 + + + + + ); +} + +const StyledChartsPanelSkeleton = styled.div` + display: flex; + flex-direction: column; + gap: 48px; + width: 464px; + height: 1061px; + padding: 32px; + background-color: ${designSystem.color.neutral.white}; +`; + +const ChartLabel = styled.h1` + margin-right: auto; + ${designSystem.font.heading3}; +`; + +const SkeletonContainer = styled.div` + display: flex; + align-items: center; + flex-direction: column; + gap: 24px; +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +`; diff --git a/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx b/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx index 259d164..51a0dfd 100644 --- a/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx +++ b/src/features/portfolio/components/errorFallback/MainPanelErrorFallback.tsx @@ -1,4 +1,5 @@ import { ErrorFallbackContent } from "@/components/ErrorFallbackContent"; +import designSystem from "@/styles/designSystem"; import { FallbackProps } from "react-error-boundary"; import styled from "styled-components"; @@ -27,6 +28,6 @@ const StyledMainPanelErrorFallback = styled.div` gap: 24px; position: relative; border-radius: 8px; - background-color: ${({ theme: { color } }) => color.neutral.white}; - color: ${({ theme: { color } }) => color.neutral.gray900}; + background-color: ${designSystem.color.neutral.white}; + color: ${designSystem.color.neutral.gray900}; `; From 2e619cb16594209b45e64ec20ee4083001ae3285 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:51:51 +0900 Subject: [PATCH 31/39] =?UTF-8?q?#24=20refactor:=20Dialog=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC=20=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../desktop/PortfolioOverviewD.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx index fa3916f..317633d 100644 --- a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx +++ b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx @@ -4,12 +4,15 @@ import Button from "@/components/Buttons/Button"; import { Icon } from "@/components/Icon"; import Routes from "@/constants/Routes"; import { securitiesFirmLogos } from "@/constants/securitiesFirm"; +import usePortfolioDeleteMutation from "@/features/portfolio/api/queries/usePortfolioDeleteMutation"; import { PortfolioDetails } from "@/features/portfolio/api/types"; +import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; import designSystem from "@/styles/designSystem"; import { thousandsDelimiter, useBoolean } from "@fineants/demolition"; -import { useRouter } from "next/router"; import { memo } from "react"; import styled from "styled-components"; +import PortfolioAddOrEditDialog from "../../PortfolioAddOrEditDialog/PortfolioAddOrEditDialog"; +import PortfolioDeleteConfirm from "../../PortfolioDeleteConfirm"; import PortfolioOverviewBodyD from "./PortfolioOverviewBodyD"; type Props = { @@ -17,10 +20,9 @@ type Props = { }; export default memo(function PortfolioOverviewD({ data }: Props) { - // const navigate = useNavigate(); - const router = useRouter(); - // const { portfolioId } = useParams(); - // const { mutate: portfolioDeleteMutate } = usePortfolioDeleteMutation(); + const portfolioId = usePortfolioId(); + + const { mutate: portfolioDeleteMutate } = usePortfolioDeleteMutation(); const { id, name, securitiesFirm, currentValuation, ...overViewData } = data; @@ -36,8 +38,7 @@ export default memo(function PortfolioOverviewD({ data }: Props) { } = useBoolean(); const onConfirmAction = () => { - // portfolioDeleteMutate(Number(portfolioId)); - router.push(Routes.PORTFOLIOS); + portfolioDeleteMutate(Number(portfolioId)); }; return ( @@ -54,13 +55,14 @@ export default memo(function PortfolioOverviewD({ data }: Props) { - {/* {isDialogOpen && ( - )} + {isConfirmOpen && ( - )} */} + )} ); }); From 34bbc5608439bcc00a9d9c75aeefa6cf61c0661f Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:52:11 +0900 Subject: [PATCH 32/39] =?UTF-8?q?#24=20refactor:=20Theme=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=EB=90=98=EB=8D=98=20color=20DesignSystem?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortfolioHolding/desktop/PortfolioHoldingRow.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx index 04bcaf5..b602a7a 100644 --- a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx @@ -245,20 +245,20 @@ const MemoizedTextCell = memo( const StyledHoldingTableRow = styled(TableRow)` &.Mui-selected { - background-color: ${({ theme: { color } }) => color.neutral.gray50}; - border-bottom: 1px solid ${({ theme: { color } }) => color.neutral.white}; + background-color: ${designSystem.color.neutral.gray50}; + border-bottom: 1px solid ${designSystem.color.neutral.white}; } &.Mui-selected:hover { - background-color: ${({ theme: { color } }) => color.neutral.gray100}; + background-color: ${designSystem.color.neutral.gray100}; } &:hover { - background-color: ${({ theme: { color } }) => color.neutral.gray100}; + background-color: ${designSystem.color.neutral.gray100}; } & > * { - border-bottom: 1px solid ${({ theme: { color } }) => color.neutral.gray100}; + border-bottom: 1px solid ${designSystem.color.neutral.gray100}; } `; From 8555a9d6a6aee896b26e27a7607e6b2e68d766ba Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 21:52:34 +0900 Subject: [PATCH 33/39] =?UTF-8?q?#24=20refactor:=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=EC=9C=BC=EB=A1=9C=20=EB=B0=9B?= =?UTF-8?q?=EA=B3=A0=20=EC=9E=88=EB=8D=98=20data=20=EA=B0=81=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=ED=81=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Portfolio/ChartPanel.tsx | 15 +++---- .../components/Portfolio/MainPanel.tsx | 42 ++++++++++--------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/features/portfolio/components/Portfolio/ChartPanel.tsx b/src/features/portfolio/components/Portfolio/ChartPanel.tsx index d2b0737..3a1248e 100644 --- a/src/features/portfolio/components/Portfolio/ChartPanel.tsx +++ b/src/features/portfolio/components/Portfolio/ChartPanel.tsx @@ -3,7 +3,7 @@ import { chartColorPalette } from "@/styles/chartColorPalette"; import designSystem from "@/styles/designSystem"; import { useParams } from "next/navigation"; import styled from "styled-components"; -import { PortfolioPageCharts } from "../../api/types"; +import usePortfolioHoldingChartsQuery from "../../api/queries/usePortfolioHoldingChartsQuery"; import { PortfolioPageTab } from "../../types"; import DividendBarChartContainer from "../Chart/Dividend/DividendBarChartContainer"; import { PieChartContainer } from "../Chart/PieChart/PieChartContainer"; @@ -11,22 +11,17 @@ import SectorBarChartContainer from "../Chart/Sector/SectorBarChartContainer"; type Props = { tab: PortfolioPageTab; - portfolioHoldingCharts: PortfolioPageCharts; onChangeTab: (tab: PortfolioPageTab) => void; }; -export default function ChartsPanel({ - tab, - portfolioHoldingCharts, - onChangeTab, -}: Props) { +export default function ChartsPanel({ tab, onChangeTab }: Props) { const { portfolioId } = useParams(); const { isMobile } = useResponsiveLayout(); - // const { data: portfolioHoldingCharts } = usePortfolioHoldingChartsQuery( - // Number(portfolioId) - // ); + const { data: portfolioHoldingCharts } = usePortfolioHoldingChartsQuery( + Number(portfolioId) + ); const { name, securitiesFirm } = portfolioHoldingCharts.portfolioDetails; diff --git a/src/features/portfolio/components/Portfolio/MainPanel.tsx b/src/features/portfolio/components/Portfolio/MainPanel.tsx index a1cc6b6..3599b46 100644 --- a/src/features/portfolio/components/Portfolio/MainPanel.tsx +++ b/src/features/portfolio/components/Portfolio/MainPanel.tsx @@ -1,21 +1,22 @@ import useResponsiveLayout from "@/hooks/useResponsiveLayout"; -import { useState } from "react"; -import { Portfolio, PortfolioDetails, PortfolioHolding } from "../../api/types"; +import { useEffect, useState } from "react"; +import usePortfolioDetailsQuery from "../../api/queries/usePortfolioDetailsQuery"; +import { PortfolioDetails, PortfolioHolding } from "../../api/types"; +import { usePortfolioId } from "../../hook/usePortfolioId"; import { PortfolioPageTab } from "../../types"; import MainPanelD from "./desktop/MainPanelD"; type Props = { tab: PortfolioPageTab; - portfolio: Portfolio; onChangeTab: (tab: PortfolioPageTab) => void; }; -export default function MainPanel({ tab, portfolio, onChangeTab }: Props) { - // const { portfolioId } = useParams(); +export default function MainPanel({ tab, onChangeTab }: Props) { + const portfolioId = usePortfolioId(); const { isDesktop, isMobile } = useResponsiveLayout(); - // const { data: portfolio } = usePortfolioDetailsQuery(Number(portfolioId)); + const { data: portfolio } = usePortfolioDetailsQuery(Number(portfolioId)); // const { // data: portfolioSSE, @@ -55,24 +56,25 @@ export default function MainPanel({ tab, portfolio, onChangeTab }: Props) { // // eslint-disable-next-line react-hooks/exhaustive-deps // }, [portfolioSSE]); - // useEffect(() => { - // setFreshPortfolioDetailsData({ - // ...portfolioDetailsSSE, - // ...portfolioDetails, - // }); + useEffect(() => { + setFreshPortfolioDetailsData(portfolio.portfolioDetails); + setFreshPortfolioHoldingsData(portfolio.portfolioHoldings); + // setFreshPortfolioDetailsData({ + // ...portfolioDetailsSSE, + // ...portfolioDetails, + // }); - // setFreshPortfolioHoldingsData( - // portfolioHoldings.map((holding, index) => ({ - // ...portfolioHoldingsSSE[index], - // ...holding, - // })) - // ); - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [portfolio]); + // setFreshPortfolioHoldingsData( + // portfolioHoldings.map((holding, index) => ({ + // ...portfolioHoldingsSSE[index], + // ...holding, + // })) + // ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [portfolio]); // const hasNoHoldings = portfolioHoldings.length === 0; const hasNoHoldings = false; - return ( <> {isDesktop && ( From 05d7b9a5fa4a17a9fdb72be4ed834206f7bbf409 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:27:01 +0900 Subject: [PATCH 34/39] =?UTF-8?q?#24=20refactor:=20Set,=20Map=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20compilerOptions=EC=97=90=20downlevelIteration=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index fb68dc1..6b3b087 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "downlevelIteration": true, "paths": { "@/*": ["./src/*"] } From c105810d36d1d2262d1dd053e5187294605287e6 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:27:53 +0900 Subject: [PATCH 35/39] =?UTF-8?q?#24=20refactor:=20TablePagination=20impor?= =?UTF-8?q?t=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Table/SelectableTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Table/SelectableTable.tsx b/src/components/Table/SelectableTable.tsx index 7f68c92..068e3b4 100644 --- a/src/components/Table/SelectableTable.tsx +++ b/src/components/Table/SelectableTable.tsx @@ -11,6 +11,7 @@ import { useMemo, useState, } from "react"; +import TablePagination from "../Pagination/TablePagination"; import { Order } from "./types"; import { getComparator } from "./utils/comparator"; From 2f2c7676eb4d8b10728e24c60fecd03ac0b66bec Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:28:33 +0900 Subject: [PATCH 36/39] =?UTF-8?q?#24=20refactor:=20Cookies=20type=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/api/apiRoutes.ts | 2 +- src/features/portfolio/api/index.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/features/auth/api/apiRoutes.ts b/src/features/auth/api/apiRoutes.ts index 62ca61b..5f74c8c 100644 --- a/src/features/auth/api/apiRoutes.ts +++ b/src/features/auth/api/apiRoutes.ts @@ -1,7 +1,7 @@ import { clientFetcher } from "@/api/fetcher"; import { Response } from "@/api/types"; -export const getAuthStatus = async (cookies?: {}) => { +export const getAuthStatus = async (cookies?: Record) => { const res = await clientFetcher.get>("/authStatus", { cookies, }); diff --git a/src/features/portfolio/api/index.ts b/src/features/portfolio/api/index.ts index 07a0c2a..63093e2 100644 --- a/src/features/portfolio/api/index.ts +++ b/src/features/portfolio/api/index.ts @@ -20,7 +20,10 @@ export const getPortfoliosNameList = async () => { return res.data; }; -export const getPortfolioCharts = async (portfolioId: number, cookies?: {}) => { +export const getPortfolioCharts = async ( + portfolioId: number, + cookies?: Record +) => { const res = await fetcher.get>( `/portfolio/${portfolioId}/charts`, { cookies } @@ -30,7 +33,7 @@ export const getPortfolioCharts = async (portfolioId: number, cookies?: {}) => { export const getPortfolioDetails = async ( portfolioId: number, - cookies?: {} + cookies?: Record ) => { const res = await fetcher.get>( `/portfolio/${portfolioId}/holdings`, From aaea6ed6d866f285a55a741f4e86029912cdb470 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:28:50 +0900 Subject: [PATCH 37/39] =?UTF-8?q?#24=20refactor:=20=EC=95=84=EC=A7=81=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20Err?= =?UTF-8?q?or=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portfolio/api/queries/usePortfolioHoldingAddMutation.ts | 2 +- .../api/queries/usePortfolioHoldingPurchaseAddMutation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts index 9fb00e2..025b274 100644 --- a/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts +++ b/src/features/portfolio/api/queries/usePortfolioHoldingAddMutation.ts @@ -24,7 +24,7 @@ export default function usePortfolioHoldingAddMutation({ }); onClose(); }, - onError: (error) => { + onError: () => { //TODO : toast 추가 // const message = (error as AxiosError>).response?.data // ?.message as string; diff --git a/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts index fff4b06..fdf0617 100644 --- a/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts +++ b/src/features/portfolio/api/queries/usePortfolioHoldingPurchaseAddMutation.ts @@ -17,7 +17,7 @@ export default function usePortfolioHoldingPurchaseAddMutation( queryKey: portfolioKeys.charts(portfolioId).queryKey, }); }, - onError: (error) => { + onError: () => { //TODO toast 추가 필요 // const message = (error as AxiosError>).response?.data // ?.message as string; From a4a45a0c85d406dae4f7391d93c97883695682c9 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:29:23 +0900 Subject: [PATCH 38/39] =?UTF-8?q?#24=20refactor:=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EA=B5=AC=ED=98=84=20=EC=9D=B4=EC=A0=84=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B0=92=EB=93=A4=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portfolio/components/Portfolio/ChartPanel.tsx | 14 +++++++------- .../portfolio/components/Portfolio/MainPanel.tsx | 14 +++++++------- .../PortfolioAddOrEditDialog.tsx | 2 +- .../PortfolioHolding/PortfolioHoldingAddDialog.tsx | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/features/portfolio/components/Portfolio/ChartPanel.tsx b/src/features/portfolio/components/Portfolio/ChartPanel.tsx index 3a1248e..041171b 100644 --- a/src/features/portfolio/components/Portfolio/ChartPanel.tsx +++ b/src/features/portfolio/components/Portfolio/ChartPanel.tsx @@ -4,17 +4,17 @@ import designSystem from "@/styles/designSystem"; import { useParams } from "next/navigation"; import styled from "styled-components"; import usePortfolioHoldingChartsQuery from "../../api/queries/usePortfolioHoldingChartsQuery"; -import { PortfolioPageTab } from "../../types"; import DividendBarChartContainer from "../Chart/Dividend/DividendBarChartContainer"; import { PieChartContainer } from "../Chart/PieChart/PieChartContainer"; import SectorBarChartContainer from "../Chart/Sector/SectorBarChartContainer"; -type Props = { - tab: PortfolioPageTab; - onChangeTab: (tab: PortfolioPageTab) => void; -}; +// type Props = { +// tab: PortfolioPageTab; +// onChangeTab: (tab: PortfolioPageTab) => void; +// }; -export default function ChartsPanel({ tab, onChangeTab }: Props) { +// export default function ChartsPanel({ tab, onChangeTab }: Props) { +export default function ChartsPanel() { const { portfolioId } = useParams(); const { isMobile } = useResponsiveLayout(); @@ -23,7 +23,7 @@ export default function ChartsPanel({ tab, onChangeTab }: Props) { Number(portfolioId) ); - const { name, securitiesFirm } = portfolioHoldingCharts.portfolioDetails; + // const { name, securitiesFirm } = portfolioHoldingCharts.portfolioDetails; const { pieChart, dividendChart, sectorChart } = portfolioHoldingCharts; diff --git a/src/features/portfolio/components/Portfolio/MainPanel.tsx b/src/features/portfolio/components/Portfolio/MainPanel.tsx index 3599b46..e451c29 100644 --- a/src/features/portfolio/components/Portfolio/MainPanel.tsx +++ b/src/features/portfolio/components/Portfolio/MainPanel.tsx @@ -3,18 +3,18 @@ import { useEffect, useState } from "react"; import usePortfolioDetailsQuery from "../../api/queries/usePortfolioDetailsQuery"; import { PortfolioDetails, PortfolioHolding } from "../../api/types"; import { usePortfolioId } from "../../hook/usePortfolioId"; -import { PortfolioPageTab } from "../../types"; import MainPanelD from "./desktop/MainPanelD"; -type Props = { - tab: PortfolioPageTab; - onChangeTab: (tab: PortfolioPageTab) => void; -}; +// type Props = { +// tab: PortfolioPageTab; +// onChangeTab: (tab: PortfolioPageTab) => void; +// }; -export default function MainPanel({ tab, onChangeTab }: Props) { +// export default function MainPanel({ tab, onChangeTab }: Props) { +export default function MainPanel() { const portfolioId = usePortfolioId(); - const { isDesktop, isMobile } = useResponsiveLayout(); + const { isDesktop } = useResponsiveLayout(); const { data: portfolio } = usePortfolioDetailsQuery(Number(portfolioId)); diff --git a/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx b/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx index 3355209..3fc53e7 100644 --- a/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx +++ b/src/features/portfolio/components/PortfolioAddOrEditDialog/PortfolioAddOrEditDialog.tsx @@ -9,7 +9,7 @@ type Props = { }; export default function PortfolioAddOrEditDialog(props: Props) { - const { isMobile, isDesktop } = useResponsiveLayout(); + const { isDesktop } = useResponsiveLayout(); return ( <> diff --git a/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx index ab8abe4..2a22292 100644 --- a/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx +++ b/src/features/portfolio/components/PortfolioHolding/PortfolioHoldingAddDialog.tsx @@ -7,7 +7,7 @@ type Props = { }; export default function PortfolioHoldingAddDialog(props: Props) { - const { isDesktop, isMobile } = useResponsiveLayout(); + const { isDesktop } = useResponsiveLayout(); return ( <> From e7d5331127d25e796b01de22bc5bcefae484b9af Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 28 Nov 2024 22:30:28 +0900 Subject: [PATCH 39/39] =?UTF-8?q?#24=20refactor:=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=92=EB=93=A4=20=EC=A3=BC=EC=84=9D=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../desktop/PortfolioHoldingRow.tsx | 128 ++++++++++-------- .../desktop/PortfolioOverviewBodyD.tsx | 11 +- .../desktop/PortfolioOverviewD.tsx | 4 +- src/pages/portfolio/[portfolioId].tsx | 15 +- 4 files changed, 90 insertions(+), 68 deletions(-) diff --git a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx index b602a7a..3af50b7 100644 --- a/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx +++ b/src/features/portfolio/components/PortfolioHolding/desktop/PortfolioHoldingRow.tsx @@ -120,14 +120,14 @@ export default memo(function PortfolioHoldingRow({ ); }); -const MemoizedHoldingTableCell = memo( - ({ - isRowOpen, - onExpandRowClick, - }: { - isRowOpen: boolean; - onExpandRowClick: (event: MouseEvent) => void; - }) => ( +const MemoizedHoldingTableCell = memo(function HoldingTableCellComponent({ + isRowOpen, + onExpandRowClick, +}: { + isRowOpen: boolean; + onExpandRowClick: (event: MouseEvent) => void; +}) { + return ( - ) -); + ); +}); -const MemoizedCheckBoxCell = memo( - ({ - isItemSelected, - labelId, - }: { - isItemSelected: boolean; - labelId: string; - }) => ( +const MemoizedCheckBoxCell = memo(function CheckBoxCellComponent({ + isItemSelected, + labelId, +}: { + isItemSelected: boolean; + labelId: string; +}) { + return ( - ) -); + ); +}); -const MemoizedCompanyInfoCell = memo( - ({ - companyName, - tickerSymbol, - }: { - companyName: string; - tickerSymbol: string; - }) => ( +const MemoizedCompanyInfoCell = memo(function CompanyInfoCellComponent({ + companyName, + tickerSymbol, +}: { + companyName: string; + tickerSymbol: string; +}) { + return ( - ) -); + ); +}); -const MemoizedTableCell = memo( - ({ - value, - width, - align = "right", - }: { - value: number; - width: string; - align?: "left" | "right"; - }) => ( +const MemoizedTableCell = memo(function TableCellComponent({ + value, + width, + align = "right", +}: { + value: number; + width: string; + align?: "left" | "right"; +}) { + return ( - ) -); + ); +}); -const MemoizedTableCellWithRateBadge = memo( - ({ value, rate, width }: { value: number; rate: number; width: string }) => ( +const MemoizedTableCellWithRateBadge = memo(function TableCellWithRateBadge({ + value, + rate, + width, +}: { + value: number; + rate: number; + width: string; +}) { + return (
- ) -); + ); +}); -const MemoizedAmountCell = memo( - ({ value, width }: { value: number; width: string }) => ( +const MemoizedAmountCell = memo(function AmountCellComponent({ + value, + width, +}: { + value: number; + width: string; +}) { + return ( {thousandsDelimiter(value)} - ) -); + ); +}); -const MemoizedTextCell = memo( - ({ text, width }: { text: number; width: string }) => ( +const MemoizedTextCell = memo(function TextCellComponent({ + text, + width, +}: { + text: number; + width: string; +}) { + return ( {text} - ) -); + ); +}); const StyledHoldingTableRow = styled(TableRow)` &.Mui-selected { diff --git a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx index abd2473..622f69b 100644 --- a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx +++ b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewBodyD.tsx @@ -4,7 +4,6 @@ import { Icon } from "@/components/Icon"; import ConditionalTooltip from "@/components/Tooltips/ConditionalTooltip"; import { CustomTooltip } from "@/components/Tooltips/CustomTooltip"; import { PortfolioDetails } from "@/features/portfolio/api/types"; -import { usePortfolioId } from "@/features/portfolio/hook/usePortfolioId"; import designSystem from "@/styles/designSystem"; import { thousandsDelimiter } from "@fineants/demolition"; import { debounce } from "@mui/material"; @@ -41,7 +40,7 @@ export default memo(function PortfolioOverviewBodyD({ data }: Props) { annualInvestmentDividendYield, } = data; - const portfolioId = usePortfolioId(); + // const portfolioId = usePortfolioId(); // TODO: 알림 추가하면서 추가하기 // const { mutate } = usePortfolioNotificationSettingsMutation( @@ -101,7 +100,7 @@ export default memo(function PortfolioOverviewBodyD({ data }: Props) { ); }); -const BudgetSection = memo(function ({ +const BudgetSection = memo(function BudgetSection({ budget, investedAmount, balance, @@ -145,7 +144,7 @@ const BudgetSection = memo(function ({ ); }); -const TargetAndLossSection = memo(function ({ +const TargetAndLossSection = memo(function TargetAndLossSection({ targetGain, targetReturnRate, targetGainNotify, @@ -233,7 +232,7 @@ const TargetAndLossSection = memo(function ({ ); }); -const GainAndLossSection = memo(function ({ +const GainAndLossSection = memo(function GainAndLossSection({ totalGain, totalGainRate, dailyGain, @@ -272,7 +271,7 @@ const GainAndLossSection = memo(function ({ ); }); -const DividendSection = memo(function ({ +const DividendSection = memo(function DividendSection({ annualDividend, annualDividendYield, annualInvestmentDividendYield, diff --git a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx index 317633d..7b7c5bf 100644 --- a/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx +++ b/src/features/portfolio/components/portfolioOverview/desktop/PortfolioOverviewD.tsx @@ -80,7 +80,7 @@ type HeaderProps = Pick & { onPortfolioEdit: () => void; }; -const Header = memo(function ({ +const Header = memo(function Header({ name, id, securitiesFirm, @@ -127,7 +127,7 @@ const Header = memo(function ({ ); }); -const CurrentValue = memo(function ({ +const CurrentValue = memo(function CurrentValue({ currentValuation, }: Pick) { return ( diff --git a/src/pages/portfolio/[portfolioId].tsx b/src/pages/portfolio/[portfolioId].tsx index 9beca6e..dc501bb 100644 --- a/src/pages/portfolio/[portfolioId].tsx +++ b/src/pages/portfolio/[portfolioId].tsx @@ -15,11 +15,12 @@ export default function PortfolioPage() { const { isMobile } = useResponsiveLayout(); const portfolioId = usePortfolioId(); - const [tab, setTab] = useState("portfolio"); + // const [tab, setTab] = useState("portfolio"); + const [tab] = useState("portfolio"); - const onChangeTab = (tab: PortfolioPageTab) => { - setTab(tab); - }; + // const onChangeTab = (tab: PortfolioPageTab) => { + // setTab(tab); + // }; return ( @@ -30,7 +31,8 @@ export default function PortfolioPage() { }> - + {/* */} + @@ -40,7 +42,8 @@ export default function PortfolioPage() { }> - + {/* */} +