From f4bf1ae80400f74dbff7c4fba97eb185b85f5edb Mon Sep 17 00:00:00 2001 From: rladydqls99 Date: Mon, 13 Jan 2025 23:17:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=20basic=20=EC=BD=94=EB=93=9C=20=ED=86=B5=EA=B3=BC=201?= =?UTF-8?q?.=20useCart=20=EA=B5=AC=ED=98=84=202.=20useProduct=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=203.=20useCoupon=20=EA=B5=AC=ED=98=84=204.=20cart=20u?= =?UTF-8?q?tils=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 393 +++++++++++++++++++++--- src/refactoring/components/CartPage.tsx | 30 +- src/refactoring/hooks/useCart.ts | 48 ++- src/refactoring/hooks/useCoupon.ts | 7 +- src/refactoring/hooks/useProduct.ts | 13 +- src/refactoring/models/cart.ts | 61 +++- 7 files changed, 475 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index d3f7e3de..5dde1499 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint": "^9.12.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.12", + "jsdom": "^26.0.0", "typescript": "^5.6.3", "vite": "^5.4.9", "vitest": "^2.1.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f1d525d..b1286148 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,9 +51,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.12 version: 0.4.12(eslint@9.12.0) - prettier: - specifier: ^3.3.3 - version: 3.3.3 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 typescript: specifier: ^5.6.3 version: 5.6.3 @@ -62,16 +62,16 @@ importers: version: 5.4.9 vitest: specifier: ^2.1.3 - version: 2.1.3(@vitest/ui@2.1.3) - zustand: - specifier: ^5.0.0 - version: 5.0.0(@types/react@18.3.11)(react@18.3.1) + version: 2.1.3(@vitest/ui@2.1.3)(jsdom@26.0.0) packages: '@adobe/css-tools@4.4.0': resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + '@asamuzakjp/css-color@2.8.2': + resolution: {integrity: sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==} + '@babel/code-frame@7.25.7': resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} @@ -88,6 +88,34 @@ packages: resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} engines: {node: '>=6.9.0'} + '@csstools/color-helpers@5.0.1': + resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.1': + resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.7': + resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -603,6 +631,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -636,6 +668,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -690,6 +725,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -700,9 +739,17 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -712,6 +759,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -719,6 +769,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -729,6 +783,10 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -844,6 +902,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -872,6 +934,22 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -900,6 +978,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -910,6 +991,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -943,6 +1033,10 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -958,6 +1052,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -984,6 +1086,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1000,6 +1105,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1034,11 +1142,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} - engines: {node: '>=14'} - hasBin: true - pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -1082,9 +1185,19 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -1134,6 +1247,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1159,6 +1275,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.71: + resolution: {integrity: sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==} + + tldts@6.1.71: + resolution: {integrity: sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1167,6 +1290,14 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@5.1.0: + resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -1246,6 +1377,26 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1260,32 +1411,41 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zustand@5.0.0: - resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} - engines: {node: '>=12.20.0'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: + bufferutil: optional: true - use-sync-external-store: + utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: '@adobe/css-tools@4.4.0': {} + '@asamuzakjp/css-color@2.8.2': + dependencies: + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 11.0.2 + '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 @@ -1304,6 +1464,26 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@csstools/color-helpers@5.0.1': {} + + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.1 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1722,7 +1902,7 @@ snapshots: sirv: 2.0.4 tinyglobby: 0.2.9 tinyrainbow: 1.2.0 - vitest: 2.1.3(@vitest/ui@2.1.3) + vitest: 2.1.3(@vitest/ui@2.1.3)(jsdom@26.0.0) '@vitest/utils@2.1.3': dependencies: @@ -1736,6 +1916,8 @@ snapshots: acorn@8.13.0: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -1765,6 +1947,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} brace-expansion@1.1.11: @@ -1822,6 +2006,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} cross-spawn@7.0.3: @@ -1832,22 +2020,38 @@ snapshots: css.escape@1.5.1: {} + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.2 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + debug@4.3.7: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + entities@4.5.0: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2001,6 +2205,12 @@ snapshots: flatted@3.3.1: {} + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -2020,6 +2230,28 @@ snapshots: has-flag@4.0.0: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} import-fresh@3.3.0: @@ -2039,6 +2271,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isexe@2.0.0: {} js-tokens@4.0.0: {} @@ -2047,6 +2281,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -2076,6 +2338,8 @@ snapshots: loupe@3.1.2: {} + lru-cache@11.0.2: {} + lz-string@1.5.0: {} magic-string@0.30.12: @@ -2089,6 +2353,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + min-indent@1.0.1: {} minimatch@3.1.2: @@ -2107,6 +2377,8 @@ snapshots: natural-compare@1.4.0: {} + nwsapi@2.2.16: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2128,6 +2400,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -2150,8 +2426,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.3.3: {} - pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -2207,10 +2481,18 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -2251,6 +2533,8 @@ snapshots: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -2268,12 +2552,26 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.71: {} + + tldts@6.1.71: + dependencies: + tldts-core: 6.1.71 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 totalist@3.0.1: {} + tough-cookie@5.1.0: + dependencies: + tldts: 6.1.71 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -2313,7 +2611,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitest@2.1.3(@vitest/ui@2.1.3): + vitest@2.1.3(@vitest/ui@2.1.3)(jsdom@26.0.0): dependencies: '@vitest/expect': 2.1.3 '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.9) @@ -2336,6 +2634,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@vitest/ui': 2.1.3(vitest@2.1.3) + jsdom: 26.0.0 transitivePeerDependencies: - less - lightningcss @@ -2347,6 +2646,23 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2358,9 +2674,10 @@ snapshots: word-wrap@1.2.5: {} - yocto-queue@0.1.0: {} + ws@8.18.0: {} - zustand@5.0.0(@types/react@18.3.11)(react@18.3.1): - optionalDependencies: - '@types/react': 18.3.11 - react: 18.3.1 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yocto-queue@0.1.0: {} diff --git a/src/refactoring/components/CartPage.tsx b/src/refactoring/components/CartPage.tsx index bafe5ecb..ad118fbb 100644 --- a/src/refactoring/components/CartPage.tsx +++ b/src/refactoring/components/CartPage.tsx @@ -1,11 +1,17 @@ import { CartItem, Coupon, Product } from '../../types.ts'; import { useCart } from "../hooks"; +import {getMaxApplicableDiscount} from "../models/cart.ts"; interface Props { products: Product[]; coupons: Coupon[]; } +export const getRemainingStock = (cart:CartItem[], product: Product) => { + const cartItem = cart.find(item => item.product.id === product.id); + return product.stock - (cartItem?.quantity || 0); +}; + export const CartPage = ({ products, coupons }: Props) => { const { cart, @@ -17,29 +23,11 @@ export const CartPage = ({ products, coupons }: Props) => { selectedCoupon } = useCart(); + const {totalDiscount, totalAfterDiscount,totalBeforeDiscount} = calculateTotal() const getMaxDiscount = (discounts: { quantity: number; rate: number }[]) => { return discounts.reduce((max, discount) => Math.max(max, discount.rate), 0); }; - const getRemainingStock = (product: Product) => { - const cartItem = cart.find(item => item.product.id === product.id); - return product.stock - (cartItem?.quantity || 0); - }; - - const { totalBeforeDiscount, totalAfterDiscount, totalDiscount } = calculateTotal() - - const getAppliedDiscount = (item: CartItem) => { - const { discounts } = item.product; - const { quantity } = item; - let appliedDiscount = 0; - for (const discount of discounts) { - if (quantity >= discount.quantity) { - appliedDiscount = Math.max(appliedDiscount, discount.rate); - } - } - return appliedDiscount; - }; - return (

장바구니

@@ -48,7 +36,7 @@ export const CartPage = ({ products, coupons }: Props) => {

상품 목록

{products.map(product => { - const remainingStock = getRemainingStock(product); + const remainingStock = getRemainingStock(cart, product); return (
@@ -95,7 +83,7 @@ export const CartPage = ({ products, coupons }: Props) => {
{cart.map(item => { - const appliedDiscount = getAppliedDiscount(item); + const appliedDiscount = getMaxApplicableDiscount(item); return (
diff --git a/src/refactoring/hooks/useCart.ts b/src/refactoring/hooks/useCart.ts index 67bf9208..fa3d0b38 100644 --- a/src/refactoring/hooks/useCart.ts +++ b/src/refactoring/hooks/useCart.ts @@ -1,25 +1,53 @@ // useCart.ts import { useState } from "react"; import { CartItem, Coupon, Product } from "../../types"; -import { calculateCartTotal, updateCartItemQuantity } from "../models/cart"; +import {getRemainingStock} from "../components/CartPage.tsx"; +import {calculateCartTotal, updateCartItemQuantity} from "../models/cart.ts"; export const useCart = () => { const [cart, setCart] = useState([]); const [selectedCoupon, setSelectedCoupon] = useState(null); - const addToCart = (product: Product) => {}; + const addToCart = (product: Product) => { + const remainingStock = getRemainingStock(cart, product); + if (remainingStock <= 0) return; - const removeFromCart = (productId: string) => {}; + setCart(prevCart => { + const existingItem = prevCart.find(item => item.product.id === product.id); + if (existingItem) { + return prevCart.map(item => + item.product.id === product.id + ? { ...item, quantity: Math.min(item.quantity + 1, product.stock) } + : item + ); + } + return [...prevCart, { product, quantity: 1 }]; + }); + }; + + const removeFromCart = (productId: string) => { + setCart(prevCart => prevCart.filter(item => item.product.id !== productId)); + }; + + const updateQuantity = (productId: string, newQuantity: number) => { + setCart(prevCart => updateCartItemQuantity(prevCart, productId, newQuantity) + ); + }; + + const applyCoupon = (coupon: Coupon) => { + setSelectedCoupon(coupon); + }; - const updateQuantity = (productId: string, newQuantity: number) => {}; + const calculateTotal = () => { + const {totalBeforeDiscount, totalAfterDiscount, totalDiscount} = calculateCartTotal(cart, selectedCoupon); - const applyCoupon = (coupon: Coupon) => {}; + return { + totalBeforeDiscount, + totalAfterDiscount, + totalDiscount, + }; + }; - const calculateTotal = () => ({ - totalBeforeDiscount: 0, - totalAfterDiscount: 0, - totalDiscount: 0, - }); return { cart, diff --git a/src/refactoring/hooks/useCoupon.ts b/src/refactoring/hooks/useCoupon.ts index dc1b9078..7de1126b 100644 --- a/src/refactoring/hooks/useCoupon.ts +++ b/src/refactoring/hooks/useCoupon.ts @@ -2,5 +2,10 @@ import { Coupon } from "../../types.ts"; import { useState } from "react"; export const useCoupons = (initialCoupons: Coupon[]) => { - return { coupons: [], addCoupon: () => undefined }; + const [coupons, setCoupons] = useState(initialCoupons); + + const addCoupon= (newCoupon: Coupon) => { + setCoupons(prevCoupons => [...prevCoupons, newCoupon]); + }; + return { coupons, addCoupon }; }; diff --git a/src/refactoring/hooks/useProduct.ts b/src/refactoring/hooks/useProduct.ts index 028bacb6..a083798e 100644 --- a/src/refactoring/hooks/useProduct.ts +++ b/src/refactoring/hooks/useProduct.ts @@ -2,5 +2,16 @@ import { useState } from 'react'; import { Product } from '../../types.ts'; export const useProducts = (initialProducts: Product[]) => { - return { products: [], updateProduct: () => undefined, addProduct: () => undefined }; + const [products, setProducts] = useState(initialProducts); + + const updateProduct = (updatedProduct: Product) => { + setProducts(prevProducts => + prevProducts.map(p => p.id === updatedProduct.id ? updatedProduct : p) + ); + }; + + const addProduct = (newProduct: Product) => { + setProducts(prevProducts => [...prevProducts, newProduct]); + }; + return { products, updateProduct, addProduct}; }; diff --git a/src/refactoring/models/cart.ts b/src/refactoring/models/cart.ts index d8f4c2ea..b3002a79 100644 --- a/src/refactoring/models/cart.ts +++ b/src/refactoring/models/cart.ts @@ -1,21 +1,60 @@ import { CartItem, Coupon } from "../../types"; export const calculateItemTotal = (item: CartItem) => { - return 0; + const { price } = item.product; + const { quantity } = item; + + const discount = item.product.discounts.reduce((maxDiscount, d) => { + return quantity >= d.quantity && d.rate > maxDiscount ? d.rate : maxDiscount; + }, 0); + + return price * quantity * (1 - discount); }; export const getMaxApplicableDiscount = (item: CartItem) => { - return 0; + const { discounts } = item.product; + const { quantity } = item; + let appliedDiscount = 0; + for (const discount of discounts) { + if (quantity >= discount.quantity) { + appliedDiscount = Math.max(appliedDiscount, discount.rate); + } + } + return appliedDiscount; + }; export const calculateCartTotal = ( cart: CartItem[], selectedCoupon: Coupon | null -) => { +) => { + let totalBeforeDiscount = 0; + let totalAfterDiscount = 0; + + cart.forEach(item => { + const { price } = item.product; + const { quantity } = item; + + totalBeforeDiscount += price * quantity; + totalAfterDiscount += calculateItemTotal(item); + }); + + let totalDiscount = totalBeforeDiscount - totalAfterDiscount; + + // 쿠폰 적용 + if (selectedCoupon) { + if (selectedCoupon.discountType === 'amount') { + totalAfterDiscount = Math.max(0, totalAfterDiscount - selectedCoupon.discountValue); + } else { + totalAfterDiscount *= (1 - selectedCoupon.discountValue / 100); + } + totalDiscount = totalBeforeDiscount - totalAfterDiscount; + } + return { - totalBeforeDiscount: 0, - totalAfterDiscount: 0, - totalDiscount: 0, + totalBeforeDiscount: Math.round(totalBeforeDiscount), + totalAfterDiscount: Math.round(totalAfterDiscount), + totalDiscount: Math.round(totalDiscount) }; }; @@ -24,5 +63,13 @@ export const updateCartItemQuantity = ( productId: string, newQuantity: number ): CartItem[] => { - return []; + return cart.map(item => { + if (item.product.id === productId) { + const maxQuantity = item.product.stock; + const updatedQuantity = Math.max(0, Math.min(newQuantity, maxQuantity)); + return updatedQuantity > 0 ? { ...item, quantity: updatedQuantity } : null; + } + return item; + }).filter((item): item is CartItem => item !== null) + }; From 018b4c7686a4e7a1cff03f82bc28a02b64b4d424 Mon Sep 17 00:00:00 2001 From: rladydqls99 Date: Tue, 14 Jan 2025 18:46:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20prettier=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.cjs | 19 ++++++------ .prettierrc | 10 ++++++ package.json | 3 ++ pnpm-lock.yaml | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d6c95379..902b0d16 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,17 +2,18 @@ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:prettier/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs", "node_modules"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh", "prettier"], rules: { - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, -} +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..dbf75fb2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": false, + "endOfLine": "auto", + "printWidth": 80, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all" +} \ No newline at end of file diff --git a/package.json b/package.json index 5dde1499..1b13af08 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,12 @@ "@vitejs/plugin-react-swc": "^3.7.1", "@vitest/ui": "^2.1.3", "eslint": "^9.12.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.12", "jsdom": "^26.0.0", + "prettier": "^3.4.2", "typescript": "^5.6.3", "vite": "^5.4.9", "vitest": "^2.1.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1286148..ba4dd27f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,12 @@ importers: eslint: specifier: ^9.12.0 version: 9.12.0 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.0.1(eslint@9.12.0) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.2.1(eslint-config-prettier@10.0.1(eslint@9.12.0))(eslint@9.12.0)(prettier@3.4.2) eslint-plugin-react-hooks: specifier: ^5.0.0 version: 5.0.0(eslint@9.12.0) @@ -54,6 +60,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.0.0 + prettier: + specifier: ^3.4.2 + version: 3.4.2 typescript: specifier: ^5.6.3 version: 5.6.3 @@ -319,6 +328,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} @@ -800,6 +813,26 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.0.1: + resolution: {integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-plugin-react-hooks@5.0.0: resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} engines: {node: '>=10'} @@ -859,6 +892,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -1142,6 +1178,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -1250,6 +1295,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1304,6 +1353,9 @@ packages: peerDependencies: typescript: '>=4.2.0' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1617,6 +1669,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@pkgr/core@0.1.1': {} + '@polka/url@1.0.0-next.28': {} '@rollup/rollup-android-arm-eabi@4.24.0': @@ -2082,6 +2136,19 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-prettier@10.0.1(eslint@9.12.0): + dependencies: + eslint: 9.12.0 + + eslint-plugin-prettier@5.2.1(eslint-config-prettier@10.0.1(eslint@9.12.0))(eslint@9.12.0)(prettier@3.4.2): + dependencies: + eslint: 9.12.0 + prettier: 3.4.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + eslint-config-prettier: 10.0.1(eslint@9.12.0) + eslint-plugin-react-hooks@5.0.0(eslint@9.12.0): dependencies: eslint: 9.12.0 @@ -2163,6 +2230,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2426,6 +2495,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.4.2: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -2535,6 +2610,11 @@ snapshots: symbol-tree@3.2.4: {} + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -2576,6 +2656,8 @@ snapshots: dependencies: typescript: 5.6.3 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 From 4db1a7cf980c7b8d525098ea7c5c45188e5c8509 Mon Sep 17 00:00:00 2001 From: rladydqls99 Date: Thu, 16 Jan 2025 00:00:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 1 - package.json | 4 +- pnpm-lock.yaml | 16 + src/refactoring/components/AdminPage.tsx | 358 ++---------------- src/refactoring/components/CartPage.tsx | 150 +++++--- .../admin-page/CouponManageSection.tsx | 110 ++++++ .../admin-page/ProductManageSection.tsx | 306 +++++++++++++++ .../components/templates/Container.tsx | 10 + .../components/templates/Title.tsx | 39 ++ src/refactoring/components/ui/Button.tsx | 56 +++ src/refactoring/components/ui/Card.tsx | 32 ++ src/refactoring/components/ui/Input.tsx | 53 +++ src/refactoring/components/ui/Select.tsx | 21 + src/refactoring/utils/classNames.ts | 6 + 14 files changed, 773 insertions(+), 389 deletions(-) create mode 100644 src/refactoring/components/admin-page/CouponManageSection.tsx create mode 100644 src/refactoring/components/admin-page/ProductManageSection.tsx create mode 100644 src/refactoring/components/templates/Container.tsx create mode 100644 src/refactoring/components/templates/Title.tsx create mode 100644 src/refactoring/components/ui/Button.tsx create mode 100644 src/refactoring/components/ui/Card.tsx create mode 100644 src/refactoring/components/ui/Input.tsx create mode 100644 src/refactoring/components/ui/Select.tsx create mode 100644 src/refactoring/utils/classNames.ts diff --git a/.prettierrc b/.prettierrc index dbf75fb2..7a96a7b2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,5 @@ { "arrowParens": "avoid", - "bracketSpacing": false, "endOfLine": "auto", "printWidth": 80, "semi": true, diff --git a/package.json b/package.json index 1b13af08..7cd952cb 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { + "classnames": "^2.5.1", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "tailwind-merge": "^2.6.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba4dd27f..1e105205 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: dependencies: + classnames: + specifier: ^2.5.1 + version: 2.5.1 react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 devDependencies: '@testing-library/jest-dom': specifier: ^6.6.2 @@ -725,6 +731,9 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1299,6 +1308,9 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2048,6 +2060,8 @@ snapshots: check-error@2.1.1: {} + classnames@2.5.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -2615,6 +2629,8 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 + tailwind-merge@2.6.0: {} + text-table@0.2.0: {} tinybench@2.9.0: {} diff --git a/src/refactoring/components/AdminPage.tsx b/src/refactoring/components/AdminPage.tsx index 4ff2bbcd..57c3f5ed 100644 --- a/src/refactoring/components/AdminPage.tsx +++ b/src/refactoring/components/AdminPage.tsx @@ -1,5 +1,8 @@ -import { useState } from 'react'; -import { Coupon, Discount, Product } from '../../types.ts'; +import { Coupon, Product } from '../../types.ts'; +import { CouponManageSection } from './admin-page/CouponManageSection.tsx'; +import { ProductManageSection } from './admin-page/ProductManageSection.tsx'; +import { Container } from './templates/Container.tsx'; +import { Title } from './templates/Title.tsx'; interface Props { products: Product[]; @@ -9,337 +12,30 @@ interface Props { onCouponAdd: (newCoupon: Coupon) => void; } -export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, onCouponAdd }: Props) => { - const [openProductIds, setOpenProductIds] = useState>(new Set()); - const [editingProduct, setEditingProduct] = useState(null); - const [newDiscount, setNewDiscount] = useState({ quantity: 0, rate: 0 }); - const [newCoupon, setNewCoupon] = useState({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 - }); - const [showNewProductForm, setShowNewProductForm] = useState(false); - const [newProduct, setNewProduct] = useState>({ - name: '', - price: 0, - stock: 0, - discounts: [] - }); - - const toggleProductAccordion = (productId: string) => { - setOpenProductIds(prev => { - const newSet = new Set(prev); - if (newSet.has(productId)) { - newSet.delete(productId); - } else { - newSet.add(productId); - } - return newSet; - }); - }; - - // handleEditProduct 함수 수정 - const handleEditProduct = (product: Product) => { - setEditingProduct({...product}); - }; - - // 새로운 핸들러 함수 추가 - const handleProductNameUpdate = (productId: string, newName: string) => { - if (editingProduct && editingProduct.id === productId) { - const updatedProduct = { ...editingProduct, name: newName }; - setEditingProduct(updatedProduct); - } - }; - - // 새로운 핸들러 함수 추가 - const handlePriceUpdate = (productId: string, newPrice: number) => { - if (editingProduct && editingProduct.id === productId) { - const updatedProduct = { ...editingProduct, price: newPrice }; - setEditingProduct(updatedProduct); - } - }; - - // 수정 완료 핸들러 함수 추가 - const handleEditComplete = () => { - if (editingProduct) { - onProductUpdate(editingProduct); - setEditingProduct(null); - } - }; - - const handleStockUpdate = (productId: string, newStock: number) => { - const updatedProduct = products.find(p => p.id === productId); - if (updatedProduct) { - const newProduct = { ...updatedProduct, stock: newStock }; - onProductUpdate(newProduct); - setEditingProduct(newProduct); - } - }; - - const handleAddDiscount = (productId: string) => { - const updatedProduct = products.find(p => p.id === productId); - if (updatedProduct && editingProduct) { - const newProduct = { - ...updatedProduct, - discounts: [...updatedProduct.discounts, newDiscount] - }; - onProductUpdate(newProduct); - setEditingProduct(newProduct); - setNewDiscount({ quantity: 0, rate: 0 }); - } - }; - - const handleRemoveDiscount = (productId: string, index: number) => { - const updatedProduct = products.find(p => p.id === productId); - if (updatedProduct) { - const newProduct = { - ...updatedProduct, - discounts: updatedProduct.discounts.filter((_, i) => i !== index) - }; - onProductUpdate(newProduct); - setEditingProduct(newProduct); - } - }; - - const handleAddCoupon = () => { - onCouponAdd(newCoupon); - setNewCoupon({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 - }); - }; - - const handleAddNewProduct = () => { - const productWithId = { ...newProduct, id: Date.now().toString() }; - onProductAdd(productWithId); - setNewProduct({ - name: '', - price: 0, - stock: 0, - discounts: [] - }); - setShowNewProductForm(false); - }; - +export const AdminPage = ({ + products, + coupons, + onProductUpdate, + onProductAdd, + onCouponAdd, +}: Props) => { return ( -
-

관리자 페이지

+ + 관리자 페이지
-
-

상품 관리

- - {showNewProductForm && ( -
-

새 상품 추가

-
- - setNewProduct({ ...newProduct, name: e.target.value })} - className="w-full p-2 border rounded" - /> -
-
- - setNewProduct({ ...newProduct, price: parseInt(e.target.value) })} - className="w-full p-2 border rounded" - /> -
-
- - setNewProduct({ ...newProduct, stock: parseInt(e.target.value) })} - className="w-full p-2 border rounded" - /> -
- -
- )} -
- {products.map((product, index) => ( -
- - {openProductIds.has(product.id) && ( -
- {editingProduct && editingProduct.id === product.id ? ( -
-
- - handleProductNameUpdate(product.id, e.target.value)} - className="w-full p-2 border rounded" - /> -
-
- - handlePriceUpdate(product.id, parseInt(e.target.value))} - className="w-full p-2 border rounded" - /> -
-
- - handleStockUpdate(product.id, parseInt(e.target.value))} - className="w-full p-2 border rounded" - /> -
- {/* 할인 정보 수정 부분 */} -
-

할인 정보

- {editingProduct.discounts.map((discount, index) => ( -
- {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인 - -
- ))} -
- setNewDiscount({ ...newDiscount, quantity: parseInt(e.target.value) })} - className="w-1/3 p-2 border rounded" - /> - setNewDiscount({ ...newDiscount, rate: parseInt(e.target.value) / 100 })} - className="w-1/3 p-2 border rounded" - /> - -
-
- -
- ) : ( -
- {product.discounts.map((discount, index) => ( -
- {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인 -
- ))} - -
- )} -
- )} -
- ))} -
-
-
-

쿠폰 관리

-
-
- setNewCoupon({ ...newCoupon, name: e.target.value })} - className="w-full p-2 border rounded" - /> - setNewCoupon({ ...newCoupon, code: e.target.value })} - className="w-full p-2 border rounded" - /> -
- - setNewCoupon({ ...newCoupon, discountValue: parseInt(e.target.value) })} - className="w-full p-2 border rounded" - /> -
- -
-
-

현재 쿠폰 목록

-
- {coupons.map((coupon, index) => ( -
- {coupon.name} ({coupon.code}): - {coupon.discountType === 'amount' ? `${coupon.discountValue}원` : `${coupon.discountValue}%`} 할인 -
- ))} -
-
-
-
+ +
-
+ ); }; diff --git a/src/refactoring/components/CartPage.tsx b/src/refactoring/components/CartPage.tsx index ad118fbb..1a19c0e0 100644 --- a/src/refactoring/components/CartPage.tsx +++ b/src/refactoring/components/CartPage.tsx @@ -1,13 +1,18 @@ import { CartItem, Coupon, Product } from '../../types.ts'; -import { useCart } from "../hooks"; -import {getMaxApplicableDiscount} from "../models/cart.ts"; +import { useCart } from '../hooks'; +import { getMaxApplicableDiscount } from '../models/cart.ts'; +import { Container } from './templates/Container.tsx'; +import { Title } from './templates/Title.tsx'; +import { Button } from './ui/Button.tsx'; +import { Card } from './ui/Card.tsx'; +import { Select } from './ui/Select.tsx'; interface Props { products: Product[]; coupons: Coupon[]; } -export const getRemainingStock = (cart:CartItem[], product: Product) => { +export const getRemainingStock = (cart: CartItem[], product: Product) => { const cartItem = cart.find(item => item.product.id === product.id); return product.stock - (cartItem?.quantity || 0); }; @@ -20,36 +25,47 @@ export const CartPage = ({ products, coupons }: Props) => { updateQuantity, applyCoupon, calculateTotal, - selectedCoupon + selectedCoupon, } = useCart(); - const {totalDiscount, totalAfterDiscount,totalBeforeDiscount} = calculateTotal() + const { totalDiscount, totalAfterDiscount, totalBeforeDiscount } = + calculateTotal(); const getMaxDiscount = (discounts: { quantity: number; rate: number }[]) => { return discounts.reduce((max, discount) => Math.max(max, discount.rate), 0); }; return ( -
-

장바구니

+ + 장바구니
-

상품 목록

+ 상품 목록
{products.map(product => { const remainingStock = getRemainingStock(cart, product); return ( -
+
{product.name} - {product.price.toLocaleString()}원 + + {product.price.toLocaleString()}원 +
- 0 ? 'text-green-600' : 'text-red-600'}`}> + 0 ? 'text-green-600' : 'text-red-600'}`} + > 재고: {remainingStock}개 {product.discounts.length > 0 && ( - 최대 {(getMaxDiscount(product.discounts) * 100).toFixed(0)}% 할인 + 최대{' '} + {(getMaxDiscount(product.discounts) * 100).toFixed(0)}% + 할인 )}
@@ -57,105 +73,127 @@ export const CartPage = ({ products, coupons }: Props) => {
    {product.discounts.map((discount, index) => (
  • - {discount.quantity}개 이상: {(discount.rate * 100).toFixed(0)}% 할인 + {discount.quantity}개 이상:{' '} + {(discount.rate * 100).toFixed(0)}% 할인
  • ))}
)} - -
+ + ); })}
-

장바구니 내역

+ 장바구니 내역
{cart.map(item => { const appliedDiscount = getMaxApplicableDiscount(item); return ( -
+
{item.product.name} -
+
- {item.product.price}원 x {item.quantity} + {item.product.price}원 x {item.quantity} {appliedDiscount > 0 && ( - ({(appliedDiscount * 100).toFixed(0)}% 할인 적용) - + ({(appliedDiscount * 100).toFixed(0)}% 할인 적용) + )} - +
- - + - +
-
+ ); })}
-
-

쿠폰 적용

- applyCoupon(coupons[parseInt(e.target.value)])} + className="mb-2" > {coupons.map((coupon, index) => ( ))} - + {selectedCoupon && (

- 적용된 쿠폰: {selectedCoupon.name} - ({selectedCoupon.discountType === 'amount' ? `${selectedCoupon.discountValue}원` : `${selectedCoupon.discountValue}%`} 할인) + 적용된 쿠폰: {selectedCoupon.name}( + {selectedCoupon.discountType === 'amount' + ? `${selectedCoupon.discountValue}원` + : `${selectedCoupon.discountValue}%`}{' '} + 할인)

)} -
+ -
-

