From 3788b4df92e9834dda2f4e62e25507e569d17c1b Mon Sep 17 00:00:00 2001 From: anne-hyeyeon Date: Wed, 15 Jan 2025 22:49:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20hooks,=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 392 ++++++++++++++++++++--- src/origin/App.tsx | 61 ++-- src/refactoring/App.tsx | 53 +-- src/refactoring/components/AdminPage.tsx | 219 +++++++++---- src/refactoring/components/CartPage.tsx | 96 +++--- src/refactoring/hooks/useCart.ts | 35 +- src/refactoring/hooks/useCoupon.ts | 10 +- src/refactoring/hooks/useProduct.ts | 20 +- src/refactoring/models/cart.ts | 83 ++++- 10 files changed, 753 insertions(+), 217 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..4e1b45f8 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.3': + resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@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,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -958,6 +1051,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 +1085,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 +1104,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 +1141,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 +1184,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 +1246,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 +1274,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 +1289,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 +1376,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 +1410,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.3': + 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: 10.4.3 + '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 @@ -1304,6 +1463,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 +1901,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 +1915,8 @@ snapshots: acorn@8.13.0: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -1765,6 +1946,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} brace-expansion@1.1.11: @@ -1822,6 +2005,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 +2019,38 @@ snapshots: css.escape@1.5.1: {} + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.3 + 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 +2204,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 +2229,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 +2270,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 +2280,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 +2337,8 @@ snapshots: loupe@3.1.2: {} + lru-cache@10.4.3: {} + lz-string@1.5.0: {} magic-string@0.30.12: @@ -2089,6 +2352,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 +2376,8 @@ snapshots: natural-compare@1.4.0: {} + nwsapi@2.2.16: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2128,6 +2399,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 +2425,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.3.3: {} - pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -2207,10 +2480,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 +2532,8 @@ snapshots: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -2268,12 +2551,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 +2610,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 +2633,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 +2645,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 +2673,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/origin/App.tsx b/src/origin/App.tsx index bf82b8f4..df186475 100644 --- a/src/origin/App.tsx +++ b/src/origin/App.tsx @@ -1,45 +1,48 @@ -import { useState } from 'react'; -import { CartPage } from './components/CartPage.tsx'; -import { AdminPage } from './components/AdminPage.tsx'; -import { Coupon, Product } from '../types.ts'; +import { useState } from "react"; +import { CartPage } from "./components/CartPage.tsx"; +import { AdminPage } from "./components/AdminPage.tsx"; +import { Coupon, Product } from "../types.ts"; const initialProducts: Product[] = [ { - id: 'p1', - name: '상품1', + id: "p1", + name: "상품1", price: 10000, stock: 20, - discounts: [{ quantity: 10, rate: 0.1 }, { quantity: 20, rate: 0.2 }] + discounts: [ + { quantity: 10, rate: 0.1 }, + { quantity: 20, rate: 0.2 }, + ], }, { - id: 'p2', - name: '상품2', + id: "p2", + name: "상품2", price: 20000, stock: 20, - discounts: [{ quantity: 10, rate: 0.15 }] + discounts: [{ quantity: 10, rate: 0.15 }], }, { - id: 'p3', - name: '상품3', + id: "p3", + name: "상품3", price: 30000, stock: 20, - discounts: [{ quantity: 10, rate: 0.2 }] - } + discounts: [{ quantity: 10, rate: 0.2 }], + }, ]; const initialCoupons: Coupon[] = [ { - name: '5000원 할인 쿠폰', - code: 'AMOUNT5000', - discountType: 'amount', - discountValue: 5000 + name: "5000원 할인 쿠폰", + code: "AMOUNT5000", + discountType: "amount", + discountValue: 5000, }, { - name: '10% 할인 쿠폰', - code: 'PERCENT10', - discountType: 'percentage', - discountValue: 10 - } + name: "10% 할인 쿠폰", + code: "PERCENT10", + discountType: "percentage", + discountValue: 10, + }, ]; const App = () => { @@ -48,17 +51,17 @@ const App = () => { const [isAdmin, setIsAdmin] = useState(false); const handleProductUpdate = (updatedProduct: Product) => { - setProducts(prevProducts => - prevProducts.map(p => p.id === updatedProduct.id ? updatedProduct : p) + setProducts((prevProducts) => + prevProducts.map((p) => (p.id === updatedProduct.id ? updatedProduct : p)) ); }; const handleProductAdd = (newProduct: Product) => { - setProducts(prevProducts => [...prevProducts, newProduct]); + setProducts((prevProducts) => [...prevProducts, newProduct]); }; const handleCouponAdd = (newCoupon: Coupon) => { - setCoupons(prevCoupons => [...prevCoupons, newCoupon]); + setCoupons((prevCoupons) => [...prevCoupons, newCoupon]); }; return ( @@ -70,7 +73,7 @@ const App = () => { onClick={() => setIsAdmin(!isAdmin)} className="bg-white text-blue-600 px-4 py-2 rounded hover:bg-blue-100" > - {isAdmin ? '장바구니 페이지로' : '관리자 페이지로'} + {isAdmin ? "장바구니 페이지로" : "관리자 페이지로"} @@ -84,7 +87,7 @@ const App = () => { onCouponAdd={handleCouponAdd} /> ) : ( - + )} diff --git a/src/refactoring/App.tsx b/src/refactoring/App.tsx index 31307173..e64cecb2 100644 --- a/src/refactoring/App.tsx +++ b/src/refactoring/App.tsx @@ -1,46 +1,49 @@ -import { useState } from 'react'; -import { CartPage } from './components/CartPage.tsx'; -import { AdminPage } from './components/AdminPage.tsx'; -import { Coupon, Product } from '../types.ts'; +import { useState } from "react"; +import { CartPage } from "./components/CartPage.tsx"; +import { AdminPage } from "./components/AdminPage.tsx"; +import { Coupon, Product } from "../types.ts"; import { useCoupons, useProducts } from "./hooks"; const initialProducts: Product[] = [ { - id: 'p1', - name: '상품1', + id: "p1", + name: "상품1", price: 10000, stock: 20, - discounts: [{ quantity: 10, rate: 0.1 }, { quantity: 20, rate: 0.2 }] + discounts: [ + { quantity: 10, rate: 0.1 }, + { quantity: 20, rate: 0.2 }, + ], }, { - id: 'p2', - name: '상품2', + id: "p2", + name: "상품2", price: 20000, stock: 20, - discounts: [{ quantity: 10, rate: 0.15 }] + discounts: [{ quantity: 10, rate: 0.15 }], }, { - id: 'p3', - name: '상품3', + id: "p3", + name: "상품3", price: 30000, stock: 20, - discounts: [{ quantity: 10, rate: 0.2 }] - } + discounts: [{ quantity: 10, rate: 0.2 }], + }, ]; const initialCoupons: Coupon[] = [ { - name: '5000원 할인 쿠폰', - code: 'AMOUNT5000', - discountType: 'amount', - discountValue: 5000 + name: "5000원 할인 쿠폰", + code: "AMOUNT5000", + discountType: "amount", + discountValue: 5000, }, { - name: '10% 할인 쿠폰', - code: 'PERCENT10', - discountType: 'percentage', - discountValue: 10 - } + name: "10% 할인 쿠폰", + code: "PERCENT10", + discountType: "percentage", + discountValue: 10, + }, ]; const App = () => { @@ -57,7 +60,7 @@ const App = () => { onClick={() => setIsAdmin(!isAdmin)} className="bg-white text-blue-600 px-4 py-2 rounded hover:bg-blue-100" > - {isAdmin ? '장바구니 페이지로' : '관리자 페이지로'} + {isAdmin ? "장바구니 페이지로" : "관리자 페이지로"} @@ -71,7 +74,7 @@ const App = () => { onCouponAdd={addCoupon} /> ) : ( - + )} diff --git a/src/refactoring/components/AdminPage.tsx b/src/refactoring/components/AdminPage.tsx index 4ff2bbcd..4b26713d 100644 --- a/src/refactoring/components/AdminPage.tsx +++ b/src/refactoring/components/AdminPage.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Coupon, Discount, Product } from '../../types.ts'; +import { useState } from "react"; +import { Coupon, Discount, Product } from "../../types.ts"; interface Props { products: Product[]; @@ -9,26 +9,35 @@ interface Props { onCouponAdd: (newCoupon: Coupon) => void; } -export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, onCouponAdd }: Props) => { +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 [newDiscount, setNewDiscount] = useState({ + quantity: 0, + rate: 0, + }); const [newCoupon, setNewCoupon] = useState({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 + name: "", + code: "", + discountType: "percentage", + discountValue: 0, }); const [showNewProductForm, setShowNewProductForm] = useState(false); - const [newProduct, setNewProduct] = useState>({ - name: '', + const [newProduct, setNewProduct] = useState>({ + name: "", price: 0, stock: 0, - discounts: [] + discounts: [], }); const toggleProductAccordion = (productId: string) => { - setOpenProductIds(prev => { + setOpenProductIds((prev) => { const newSet = new Set(prev); if (newSet.has(productId)) { newSet.delete(productId); @@ -39,28 +48,28 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on }); }; - // handleEditProduct 함수 수정 const handleEditProduct = (product: Product) => { - setEditingProduct({...product}); + setEditingProduct({ ...product }); }; - // 새로운 핸들러 함수 추가 const handleProductNameUpdate = (productId: string, newName: string) => { if (editingProduct && editingProduct.id === productId) { - const updatedProduct = { ...editingProduct, name: newName }; - setEditingProduct(updatedProduct); + setEditingProduct({ ...editingProduct, name: newName }); } }; - // 새로운 핸들러 함수 추가 const handlePriceUpdate = (productId: string, newPrice: number) => { if (editingProduct && editingProduct.id === productId) { - const updatedProduct = { ...editingProduct, price: newPrice }; - setEditingProduct(updatedProduct); + setEditingProduct({ ...editingProduct, price: newPrice }); + } + }; + + const handleStockUpdate = (productId: string, newStock: number) => { + if (editingProduct && editingProduct.id === productId) { + setEditingProduct({ ...editingProduct, stock: newStock }); } }; - // 수정 완료 핸들러 함수 추가 const handleEditComplete = () => { if (editingProduct) { onProductUpdate(editingProduct); @@ -68,21 +77,12 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on } }; - 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); + const updatedProduct = products.find((p) => p.id === productId); if (updatedProduct && editingProduct) { const newProduct = { ...updatedProduct, - discounts: [...updatedProduct.discounts, newDiscount] + discounts: [...updatedProduct.discounts, newDiscount], }; onProductUpdate(newProduct); setEditingProduct(newProduct); @@ -91,11 +91,11 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on }; const handleRemoveDiscount = (productId: string, index: number) => { - const updatedProduct = products.find(p => p.id === productId); + const updatedProduct = products.find((p) => p.id === productId); if (updatedProduct) { const newProduct = { ...updatedProduct, - discounts: updatedProduct.discounts.filter((_, i) => i !== index) + discounts: updatedProduct.discounts.filter((_, i) => i !== index), }; onProductUpdate(newProduct); setEditingProduct(newProduct); @@ -105,10 +105,10 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on const handleAddCoupon = () => { onCouponAdd(newCoupon); setNewCoupon({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 + name: "", + code: "", + discountType: "percentage", + discountValue: 0, }); }; @@ -116,10 +116,10 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on const productWithId = { ...newProduct, id: Date.now().toString() }; onProductAdd(productWithId); setNewProduct({ - name: '', + name: "", price: 0, stock: 0, - discounts: [] + discounts: [], }); setShowNewProductForm(false); }; @@ -134,38 +134,65 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on onClick={() => setShowNewProductForm(!showNewProductForm)} className="bg-green-500 text-white px-4 py-2 rounded mb-4 hover:bg-green-600" > - {showNewProductForm ? '취소' : '새 상품 추가'} + {showNewProductForm ? "취소" : "새 상품 추가"} {showNewProductForm && (

