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 (
+
+
+
+
+ 종목 검색 *
+
+
+
+
+
+ );
+});
+
+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 (
+
+
+
+ );
+}
+
+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() {
}>
-
+ {/* */}
+