주문 요약

+ + + 주문 요약 +

상품 금액: {totalBeforeDiscount.toLocaleString()}원

-

할인 금액: {totalDiscount.toLocaleString()}원

+

+ 할인 금액: {totalDiscount.toLocaleString()}원 +

최종 결제 금액: {totalAfterDiscount.toLocaleString()}원

-
+
-
+ ); }; diff --git a/src/refactoring/components/admin-page/CouponManageSection.tsx b/src/refactoring/components/admin-page/CouponManageSection.tsx new file mode 100644 index 00000000..78e3a012 --- /dev/null +++ b/src/refactoring/components/admin-page/CouponManageSection.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; +import { Coupon, Product } from '../../../types.ts'; +import { Title } from '../templates/Title.tsx'; +import { Button } from '../ui/Button.tsx'; +import { Card } from '../ui/Card.tsx'; +import { Input } from '../ui/Input.tsx'; +import { Select } from '../ui/Select.tsx'; + +type PropsType = { + products: Product[]; + coupons: Coupon[]; + onProductUpdate: (updatedProduct: Product) => void; + onProductAdd: (newProduct: Product) => void; + onCouponAdd: (newCoupon: Coupon) => void; +}; +export const CouponManageSection = (props: PropsType) => { + const { coupons, onCouponAdd } = props; + + const [newCoupon, setNewCoupon] = useState({ + name: '', + code: '', + discountType: 'percentage', + discountValue: 0, + }); + + const handleAddCoupon = () => { + onCouponAdd(newCoupon); + setNewCoupon({ + name: '', + code: '', + discountType: 'percentage', + discountValue: 0, + }); + }; + + return ( +
+ 쿠폰 관리 + +
+ setNewCoupon({ ...newCoupon, name: e.target.value })} + /> + setNewCoupon({ ...newCoupon, code: e.target.value })} + /> +
+ + + setNewCoupon({ + ...newCoupon, + discountValue: parseInt(e.target.value), + }) + } + className="w-full p-2 border rounded" + /> +
+ +
+
+ 현재 쿠폰 목록 +
+ {coupons.map((coupon, index) => ( +
+ {coupon.name} ({coupon.code}): + {coupon.discountType === 'amount' + ? `${coupon.discountValue}원` + : `${coupon.discountValue}%`}{' '} + 할인 +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/refactoring/components/admin-page/ProductManageSection.tsx b/src/refactoring/components/admin-page/ProductManageSection.tsx new file mode 100644 index 00000000..bc5186d7 --- /dev/null +++ b/src/refactoring/components/admin-page/ProductManageSection.tsx @@ -0,0 +1,306 @@ +import { useState } from 'react'; +import { Discount, Product } from '../../../types.ts'; +import { Title } from '../templates/Title.tsx'; +import { Button } from '../ui/Button.tsx'; +import { Card } from '../ui/Card.tsx'; +import { Input } from '../ui/Input.tsx'; + +type PropsType = { + products: Product[]; + onProductUpdate: (updatedProduct: Product) => void; + onProductAdd: (newProduct: Product) => void; +}; +export const ProductManageSection = (props: PropsType) => { + const { products, onProductUpdate, onProductAdd } = props; + + const [openProductIds, setOpenProductIds] = useState>(new Set()); + + const [editingProduct, setEditingProduct] = useState(null); + const [newDiscount, setNewDiscount] = useState({ + quantity: 0, + rate: 0, + }); + + const [showNewProductForm, setShowNewProductForm] = useState(false); + + const [newProduct, setNewProduct] = useState>({ + name: '', + price: 0, + stock: 0, + discounts: [], + }); + + const toggleProductAccordion = (productId: string) => { + setOpenProductIds(prev => { + const newSet = new Set(prev); + if (newSet.has(productId)) { + newSet.delete(productId); + } else { + newSet.add(productId); + } + return newSet; + }); + }; + + // handleEditProduct 함수 수정 + const handleEditProduct = (product: Product) => { + setEditingProduct({ ...product }); + }; + + // 새로운 핸들러 함수 추가 + const handleProductNameUpdate = (productId: string, newName: string) => { + if (editingProduct && editingProduct.id === productId) { + const updatedProduct = { ...editingProduct, name: newName }; + setEditingProduct(updatedProduct); + } + }; + + // 새로운 핸들러 함수 추가 + const handlePriceUpdate = (productId: string, newPrice: number) => { + if (editingProduct && editingProduct.id === productId) { + const updatedProduct = { ...editingProduct, price: newPrice }; + setEditingProduct(updatedProduct); + } + }; + + // 수정 완료 핸들러 함수 추가 + const handleEditComplete = () => { + if (editingProduct) { + onProductUpdate(editingProduct); + setEditingProduct(null); + } + }; + + const handleStockUpdate = (productId: string, newStock: number) => { + const updatedProduct = products.find(p => p.id === productId); + if (updatedProduct) { + const newProduct = { ...updatedProduct, stock: newStock }; + onProductUpdate(newProduct); + setEditingProduct(newProduct); + } + }; + + const handleAddDiscount = (productId: string) => { + const updatedProduct = products.find(p => p.id === productId); + if (updatedProduct && editingProduct) { + const newProduct = { + ...updatedProduct, + discounts: [...updatedProduct.discounts, newDiscount], + }; + onProductUpdate(newProduct); + setEditingProduct(newProduct); + setNewDiscount({ quantity: 0, rate: 0 }); + } + }; + + const handleRemoveDiscount = (productId: string, index: number) => { + const updatedProduct = products.find(p => p.id === productId); + if (updatedProduct) { + const newProduct = { + ...updatedProduct, + discounts: updatedProduct.discounts.filter((_, i) => i !== index), + }; + onProductUpdate(newProduct); + setEditingProduct(newProduct); + } + }; + + const handleAddNewProduct = () => { + const productWithId = { ...newProduct, id: Date.now().toString() }; + onProductAdd(productWithId); + setNewProduct({ + name: '', + price: 0, + stock: 0, + discounts: [], + }); + setShowNewProductForm(false); + }; + + return ( +
+ 상품 관리 + + {showNewProductForm && ( + + 새 상품 추가 + + setNewProduct({ ...newProduct, name: e.target.value }) + } + /> + + setNewProduct({ + ...newProduct, + price: parseInt(e.target.value), + }) + } + /> + + setNewProduct({ + ...newProduct, + stock: parseInt(e.target.value), + }) + } + /> + + + )} +
+ {products.map((product, index) => ( + + + {openProductIds.has(product.id) && ( +
+ {editingProduct && editingProduct.id === product.id ? ( +
+ + handleProductNameUpdate(product.id, e.target.value) + } + /> + + handlePriceUpdate(product.id, parseInt(e.target.value)) + } + /> + + handleStockUpdate(product.id, parseInt(e.target.value)) + } + /> + + {/* 할인 정보 수정 부분 */} +
+ 할인 정보 + {editingProduct.discounts.map((discount, index) => ( +
+ + {discount.quantity}개 이상 구매 시{' '} + {discount.rate * 100}% 할인 + + +
+ ))} +
+ + setNewDiscount({ + ...newDiscount, + quantity: parseInt(e.target.value), + }) + } + className="w-1/3 p-2 border rounded" + /> + + setNewDiscount({ + ...newDiscount, + rate: parseInt(e.target.value) / 100, + }) + } + className="w-1/3 p-2 border rounded" + /> + +
+
+ +
+ ) : ( +
+ {product.discounts.map((discount, index) => ( +
+ + {discount.quantity}개 이상 구매 시{' '} + {discount.rate * 100}% 할인 + +
+ ))} + +
+ )} +
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src/refactoring/components/templates/Container.tsx b/src/refactoring/components/templates/Container.tsx new file mode 100644 index 00000000..0e608e17 --- /dev/null +++ b/src/refactoring/components/templates/Container.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +type PropsType = { + children: ReactNode; +}; +export const Container = (props: PropsType) => { + const { children } = props; + + return
{children}
; +}; diff --git a/src/refactoring/components/templates/Title.tsx b/src/refactoring/components/templates/Title.tsx new file mode 100644 index 00000000..fc95b5b6 --- /dev/null +++ b/src/refactoring/components/templates/Title.tsx @@ -0,0 +1,39 @@ +import { ReactNode } from 'react'; +import classNames from '../../utils/classNames.ts'; + +type PropsType = { + children: ReactNode; + level: 1 | 2 | 3 | 4; + className?: string; +}; +export const Title = (props: PropsType) => { + const { children, level, className } = props; + if (level === 1) { + return ( +

+ {children} +

+ ); + } + + if (level === 2) { + return ( +

+ {children} +

+ ); + } + + if (level === 3) { + return ( +

+ {children} +

+ ); + } + return ( +

+ {children} +

+ ); +}; diff --git a/src/refactoring/components/ui/Button.tsx b/src/refactoring/components/ui/Button.tsx new file mode 100644 index 00000000..30233760 --- /dev/null +++ b/src/refactoring/components/ui/Button.tsx @@ -0,0 +1,56 @@ +import { ReactNode } from 'react'; +import classNames from '../../utils/classNames.ts'; + +type PropsType = { + children: ReactNode; + variant?: 'none' | 'primary' | 'secondary' | 'danger' | 'green'; + fullWidth?: boolean; + disabled?: boolean; + className?: string; + onClick?: () => void; +}; + +export const Button = (props: PropsType) => { + const { + children, + variant = 'primary', + fullWidth = false, + disabled = false, + className = '', + onClick, + ...rest + } = props; + + const primaryColor = 'bg-blue-500 text-white hover:bg-blue-600'; + const secondaryColor = 'bg-gray-300 text-gray-800 hover:bg-gray-400'; + const dangerColor = 'bg-red-500 text-white hover:bg-red-600'; + const greenColor = 'bg-green-500 text-white hover:bg-green-600'; + + const getButtonColor = () => { + if (variant === 'none') return ''; + if (variant === 'primary') return primaryColor; + if (variant === 'secondary') return secondaryColor; + if (variant === 'danger') return dangerColor; + if (variant === 'green') return greenColor; + }; + + const widthClass = fullWidth ? 'w-full' : ''; + const cursorClass = disabled ? 'cursor-not-allowed' : ''; + + return ( + + ); +}; diff --git a/src/refactoring/components/ui/Card.tsx b/src/refactoring/components/ui/Card.tsx new file mode 100644 index 00000000..ce03c5b3 --- /dev/null +++ b/src/refactoring/components/ui/Card.tsx @@ -0,0 +1,32 @@ +import { ReactNode } from 'react'; +import classNames from '../../utils/classNames.ts'; + +type PropsType = { + children: ReactNode; + padding?: 'sm' | 'md' | 'lg'; + className?: string; + testId?: string; +}; +export const Card = (props: PropsType) => { + const { children, padding = 'md', className, testId, ...rest } = props; + + const paddingClass = { + sm: 'p-3', + md: 'p-4', + lg: 'p-6', + }; + + return ( +
+ {children} +
+ ); +}; diff --git a/src/refactoring/components/ui/Input.tsx b/src/refactoring/components/ui/Input.tsx new file mode 100644 index 00000000..5e28a1af --- /dev/null +++ b/src/refactoring/components/ui/Input.tsx @@ -0,0 +1,53 @@ +import { ChangeEvent } from 'react'; +import classNames from '../../utils/classNames.ts'; + +type PropsType = { + value: string | number; + onChange: (e: ChangeEvent) => void; + type?: 'text' | 'number'; + placeholder?: string; + label?: string; + id?: string; + wrapperClassName?: string; + labelClassName?: string; + inputClassName?: string; +}; +export const Input = (props: PropsType) => { + const { + value, + onChange, + type = 'text', + placeholder = '', + label, + id, + wrapperClassName, + labelClassName, + inputClassName, + ...rest + } = props; + + return ( +
+ {label && ( + + )} + +
+ ); +}; diff --git a/src/refactoring/components/ui/Select.tsx b/src/refactoring/components/ui/Select.tsx new file mode 100644 index 00000000..581cd31c --- /dev/null +++ b/src/refactoring/components/ui/Select.tsx @@ -0,0 +1,21 @@ +import { ChangeEvent, ReactNode } from 'react'; + +type PropsType = { + value?: string | number; + onChange: (e: ChangeEvent) => void; + className?: string; + children: ReactNode; +}; +export const Select = (props: PropsType) => { + const { onChange, className, children, value = '' } = props; + + return ( + + ); +}; diff --git a/src/refactoring/utils/classNames.ts b/src/refactoring/utils/classNames.ts new file mode 100644 index 00000000..6fab0f66 --- /dev/null +++ b/src/refactoring/utils/classNames.ts @@ -0,0 +1,6 @@ +import cn from 'classnames'; +import { twMerge } from 'tailwind-merge'; + +export default function classNames(...args: cn.ArgumentArray) { + return twMerge(cn(args)); +} From c7e6db8a67209e74111c5e9620a8e07fbf33caa4 Mon Sep 17 00:00:00 2001 From: rladydqls99 Date: Thu, 16 Jan 2025 20:22:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=9B=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 2 +- .../admin-page/ProductManageSection.tsx | 143 ++++-------------- .../admin-page/hooks/useDiscountForm.ts | 25 +++ .../admin-page/hooks/useNewProductForm.ts | 29 ++++ src/refactoring/hooks/useToggle.ts | 19 +++ src/refactoring/hooks/useToggleSet.ts | 19 +++ 6 files changed, 126 insertions(+), 111 deletions(-) create mode 100644 src/refactoring/components/admin-page/hooks/useDiscountForm.ts create mode 100644 src/refactoring/components/admin-page/hooks/useNewProductForm.ts create mode 100644 src/refactoring/hooks/useToggle.ts create mode 100644 src/refactoring/hooks/useToggleSet.ts diff --git a/.prettierrc b/.prettierrc index 7a96a7b2..e726f91c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "arrowParens": "avoid", "endOfLine": "auto", - "printWidth": 80, + "printWidth": 120, "semi": true, "singleQuote": true, "tabWidth": 2, diff --git a/src/refactoring/components/admin-page/ProductManageSection.tsx b/src/refactoring/components/admin-page/ProductManageSection.tsx index bc5186d7..c6a733fe 100644 --- a/src/refactoring/components/admin-page/ProductManageSection.tsx +++ b/src/refactoring/components/admin-page/ProductManageSection.tsx @@ -1,9 +1,13 @@ import { useState } from 'react'; -import { Discount, Product } from '../../../types.ts'; +import { Product } from '../../../types.ts'; +import { useToggle } from '../../hooks/useToggle.ts'; +import { useToggleSet } from '../../hooks/useToggleSet.ts'; import { Title } from '../templates/Title.tsx'; import { Button } from '../ui/Button.tsx'; import { Card } from '../ui/Card.tsx'; import { Input } from '../ui/Input.tsx'; +import { useDiscountForm } from './hooks/useDiscountForm.ts'; +import { useNewProductForm } from './hooks/useNewProductForm.ts'; type PropsType = { products: Product[]; @@ -13,34 +17,12 @@ type PropsType = { export const ProductManageSection = (props: PropsType) => { const { products, onProductUpdate, onProductAdd } = props; - const [openProductIds, setOpenProductIds] = useState>(new Set()); + const { items: openProductIds, toggle: toggleProductAccordion } = useToggleSet(new Set()); + const { state: showProductForm, toggle: handleToggleProjectForm, onClose: onCloseProductForm } = useToggle(false); + const { discountForm, onChange: handleDiscountFormChange, reset: resetDiscountForm } = useDiscountForm(); + const { newProduct, onChangeNewProduct, resetNewProduct } = useNewProductForm(); const [editingProduct, setEditingProduct] = useState(null); - const [newDiscount, setNewDiscount] = useState({ - quantity: 0, - rate: 0, - }); - - const [showNewProductForm, setShowNewProductForm] = useState(false); - - const [newProduct, setNewProduct] = useState>({ - name: '', - price: 0, - stock: 0, - discounts: [], - }); - - const toggleProductAccordion = (productId: string) => { - setOpenProductIds(prev => { - const newSet = new Set(prev); - if (newSet.has(productId)) { - newSet.delete(productId); - } else { - newSet.add(productId); - } - return newSet; - }); - }; // handleEditProduct 함수 수정 const handleEditProduct = (product: Product) => { @@ -85,11 +67,11 @@ export const ProductManageSection = (props: PropsType) => { if (updatedProduct && editingProduct) { const newProduct = { ...updatedProduct, - discounts: [...updatedProduct.discounts, newDiscount], + discounts: [...updatedProduct.discounts, discountForm], }; onProductUpdate(newProduct); setEditingProduct(newProduct); - setNewDiscount({ quantity: 0, rate: 0 }); + resetDiscountForm(); } }; @@ -108,59 +90,38 @@ export const ProductManageSection = (props: PropsType) => { const handleAddNewProduct = () => { const productWithId = { ...newProduct, id: Date.now().toString() }; onProductAdd(productWithId); - setNewProduct({ - name: '', - price: 0, - stock: 0, - discounts: [], - }); - setShowNewProductForm(false); + resetNewProduct(); + onCloseProductForm(); }; return (
상품 관리 - - {showNewProductForm && ( + {showProductForm && ( 새 상품 추가 - setNewProduct({ ...newProduct, name: e.target.value }) - } + onChange={e => onChangeNewProduct('name', e.target.value)} /> - setNewProduct({ - ...newProduct, - price: parseInt(e.target.value), - }) - } + onChange={e => onChangeNewProduct('price', parseInt(e.target.value))} /> - setNewProduct({ - ...newProduct, - stock: parseInt(e.target.value), - }) - } + onChange={e => onChangeNewProduct('stock', parseInt(e.target.value))} />
@@ -239,41 +185,23 @@ export const ProductManageSection = (props: PropsType) => { - setNewDiscount({ - ...newDiscount, - quantity: parseInt(e.target.value), - }) - } + value={discountForm.quantity} + onChange={e => handleDiscountFormChange('quantity', parseInt(e.target.value))} className="w-1/3 p-2 border rounded" /> - setNewDiscount({ - ...newDiscount, - rate: parseInt(e.target.value) / 100, - }) - } + value={discountForm.rate * 100} + onChange={e => handleDiscountFormChange('rate', parseInt(e.target.value) / 100)} className="w-1/3 p-2 border rounded" /> -
-
@@ -282,16 +210,11 @@ export const ProductManageSection = (props: PropsType) => { {product.discounts.map((discount, index) => (
- {discount.quantity}개 이상 구매 시{' '} - {discount.rate * 100}% 할인 + {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인
))} -
diff --git a/src/refactoring/components/admin-page/hooks/useDiscountForm.ts b/src/refactoring/components/admin-page/hooks/useDiscountForm.ts new file mode 100644 index 00000000..a32fb0db --- /dev/null +++ b/src/refactoring/components/admin-page/hooks/useDiscountForm.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import { Discount } from '../../../../types.ts'; + +export const useDiscountForm = () => { + const [newDiscount, setNewDiscount] = useState({ + quantity: 0, + rate: 0, + }); + + const onChange = (key: 'quantity' | 'rate', value: number) => { + setNewDiscount(prev => ({ + ...prev, + [key]: value, + })); + }; + + const reset = () => { + setNewDiscount({ + quantity: 0, + rate: 0, + }); + }; + + return { discountForm: newDiscount, onChange, reset }; +}; diff --git a/src/refactoring/components/admin-page/hooks/useNewProductForm.ts b/src/refactoring/components/admin-page/hooks/useNewProductForm.ts new file mode 100644 index 00000000..b7fbb0db --- /dev/null +++ b/src/refactoring/components/admin-page/hooks/useNewProductForm.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import { Product } from '../../../../types.ts'; + +export const useNewProductForm = () => { + const [newProduct, setNewProduct] = useState>({ + name: '', + price: 0, + stock: 0, + discounts: [], + }); + + const onChangeNewProduct = (key: 'name' | 'price' | 'stock', value: string | number) => { + setNewProduct(prev => ({ + ...prev, + [key]: value, + })); + }; + + const resetNewProduct = () => { + setNewProduct({ + name: '', + price: 0, + stock: 0, + discounts: [], + }); + }; + + return { newProduct, onChangeNewProduct, resetNewProduct }; +}; diff --git a/src/refactoring/hooks/useToggle.ts b/src/refactoring/hooks/useToggle.ts new file mode 100644 index 00000000..269268c5 --- /dev/null +++ b/src/refactoring/hooks/useToggle.ts @@ -0,0 +1,19 @@ +import { useState } from 'react'; + +export const useToggle = (initialState: boolean) => { + const [state, setState] = useState(initialState); + + const toggle = () => { + setState((prev: boolean) => !prev); + }; + + const onClose = () => { + setState(false); + }; + + const onOpen = () => { + setState(true); + }; + + return { state, toggle, onOpen, onClose }; +}; diff --git a/src/refactoring/hooks/useToggleSet.ts b/src/refactoring/hooks/useToggleSet.ts new file mode 100644 index 00000000..d23ec25e --- /dev/null +++ b/src/refactoring/hooks/useToggleSet.ts @@ -0,0 +1,19 @@ +import { useState } from 'react'; + +export const useToggleSet = (initialState: Set) => { + const [items, setItems] = useState>(initialState); + + const toggle = (item: string) => { + setItems((prev: Set) => { + const newSet = new Set(prev); + if (newSet.has(item)) { + newSet.delete(item); + } else { + newSet.add(item); + } + return newSet; + }); + }; + + return { items, toggle }; +};