새 상품 추가

- + setNewProduct({ ...newProduct, name: e.target.value })} + onChange={(e) => + setNewProduct({ ...newProduct, name: e.target.value }) + } className="w-full p-2 border rounded" />
- + setNewProduct({ ...newProduct, price: parseInt(e.target.value) })} + onChange={(e) => + setNewProduct({ + ...newProduct, + price: parseInt(e.target.value), + }) + } className="w-full p-2 border rounded" />
- + setNewProduct({ ...newProduct, stock: parseInt(e.target.value) })} + onChange={(e) => + setNewProduct({ + ...newProduct, + stock: parseInt(e.target.value), + }) + } className="w-full p-2 border rounded" />
@@ -179,7 +206,11 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on )}
{products.map((product, index) => ( -
+
@@ -205,7 +241,12 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on handlePriceUpdate(product.id, parseInt(e.target.value))} + onChange={(e) => + handlePriceUpdate( + product.id, + parseInt(e.target.value) + ) + } className="w-full p-2 border rounded" />
@@ -214,18 +255,32 @@ export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, on handleStockUpdate(product.id, parseInt(e.target.value))} + onChange={(e) => + handleStockUpdate( + product.id, + parseInt(e.target.value) + ) + } className="w-full p-2 border rounded" />
- {/* 할인 정보 수정 부분 */}
-

할인 정보

+

+ 할인 정보 +

{editingProduct.discounts.map((discount, index) => ( -
- {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인 +
+ + {discount.quantity}개 이상 구매 시{" "} + {discount.rate * 100}% 할인 + -
- -
- {isAdmin ? ( - - ) : ( - - )} -
-
+ + +
{currentPage}
+
); }; diff --git a/src/refactoring/components/AdminPage.tsx b/src/refactoring/components/Admin/AdminPage.tsx similarity index 67% rename from src/refactoring/components/AdminPage.tsx rename to src/refactoring/components/Admin/AdminPage.tsx index 4b26713d..ca578a61 100644 --- a/src/refactoring/components/AdminPage.tsx +++ b/src/refactoring/components/Admin/AdminPage.tsx @@ -1,5 +1,12 @@ import { useState } from "react"; -import { Coupon, Discount, Product } from "../../types.ts"; +import { Coupon, Product } from "../../../types.ts"; +import { + useCouponManager, + useDiscountManager, + useNewProductManager, + useProductEditor, +} from "./hooks"; +import { formatCouponDisplay } from "../../models"; interface Props { products: Product[]; @@ -17,125 +24,56 @@ export const AdminPage = ({ 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; - }); - }; + const { + editingProduct, + setEditingProduct, + handleEditProduct, + handleProductFieldUpdate, + handleEditComplete, + } = useProductEditor(onProductUpdate); - const handleEditProduct = (product: Product) => { - setEditingProduct({ ...product }); - }; + const { newCoupon, handleCouponChange, handleAddCoupon } = + useCouponManager(onCouponAdd); - const handleProductNameUpdate = (productId: string, newName: string) => { - if (editingProduct && editingProduct.id === productId) { - setEditingProduct({ ...editingProduct, name: newName }); - } - }; + const { + newDiscount, + handleAddDiscount, + handleRemoveDiscount, + handleDiscountChange, + } = useDiscountManager(onProductUpdate); - const handlePriceUpdate = (productId: string, newPrice: number) => { - if (editingProduct && editingProduct.id === productId) { - setEditingProduct({ ...editingProduct, price: newPrice }); - } - }; - - const handleStockUpdate = (productId: string, newStock: number) => { - if (editingProduct && editingProduct.id === productId) { - setEditingProduct({ ...editingProduct, stock: newStock }); - } - }; - - const handleEditComplete = () => { - if (editingProduct) { - onProductUpdate(editingProduct); - setEditingProduct(null); - } - }; - - 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 { + showNewProductForm, + newProduct, + handleProductFieldChange, + handleAddNewProduct, + toggleNewProductForm, + } = useNewProductManager(onProductAdd); - 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: [], + const toggleProductAccordion = (productId: string) => { + setOpenProductIds((prev) => { + const newSet = new Set(prev); + newSet.has(productId) ? newSet.delete(productId) : newSet.add(productId); + return newSet; }); - setShowNewProductForm(false); }; return (

관리자 페이지

+ {/* 상품 관리 섹션 */}

상품 관리

+ + {/* 새 상품 추가 폼 */} {showNewProductForm && (

새 상품 추가

@@ -151,7 +89,7 @@ export const AdminPage = ({ type="text" value={newProduct.name} onChange={(e) => - setNewProduct({ ...newProduct, name: e.target.value }) + handleProductFieldChange("name", e.target.value) } className="w-full p-2 border rounded" /> @@ -168,10 +106,7 @@ export const AdminPage = ({ type="number" value={newProduct.price} onChange={(e) => - setNewProduct({ - ...newProduct, - price: parseInt(e.target.value), - }) + handleProductFieldChange("price", e.target.value) } className="w-full p-2 border rounded" /> @@ -188,10 +123,7 @@ export const AdminPage = ({ type="number" value={newProduct.stock} onChange={(e) => - setNewProduct({ - ...newProduct, - stock: parseInt(e.target.value), - }) + handleProductFieldChange("stock", e.target.value) } className="w-full p-2 border rounded" /> @@ -204,7 +136,9 @@ export const AdminPage = ({
)} -
+ + {/* 상품 목록 */} +
{products.map((product, index) => (
{editingProduct && editingProduct.id === product.id ? (
+ {/* 상품 수정 폼 */}
- handleProductNameUpdate( + handleProductFieldUpdate( product.id, + "name", e.target.value ) } className="w-full p-2 border rounded" /> -
-
- - handlePriceUpdate( + handleProductFieldUpdate( product.id, + "price", parseInt(e.target.value) ) } className="w-full p-2 border rounded" /> -
-
- - handleStockUpdate( + handleProductFieldUpdate( product.id, + "stock", parseInt(e.target.value) ) } className="w-full p-2 border rounded" />
+ {/* 할인 정보 수정 */}

할인 정보 @@ -278,9 +211,13 @@ export const AdminPage = ({ {discount.rate * 100}% 할인

+ + {/* 쿠폰 관리 섹션 */}

쿠폰 관리

@@ -360,28 +305,24 @@ export const AdminPage = ({ type="text" placeholder="쿠폰 이름" value={newCoupon.name} - onChange={(e) => - setNewCoupon({ ...newCoupon, name: e.target.value }) - } + onChange={(e) => handleCouponChange("name", e.target.value)} className="w-full p-2 border rounded" /> - setNewCoupon({ ...newCoupon, code: e.target.value }) - } + onChange={(e) => handleCouponChange("code", e.target.value)} className="w-full p-2 border rounded" />
+ {selectedCoupon && (

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

)}
@@ -175,12 +178,12 @@ export const CartPage = ({ products, coupons }: Props) => {

주문 요약

-

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

+

상품 금액: {formatPrice(totalBeforeDiscount)}

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

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

diff --git a/src/refactoring/components/Cart/index.ts b/src/refactoring/components/Cart/index.ts new file mode 100644 index 00000000..3077b727 --- /dev/null +++ b/src/refactoring/components/Cart/index.ts @@ -0,0 +1 @@ +export { CartPage } from "./CartPage"; diff --git a/src/refactoring/components/Shared/Layout.tsx b/src/refactoring/components/Shared/Layout.tsx new file mode 100644 index 00000000..546e8894 --- /dev/null +++ b/src/refactoring/components/Shared/Layout.tsx @@ -0,0 +1,3 @@ +export const Layout = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); diff --git a/src/refactoring/components/Shared/Navigation.tsx b/src/refactoring/components/Shared/Navigation.tsx new file mode 100644 index 00000000..4fb95ffd --- /dev/null +++ b/src/refactoring/components/Shared/Navigation.tsx @@ -0,0 +1,19 @@ +export const Navigation = ({ + isAdmin, + onToggle, +}: { + isAdmin: boolean; + onToggle: () => void; +}) => ( + +); diff --git a/src/refactoring/components/Shared/index.ts b/src/refactoring/components/Shared/index.ts new file mode 100644 index 00000000..5cca6790 --- /dev/null +++ b/src/refactoring/components/Shared/index.ts @@ -0,0 +1,2 @@ +export * from "./Layout"; +export * from "./Navigation"; diff --git a/src/refactoring/constants/index.ts b/src/refactoring/constants/index.ts new file mode 100644 index 00000000..b313b308 --- /dev/null +++ b/src/refactoring/constants/index.ts @@ -0,0 +1 @@ +export * from "./initialData"; diff --git a/src/refactoring/constants/initialData.ts b/src/refactoring/constants/initialData.ts new file mode 100644 index 00000000..6e75a024 --- /dev/null +++ b/src/refactoring/constants/initialData.ts @@ -0,0 +1,63 @@ +import { Coupon, Discount, Product } from "../../types"; + +export const initialProducts: Product[] = [ + { + id: "p1", + name: "상품1", + price: 10000, + stock: 20, + discounts: [ + { quantity: 10, rate: 0.1 }, + { quantity: 20, rate: 0.2 }, + ], + }, + { + id: "p2", + name: "상품2", + price: 20000, + stock: 20, + discounts: [{ quantity: 10, rate: 0.15 }], + }, + { + id: "p3", + name: "상품3", + price: 30000, + stock: 20, + discounts: [{ quantity: 10, rate: 0.2 }], + }, +]; + +export const initialCoupons: Coupon[] = [ + { + name: "5000원 할인 쿠폰", + code: "AMOUNT5000", + discountType: "amount", + discountValue: 5000, + }, + { + name: "10% 할인 쿠폰", + code: "PERCENT10", + discountType: "percentage", + discountValue: 10, + }, +]; + +// AdminPage.tsx +export const INITIAL_DISCOUNT: Discount = { + quantity: 0, + rate: 0, +}; + +export const INITIAL_COUPON: Coupon = { + name: "", + code: "", + discountType: "percentage", + discountValue: 0, +}; + +export const INITIAL_PRODUCT: Omit = { + name: "", + price: 0, + stock: 0, + discounts: [], +}; diff --git a/src/refactoring/hooks/useCoupon.ts b/src/refactoring/hooks/useCoupon.ts index ecab6ee2..e6ca4ce0 100644 --- a/src/refactoring/hooks/useCoupon.ts +++ b/src/refactoring/hooks/useCoupon.ts @@ -1,5 +1,5 @@ -import { Coupon } from "../../types"; import { useState } from "react"; +import { Coupon } from "../../types"; export const useCoupons = (initialCoupons: Coupon[]) => { const [coupons, setCoupons] = useState(initialCoupons); diff --git a/src/refactoring/models/cart.ts b/src/refactoring/models/cart.ts index 753bbcc1..6eba785a 100644 --- a/src/refactoring/models/cart.ts +++ b/src/refactoring/models/cart.ts @@ -1,52 +1,79 @@ -import { CartItem, Coupon } from "../../types"; +import { CartItem, Coupon, Discount, Product } from "../../types"; -export const calculateItemTotal = (item: CartItem) => { - const { product, quantity } = item; - const baseTotal = calculateBaseTotal(product.price, quantity); - const maxDiscountRate = getMaxDiscountRate(product.discounts, quantity); - const finalTotal = applyDiscount(baseTotal, maxDiscountRate); - - return Math.round(finalTotal); -}; - -const calculateBaseTotal = (price: number, quantity: number): number => { - return price * quantity; -}; - -const getMaxDiscountRate = ( - discounts: { quantity: number; rate: number }[], +// 할인 관련 함수 +// 상품의 최대 할인율 계산 +export const getMaxDiscountRate = ( + discounts: Discount[], quantity: number ): number => { return discounts.reduce((maxRate, discount) => { - if (!isDiscountApplicable(discount, quantity)) { - return maxRate; + if (quantity >= discount.quantity) { + return Math.max(maxRate, discount.rate); } - return Math.max(maxRate, discount.rate); + return maxRate; }, 0); }; -const isDiscountApplicable = ( - discount: { quantity: number; rate: number }, - quantity: number -): boolean => { - return quantity >= discount.quantity; +// 최대 할인율 문자열 표시 +export const getMaxDiscountDisplay = (product: Product) => + product.discounts.length > 0 + ? `최대 ${(getMaxDiscountRate(product.discounts, 1) * 100).toFixed( + 0 + )}% 할인` + : null; + +// 특정 장바구니 아이템의 최대 할인율 계산 +export const getMaxApplicableDiscount = (item: CartItem) => { + const { product, quantity } = item; + return getMaxDiscountRate(product.discounts, quantity); }; -const applyDiscount = (amount: number, discountRate: number): number => { - return amount * (1 - discountRate); +// 쿠폰 관련 함수 +// 쿠폰 할인 적용 +export const applyCouponDiscount = ( + amount: number, + coupon: Coupon | null +): number => { + if (!coupon) return amount; + + if (coupon.discountType === "amount") { + return Math.max(0, amount - coupon.discountValue); + } + + return amount * (1 - coupon.discountValue / 100); }; -export const getMaxApplicableDiscount = (item: CartItem) => { +// 쿠폰 할인 표시 형식 +export const formatCouponDisplay = (coupon: Coupon) => { + return coupon.discountType === "amount" + ? `${coupon.discountValue}원` + : `${coupon.discountValue}%`; +}; + +// 장바구니 관련 함수 +// 단일 장바구니 아이템의 최종 가격 계산 +export const calculateItemTotal = (item: CartItem) => { const { product, quantity } = item; - return getMaxDiscountRate(product.discounts, quantity); + const baseTotal = product.price * quantity; + const maxDiscountRate = getMaxDiscountRate(product.discounts, quantity); + return Math.round(baseTotal * (1 - maxDiscountRate)); }; +// 전체 장바구니 총액 계산 export const calculateCartTotal = ( cart: CartItem[], selectedCoupon: Coupon | null ) => { - const totalBeforeDiscount = calculateTotalBeforeDiscount(cart); - const totalAfterItemDiscounts = calculateTotalAfterItemDiscounts(cart); + const totalBeforeDiscount = cart.reduce( + (total, item) => total + item.product.price * item.quantity, + 0 + ); + + const totalAfterItemDiscounts = cart.reduce( + (total, item) => total + calculateItemTotal(item), + 0 + ); + const totalAfterCoupon = applyCouponDiscount( totalAfterItemDiscounts, selectedCoupon @@ -59,28 +86,7 @@ export const calculateCartTotal = ( }; }; -const calculateTotalBeforeDiscount = (cart: CartItem[]): number => { - return cart.reduce((total, item) => { - return total + item.product.price * item.quantity; - }, 0); -}; - -const calculateTotalAfterItemDiscounts = (cart: CartItem[]): number => { - return cart.reduce((total, item) => { - return total + calculateItemTotal(item); - }, 0); -}; - -const applyCouponDiscount = (amount: number, coupon: Coupon | null): number => { - if (!coupon) return amount; - - if (coupon.discountType === "amount") { - return Math.max(0, amount - coupon.discountValue); - } - - return amount * (1 - coupon.discountValue / 100); -}; - +// 장바구니 아이템 수량 업데이트 export const updateCartItemQuantity = ( cart: CartItem[], productId: string, @@ -89,11 +95,15 @@ export const updateCartItemQuantity = ( return cart .map((item) => { if (item.product.id !== productId) return item; - if (newQuantity <= 0) return null; - const updatedQuantity = Math.min(newQuantity, item.product.stock); return { ...item, quantity: updatedQuantity }; }) .filter((item): item is CartItem => item !== null); }; + +// 특정 상품의 남은 재고 계산 +export const getRemainingStock = (product: Product, cart: CartItem[]) => { + const cartItem = cart.find((item) => item.product.id === product.id); + return product.stock - (cartItem?.quantity || 0); +}; diff --git a/src/refactoring/models/index.ts b/src/refactoring/models/index.ts new file mode 100644 index 00000000..ca807410 --- /dev/null +++ b/src/refactoring/models/index.ts @@ -0,0 +1 @@ +export * from "./cart.ts"; diff --git a/src/refactoring/utils/index.ts b/src/refactoring/utils/index.ts new file mode 100644 index 00000000..178cd64f --- /dev/null +++ b/src/refactoring/utils/index.ts @@ -0,0 +1 @@ +export * from "./utils"; diff --git a/src/refactoring/utils/utils.ts b/src/refactoring/utils/utils.ts new file mode 100644 index 00000000..7af0436c --- /dev/null +++ b/src/refactoring/utils/utils.ts @@ -0,0 +1,5 @@ +import { Coupon } from "../../types"; + +export const formatPrice = (price: number) => { + return `${price.toLocaleString()}원`; +};