diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 1a976374..0b50c0cb 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -7,21 +7,61 @@
- Component에서 비즈니스 로직을 분리하기
- 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
-- [ ] Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
-- [ ] 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
-- [ ] 계산함수는 순수함수로 작성이 되었나요?
+- [x] Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
+- [x] 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
+- [x] 계산함수는 순수함수로 작성이 되었나요?
### 심화과제
- 뷰데이터와 엔티티데이터의 분리에 대한 이해
- 엔티티 -> 리파지토리 -> 유즈케이스 -> UI 계층에 대한 이해
-- [ ] Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
-- [ ] 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
-- [ ] 계산함수는 순수함수로 작성이 되었나요?
-- [ ] 특정 Entitiy만 다루는 함수는 분리되어 있나요?
-- [ ] 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
-- [ ] 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
+- [x] Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
+- [x] 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
+- [x] 계산함수는 순수함수로 작성이 되었나요?
+- [x] 특정 Entitiy만 다루는 함수는 분리되어 있나요?
+- [x] 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
+- [x] 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
+
+## 과제를 시작하면서
+어떻게 시작해야할까…?
+
+발제로 알 수 있는 이번 과제의 목표
+* 모든 소프트웨어에 적절히 존재하는 주요 계층에 따라 적절히 리팩토링 할 수 있다
+
+여기서 React 의 주요 역할계층은 세 가지가 있다
+* Hook (훅)
+* Calc function (계산 함수)
+* Component (컴포넌트)
+
+각각 계층은 무슨 역할을 하는지 자세히 알아보자
+1. hooks 에서는 리액트 훅을 사용해서 엔티티를 다룬다
+2. utils 에서는 계산 로직이 포함 된 순수 함수만을 다룬다
+3. components 에서는 hooks 와 utils 에 비지니스 로직을 맡기고
합성 컴포넌트를 적절히 수행해 책임을 나눈다
+
+###### (지피티 첨가 버전)
+
+최종 설계 단계
+1. 커스텀 훅에서 데이터와 로직 분리 - 훅 계층
+ * 상태 관리와 비즈니스 로직은 커스텀 훅에서 담당합니다
+ * map, filter, reduce 등의 메서드를 활용해 데이터를 변환하거나 조건에 따른 동작을 처리합니다
+2. UI 컴포넌트를 합성 컴포넌트 패턴으로 설계 - 컴포넌트 계층
+ * UI 컴포넌트를 작은 단위로 나누고, Props를 활용해 데이터를 전달받아 렌더링합니다
+ * UI와 로직 간의 의존성을 제거합니다
+3. 부모 컴포넌트에서 데이터와 UI 조합 - 컴포넌트 계층
+ * 커스텀 훅에서 데이터를 가져오고, UI 컴포넌트에 전달하여 상태를 표시합니다
+ * 자식 Props 패턴으로 특정 UI를 부모에서 제어할 수 있도록 설계합니다
+4. 확장성과 재사용성을 위한 설계 - 훅 및 계산 함수 계층
+ * Render Props, Slot 패턴 등을 활용하여 더 유연한 UI 구성을 지원합니다
+ * 특정 컴포넌트에서 사용하는 로직이나 이벤트를 쉽게 대체하거나 확장할 수 있도록 설계합니다
+
+위 설계로 얻는 리팩토링 결과의 장점은
+* 관심사의 분리: 데이터 관리, 로직, UI가 명확히 분리되어 유지보수가 용이하다
+* 유연성: 자식 Props 패턴과 합성 컴포넌트로 UI 구성을 유연하게 제어할 수 있다
+* 재사용성: 커스텀 훅과 컴포넌트가 독립적으로 설계되어 다양한 컨텍스트에서 재사용 가능하다
+* 테스트 용이성: 상태 관리와 로직, UI를 독립적으로 테스트할 수 있다
+
+#### 이제 가장 먼저 커스텀 훅 로직을 만들어보자
## 과제 셀프회고
@@ -31,6 +71,13 @@
### 과제를 하면서 새롭게 알게된 점
+
+
### 과제를 진행하면서 아직 애매하게 잘 모르겠다 하는 점, 혹은 뭔가 잘 안되서 아쉬운 것들
## 리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
diff --git a/package.json b/package.json
index d3f7e3de..6f5b8363 100644
--- a/package.json
+++ b/package.json
@@ -28,9 +28,10 @@
"@typescript-eslint/parser": "^8.10.0",
"@vitejs/plugin-react-swc": "^3.7.1",
"@vitest/ui": "^2.1.3",
- "eslint": "^9.12.0",
+ "eslint": "^8.57.1",
"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..bfb95d81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,10 +32,10 @@ importers:
version: 18.3.1
'@typescript-eslint/eslint-plugin':
specifier: ^8.10.0
- version: 8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)
+ version: 8.10.0(@typescript-eslint/parser@8.10.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
'@typescript-eslint/parser':
specifier: ^8.10.0
- version: 8.10.0(eslint@9.12.0)(typescript@5.6.3)
+ version: 8.10.0(eslint@8.57.1)(typescript@5.6.3)
'@vitejs/plugin-react-swc':
specifier: ^3.7.1
version: 3.7.1(vite@5.4.9)
@@ -43,17 +43,17 @@ importers:
specifier: ^2.1.3
version: 2.1.3(vitest@2.1.3)
eslint:
- specifier: ^9.12.0
- version: 9.12.0
+ specifier: ^8.57.1
+ version: 8.57.1
eslint-plugin-react-hooks:
specifier: ^5.0.0
- version: 5.0.0(eslint@9.12.0)
+ version: 5.0.0(eslint@8.57.1)
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
+ version: 0.4.12(eslint@8.57.1)
+ 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'}
@@ -236,45 +264,26 @@ packages:
resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
- '@eslint/config-array@0.18.0':
- resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/core@0.6.0':
- resolution: {integrity: sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/eslintrc@3.1.0':
- resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/js@9.12.0':
- resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/object-schema@2.1.4':
- resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/plugin-kit@0.2.0':
- resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/eslintrc@2.1.4':
+ resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- '@humanfs/core@0.19.0':
- resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==}
- engines: {node: '>=18.18.0'}
+ '@eslint/js@8.57.1':
+ resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- '@humanfs/node@0.16.5':
- resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==}
- engines: {node: '>=18.18.0'}
+ '@humanwhocodes/config-array@0.13.0':
+ resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
+ engines: {node: '>=10.10.0'}
+ deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
- '@humanwhocodes/retry@0.3.1':
- resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
- engines: {node: '>=18.18'}
+ '@humanwhocodes/object-schema@2.0.3':
+ resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
+ deprecated: Use @eslint/object-schema instead
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
@@ -484,9 +493,6 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
- '@types/json-schema@7.0.15':
- resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
-
'@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
@@ -553,6 +559,9 @@ packages:
resolution: {integrity: sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@ungap/structured-clone@1.2.1':
+ resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==}
+
'@vitejs/plugin-react-swc@3.7.1':
resolution: {integrity: sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==}
peerDependencies:
@@ -603,6 +612,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 +649,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 +706,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 +720,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 +740,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,16 +750,28 @@ 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'}
+ doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
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'}
@@ -753,31 +796,23 @@ packages:
peerDependencies:
eslint: '>=7'
- eslint-scope@8.1.0:
- resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint-scope@7.2.2:
+ resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- eslint-visitor-keys@4.1.0:
- resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- eslint@9.12.0:
- resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint@8.57.1:
+ resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
- peerDependencies:
- jiti: '*'
- peerDependenciesMeta:
- jiti:
- optional: true
- espree@10.2.0:
- resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ espree@9.6.1:
+ resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
esquery@1.6.0:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
@@ -825,9 +860,9 @@ packages:
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
- file-entry-cache@8.0.0:
- resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
- engines: {node: '>=16.0.0'}
+ file-entry-cache@6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
@@ -837,13 +872,20 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
- flat-cache@4.0.1:
- resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
- engines: {node: '>=16'}
+ flat-cache@3.2.0:
+ resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
+ engines: {node: ^10.12.0 || >=12.0.0}
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'}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -857,9 +899,13 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
- globals@14.0.0:
- resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
- engines: {node: '>=18'}
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
+
+ globals@13.24.0:
+ resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+ engines: {node: '>=8'}
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -872,6 +918,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'}
@@ -888,6 +950,13 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -900,6 +969,13 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+
+ 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 +986,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 +1028,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 +1047,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 +1081,12 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ nwsapi@2.2.16:
+ resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -1000,10 +1103,17 @@ 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'}
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -1034,11 +1144,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}
@@ -1077,14 +1182,29 @@ packages:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
rollup@4.24.0:
resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==}
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==}
@@ -1118,6 +1238,10 @@ packages:
std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@@ -1134,6 +1258,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 +1286,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 +1301,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'}
@@ -1177,6 +1319,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+
typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'}
@@ -1246,6 +1392,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 +1426,44 @@ 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'}
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- 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':
+ bufferutil:
optional: true
- immer:
- optional: true
- react:
- 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 +1482,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
@@ -1373,29 +1571,19 @@ snapshots:
'@esbuild/win32-x64@0.21.5':
optional: true
- '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)':
+ '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)':
dependencies:
- eslint: 9.12.0
+ eslint: 8.57.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.11.1': {}
- '@eslint/config-array@0.18.0':
- dependencies:
- '@eslint/object-schema': 2.1.4
- debug: 4.3.7
- minimatch: 3.1.2
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/core@0.6.0': {}
-
- '@eslint/eslintrc@3.1.0':
+ '@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.12.6
debug: 4.3.7
- espree: 10.2.0
- globals: 14.0.0
+ espree: 9.6.1
+ globals: 13.24.0
ignore: 5.3.2
import-fresh: 3.3.0
js-yaml: 4.1.0
@@ -1404,24 +1592,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint/js@9.12.0': {}
-
- '@eslint/object-schema@2.1.4': {}
-
- '@eslint/plugin-kit@0.2.0':
- dependencies:
- levn: 0.4.1
+ '@eslint/js@8.57.1': {}
- '@humanfs/core@0.19.0': {}
-
- '@humanfs/node@0.16.5':
+ '@humanwhocodes/config-array@0.13.0':
dependencies:
- '@humanfs/core': 0.19.0
- '@humanwhocodes/retry': 0.3.1
+ '@humanwhocodes/object-schema': 2.0.3
+ debug: 4.3.7
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
'@humanwhocodes/module-importer@1.0.1': {}
- '@humanwhocodes/retry@0.3.1': {}
+ '@humanwhocodes/object-schema@2.0.3': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
@@ -1578,8 +1761,6 @@ snapshots:
'@types/estree@1.0.6': {}
- '@types/json-schema@7.0.15': {}
-
'@types/prop-types@15.7.13': {}
'@types/react-dom@18.3.1':
@@ -1591,15 +1772,15 @@ snapshots:
'@types/prop-types': 15.7.13
csstype: 3.1.3
- '@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)':
+ '@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.11.1
- '@typescript-eslint/parser': 8.10.0(eslint@9.12.0)(typescript@5.6.3)
+ '@typescript-eslint/parser': 8.10.0(eslint@8.57.1)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.10.0
- '@typescript-eslint/type-utils': 8.10.0(eslint@9.12.0)(typescript@5.6.3)
- '@typescript-eslint/utils': 8.10.0(eslint@9.12.0)(typescript@5.6.3)
+ '@typescript-eslint/type-utils': 8.10.0(eslint@8.57.1)(typescript@5.6.3)
+ '@typescript-eslint/utils': 8.10.0(eslint@8.57.1)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0
- eslint: 9.12.0
+ eslint: 8.57.1
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@@ -1609,14 +1790,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.10.0(eslint@9.12.0)(typescript@5.6.3)':
+ '@typescript-eslint/parser@8.10.0(eslint@8.57.1)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/types': 8.10.0
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0
debug: 4.3.7
- eslint: 9.12.0
+ eslint: 8.57.1
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
@@ -1627,10 +1808,10 @@ snapshots:
'@typescript-eslint/types': 8.10.0
'@typescript-eslint/visitor-keys': 8.10.0
- '@typescript-eslint/type-utils@8.10.0(eslint@9.12.0)(typescript@5.6.3)':
+ '@typescript-eslint/type-utils@8.10.0(eslint@8.57.1)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
- '@typescript-eslint/utils': 8.10.0(eslint@9.12.0)(typescript@5.6.3)
+ '@typescript-eslint/utils': 8.10.0(eslint@8.57.1)(typescript@5.6.3)
debug: 4.3.7
ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies:
@@ -1656,13 +1837,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.10.0(eslint@9.12.0)(typescript@5.6.3)':
+ '@typescript-eslint/utils@8.10.0(eslint@8.57.1)(typescript@5.6.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0)
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/types': 8.10.0
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
- eslint: 9.12.0
+ eslint: 8.57.1
transitivePeerDependencies:
- supports-color
- typescript
@@ -1672,6 +1853,8 @@ snapshots:
'@typescript-eslint/types': 8.10.0
eslint-visitor-keys: 3.4.3
+ '@ungap/structured-clone@1.2.1': {}
+
'@vitejs/plugin-react-swc@3.7.1(vite@5.4.9)':
dependencies:
'@swc/core': 1.7.36
@@ -1722,7 +1905,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 +1919,8 @@ snapshots:
acorn@8.13.0: {}
+ agent-base@7.1.3: {}
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -1765,6 +1950,8 @@ snapshots:
assertion-error@2.0.1: {}
+ asynckit@0.4.0: {}
+
balanced-match@1.0.2: {}
brace-expansion@1.1.11:
@@ -1822,6 +2009,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 +2023,42 @@ 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: {}
+ doctrine@3.0.0:
+ dependencies:
+ esutils: 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
@@ -1878,68 +2089,69 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-plugin-react-hooks@5.0.0(eslint@9.12.0):
+ eslint-plugin-react-hooks@5.0.0(eslint@8.57.1):
dependencies:
- eslint: 9.12.0
+ eslint: 8.57.1
- eslint-plugin-react-refresh@0.4.12(eslint@9.12.0):
+ eslint-plugin-react-refresh@0.4.12(eslint@8.57.1):
dependencies:
- eslint: 9.12.0
+ eslint: 8.57.1
- eslint-scope@8.1.0:
+ eslint-scope@7.2.2:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
- eslint-visitor-keys@4.1.0: {}
-
- eslint@9.12.0:
+ eslint@8.57.1:
dependencies:
- '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0)
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
'@eslint-community/regexpp': 4.11.1
- '@eslint/config-array': 0.18.0
- '@eslint/core': 0.6.0
- '@eslint/eslintrc': 3.1.0
- '@eslint/js': 9.12.0
- '@eslint/plugin-kit': 0.2.0
- '@humanfs/node': 0.16.5
+ '@eslint/eslintrc': 2.1.4
+ '@eslint/js': 8.57.1
+ '@humanwhocodes/config-array': 0.13.0
'@humanwhocodes/module-importer': 1.0.1
- '@humanwhocodes/retry': 0.3.1
- '@types/estree': 1.0.6
- '@types/json-schema': 7.0.15
+ '@nodelib/fs.walk': 1.2.8
+ '@ungap/structured-clone': 1.2.1
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.7
+ doctrine: 3.0.0
escape-string-regexp: 4.0.0
- eslint-scope: 8.1.0
- eslint-visitor-keys: 4.1.0
- espree: 10.2.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
esquery: 1.6.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
- file-entry-cache: 8.0.0
+ file-entry-cache: 6.0.1
find-up: 5.0.0
glob-parent: 6.0.2
+ globals: 13.24.0
+ graphemer: 1.4.0
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.0
json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
lodash.merge: 4.6.2
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
+ strip-ansi: 6.0.1
text-table: 0.2.0
transitivePeerDependencies:
- supports-color
- espree@10.2.0:
+ espree@9.6.1:
dependencies:
acorn: 8.13.0
acorn-jsx: 5.3.2(acorn@8.13.0)
- eslint-visitor-keys: 4.1.0
+ eslint-visitor-keys: 3.4.3
esquery@1.6.0:
dependencies:
@@ -1981,9 +2193,9 @@ snapshots:
fflate@0.8.2: {}
- file-entry-cache@8.0.0:
+ file-entry-cache@6.0.1:
dependencies:
- flat-cache: 4.0.1
+ flat-cache: 3.2.0
fill-range@7.1.1:
dependencies:
@@ -1994,13 +2206,22 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
- flat-cache@4.0.1:
+ flat-cache@3.2.0:
dependencies:
flatted: 3.3.1
keyv: 4.5.4
+ rimraf: 3.0.2
flatted@3.3.1: {}
+ form-data@4.0.1:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.35
+
+ fs.realpath@1.0.0: {}
+
fsevents@2.3.3:
optional: true
@@ -2012,7 +2233,18 @@ snapshots:
dependencies:
is-glob: 4.0.3
- globals@14.0.0: {}
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ globals@13.24.0:
+ dependencies:
+ type-fest: 0.20.2
graphemer@1.4.0: {}
@@ -2020,6 +2252,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:
@@ -2031,6 +2285,13 @@ snapshots:
indent-string@4.0.0: {}
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
+
is-extglob@2.1.1: {}
is-glob@4.0.3:
@@ -2039,6 +2300,10 @@ snapshots:
is-number@7.0.0: {}
+ is-path-inside@3.0.3: {}
+
+ is-potential-custom-element-name@1.0.1: {}
+
isexe@2.0.0: {}
js-tokens@4.0.0: {}
@@ -2047,6 +2312,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 +2369,8 @@ snapshots:
loupe@3.1.2: {}
+ lru-cache@11.0.2: {}
+
lz-string@1.5.0: {}
magic-string@0.30.12:
@@ -2089,6 +2384,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 +2408,12 @@ snapshots:
natural-compare@1.4.0: {}
+ nwsapi@2.2.16: {}
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -2128,8 +2435,14 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse5@7.2.1:
+ dependencies:
+ entities: 4.5.0
+
path-exists@4.0.0: {}
+ path-is-absolute@1.0.1: {}
+
path-key@3.1.1: {}
pathe@1.1.2: {}
@@ -2150,8 +2463,6 @@ snapshots:
prelude-ls@1.2.1: {}
- prettier@3.3.3: {}
-
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@@ -2185,6 +2496,10 @@ snapshots:
reusify@1.0.4: {}
+ rimraf@3.0.2:
+ dependencies:
+ glob: 7.2.3
+
rollup@4.24.0:
dependencies:
'@types/estree': 1.0.6
@@ -2207,10 +2522,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
@@ -2237,6 +2560,10 @@ snapshots:
std-env@3.7.0: {}
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
strip-indent@3.0.0:
dependencies:
min-indent: 1.0.1
@@ -2251,6 +2578,8 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ symbol-tree@3.2.4: {}
+
text-table@0.2.0: {}
tinybench@2.9.0: {}
@@ -2268,12 +2597,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
@@ -2282,6 +2625,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-fest@0.20.2: {}
+
typescript@5.6.3: {}
uri-js@4.4.1:
@@ -2313,7 +2658,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 +2681,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 +2693,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 +2721,12 @@ snapshots:
word-wrap@1.2.5: {}
- yocto-queue@0.1.0: {}
+ wrappy@1.0.2: {}
- zustand@5.0.0(@types/react@18.3.11)(react@18.3.1):
- optionalDependencies:
- '@types/react': 18.3.11
- react: 18.3.1
+ ws@8.18.0: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ yocto-queue@0.1.0: {}
diff --git a/src/advanced/__tests__/advanced.test.tsx b/src/advanced/__tests__/advanced.test.tsx
index ef8e6ae0..6b80034b 100644
--- a/src/advanced/__tests__/advanced.test.tsx
+++ b/src/advanced/__tests__/advanced.test.tsx
@@ -1,9 +1,11 @@
import { useState } from "react";
import { describe, expect, test } from 'vitest';
-import { act, fireEvent, render, screen, within } from '@testing-library/react';
+import { act, fireEvent, render, screen, within, renderHook } from '@testing-library/react';
import { CartPage } from '../../refactoring/components/CartPage';
import { AdminPage } from "../../refactoring/components/AdminPage";
-import { Coupon, Product } from '../../types';
+import { Coupon, Product, CartItem } from '../../types';
+import * as discountUtils from "../../refactoring/utils/discount";
+import { useAdmin } from "../../refactoring/hooks/useAdmin";
const mockProducts: Product[] = [
{
@@ -232,13 +234,152 @@ describe('advanced > ', () => {
})
describe('자유롭게 작성해보세요.', () => {
- test('새로운 유틸 함수를 만든 후에 테스트 코드를 작성해서 실행해보세요', () => {
- expect(true).toBe(false);
- })
-
- test('새로운 hook 함수르 만든 후에 테스트 코드를 작성해서 실행해보세요', () => {
- expect(true).toBe(false);
- })
- })
-})
+ describe("discountUtils", () => {
+ describe("getMaxDiscount", () => {
+ test("최대 할인율 반환하기", () => {
+ const discounts = mockProducts.flatMap(product => product.discounts)
+ const getMaxDiscount = discountUtils.getMaxDiscount(discounts);
+ expect(getMaxDiscount).toBe(0.2);
+ });
+ });
+
+ describe("getAppliedDiscount", () => {
+ const testProduct: Product = {
+ id: "1",
+ name: "Test Case",
+ price: 100,
+ stock: 10,
+ discounts: [
+ { quantity: 2, rate: 0.1 },
+ { quantity: 5, rate: 0.5 },
+ ],
+ };
+
+ test("상황에 맞는 할인율 반환하기", () => {
+ const firstCase: CartItem = { product: testProduct, quantity: 1 };
+ const secondCase: CartItem = { product: testProduct, quantity: 5 };
+
+ expect(discountUtils.getAppliedDiscount(firstCase)).toBe(0);
+ expect(discountUtils.getAppliedDiscount(secondCase)).toBe(0.5);
+ });
+ });
+ });
+ describe("useAdmin", () => {
+ describe("handleAddCoupon", () => {
+ test("쿠폰이 newCoupon에 잘 초기화(추가) 하기", () => {
+ // 훅을 사용하여 상태와 메서드에 접근
+ const { result } = renderHook(() => useAdmin());
+ const couponValue: Coupon = {
+ name: "New Coupon",
+ code: "NEWCODE",
+ discountType: "amount",
+ discountValue: 5000,
+ };
+ let couponAdded = null;
+ const onCouponAdd = (coupon: Coupon) => {
+ couponAdded = coupon;
+ };
+
+ act(() => {
+ result.current.setNewCoupon(couponValue);
+ });
+ // 비동기 때문인가 따로 해야하는듯 하다
+ act(() => {
+ result.current.handleAddCoupon(onCouponAdd);
+ });
+
+ // `onCouponAdd`가 쿠폰을 정상적으로 받았는지 확인
+ expect(couponAdded).toEqual(couponValue);
+
+ // `newCoupon` 상태가 초기화되었는지 확인
+ expect(result.current.newCoupon).toEqual({
+ name: '',
+ code: '',
+ discountType: 'percentage',
+ discountValue: 0
+ });
+ });
+ });
+
+ describe("handleProductNameUpdate", () => {
+ test("상품 이름을 업데이트하기", () => {
+ const { result } = renderHook(() => useAdmin());
+
+ const product: Product = {
+ id: "1",
+ name: "Old Product",
+ price: 100,
+ stock: 10,
+ discounts: []
+ };
+
+ // handleEditProduct로 업데이트할 상품을 수정
+ act(() => {
+ result.current.handleEditProduct(product);
+ });
+
+ // 이름 업데이트
+ act(() => {
+ result.current.handleProductNameUpdate("1", "New Product");
+ });
+
+ // 이름이 업데이트되었는지 확인
+ expect(result.current.editingProduct?.name).toBe("New Product");
+ });
+ });
+
+ describe("handleAddNewProduct", () => {
+ test("새로운 상품을 추가하기", () => {
+ const { result } = renderHook(() => useAdmin());
+
+ const newProduct = { name: "New Product", price: 100, stock: 10, discounts: [] };
+
+ // newProduct 값을 설정
+ act(() => {
+ result.current.setNewProduct(newProduct);
+ });
+
+ // handleAddNewProduct 실행
+ act(() => {
+ result.current.handleAddNewProduct(() => {});
+ });
+
+ // 상태가 초기화되었는지 확인
+ expect(result.current.newProduct).toEqual({
+ name: '',
+ price: 0,
+ stock: 0,
+ discounts: []
+ });
+ });
+ });
+
+ describe("toggleProductAccordion", () => {
+ test("특정 제품의 accordion을 토글하기", () => {
+ const { result } = renderHook(() => useAdmin());
+
+ // 초기 상태에서 토글이 열린 제품이 없는지 확인
+ expect(result.current.openProductIds.size).toBe(0);
+
+ // product 의 id 가 1인 경우가 케이스
+ act(() => {
+ result.current.toggleProductAccordion("1");
+ });
+
+ // id 가 1 인 product 가 토글되었는지 확인
+ expect(result.current.openProductIds.size).toBe(1);
+ expect(result.current.openProductIds.has("1")).toBe(true);
+
+ // id 가 1 인 accordion 의 토글을 닫기
+ act(() => {
+ result.current.toggleProductAccordion("1");
+ });
+
+ // id 가 1 인 accordion 의 토글이 닫혔는지 확인
+ expect(result.current.openProductIds.size).toBe(0);
+ });
+ });
+ });
+ });
+});
diff --git a/src/basic/__tests__/basic.test.tsx b/src/basic/__tests__/basic.test.tsx
index f8a26836..d4cec078 100644
--- a/src/basic/__tests__/basic.test.tsx
+++ b/src/basic/__tests__/basic.test.tsx
@@ -12,7 +12,7 @@ import { CartPage } from "../../refactoring/components/CartPage";
import { AdminPage } from "../../refactoring/components/AdminPage";
import { CartItem, Coupon, Product } from "../../types";
import { useCart, useCoupons, useProducts } from "../../refactoring/hooks";
-import * as cartUtils from "../../refactoring/models/cart";
+import * as cartUtils from "../../refactoring/utils/cart";
const mockProducts: Product[] = [
{
diff --git a/src/refactoring/App.tsx b/src/refactoring/App.tsx
index 31307173..21eb48a1 100644
--- a/src/refactoring/App.tsx
+++ b/src/refactoring/App.tsx
@@ -1,52 +1,13 @@
-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',
- 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 }]
- }
-];
-
-const initialCoupons: Coupon[] = [
- {
- name: '5000원 할인 쿠폰',
- code: 'AMOUNT5000',
- discountType: 'amount',
- discountValue: 5000
- },
- {
- name: '10% 할인 쿠폰',
- code: 'PERCENT10',
- discountType: 'percentage',
- discountValue: 10
- }
-];
+import { useState } from "react";
+import { CartPage } from "./components/CartPage.tsx";
+import { AdminPage } from "./components/AdminPage.tsx";
+import { initialCoupons, initialProducts } from "./models/datas.ts";
+import { useCoupons, useProducts} from "./hooks";
const App = () => {
- const { products, updateProduct, addProduct } = useProducts(initialProducts);
- const { coupons, addCoupon } = useCoupons(initialCoupons);
const [isAdmin, setIsAdmin] = useState(false);
+ const { products, addProduct, updateProduct } = useProducts(initialProducts);
+ const { coupons, addCoupon } = useCoupons(initialCoupons);
return (
@@ -57,14 +18,14 @@ const App = () => {
onClick={() => setIsAdmin(!isAdmin)}
className="bg-white text-blue-600 px-4 py-2 rounded hover:bg-blue-100"
>
- {isAdmin ? '장바구니 페이지로' : '관리자 페이지로'}
+ {isAdmin ? "장바구니 페이지로" : "관리자 페이지로"}
{isAdmin ? (
- void;
- onProductAdd: (newProduct: Product) => void;
- onCouponAdd: (newCoupon: Coupon) => void;
+ onProductUpdate: (product: Product) => void;
+ onProductAdd: (product: Product) => void;
+ onCouponAdd: (coupon: 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}% 할인
-
- ))}
-
-
- )}
-
- )}
-
- ))}
-
-
-
-
쿠폰 관리
-
-
-
-
현재 쿠폰 목록
-
- {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 bafe5ecb..50daf2d8 100644
--- a/src/refactoring/components/CartPage.tsx
+++ b/src/refactoring/components/CartPage.tsx
@@ -1,45 +1,27 @@
-import { CartItem, Coupon, Product } from '../../types.ts';
import { useCart } from "../hooks";
+import { CartList } from "./carts/CartList.tsx";
+import { CartProduct } from "./carts/CartProduct.tsx";
+import { Coupon, Product } from "../../types.ts";
interface Props {
- products: Product[];
coupons: Coupon[];
+ products: Product[];
}
-export const CartPage = ({ products, coupons }: Props) => {
+export const CartPage = ({coupons, products}: Props) => {
const {
cart,
+ selectedCoupon,
+ totalBeforeDiscount,
+ totalAfterDiscount,
+ totalDiscount,
+ applyCoupon,
addToCart,
- removeFromCart,
+ removeFromCart,
updateQuantity,
- applyCoupon,
- calculateTotal,
- selectedCoupon
+ getRemainingStock
} = useCart();
- 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 (
장바구니
@@ -47,92 +29,27 @@ export const CartPage = ({ products, coupons }: Props) => {
상품 목록
- {products.map(product => {
- const remainingStock = getRemainingStock(product);
- return (
-
-
- {product.name}
- {product.price.toLocaleString()}원
-
-
- 0 ? 'text-green-600' : 'text-red-600'}`}>
- 재고: {remainingStock}개
-
- {product.discounts.length > 0 && (
-
- 최대 {(getMaxDiscount(product.discounts) * 100).toFixed(0)}% 할인
-
- )}
-
- {product.discounts.length > 0 && (
-
- {product.discounts.map((discount, index) => (
- -
- {discount.quantity}개 이상: {(discount.rate * 100).toFixed(0)}% 할인
-
- ))}
-
- )}
-
-
- );
- })}
+ {products.map((product) =>
+
+ )}
장바구니 내역
-
- {cart.map(item => {
- const appliedDiscount = getAppliedDiscount(item);
- return (
-
-
- {item.product.name}
-
-
- {item.product.price}원 x {item.quantity}
- {appliedDiscount > 0 && (
-
- ({(appliedDiscount * 100).toFixed(0)}% 할인 적용)
-
- )}
-
-
-
-
-
-
-
-
- );
- })}
+ {cart.map(item =>
+
+ )}
diff --git a/src/refactoring/components/admin/ManageCoupon.tsx b/src/refactoring/components/admin/ManageCoupon.tsx
new file mode 100644
index 00000000..217b9b25
--- /dev/null
+++ b/src/refactoring/components/admin/ManageCoupon.tsx
@@ -0,0 +1,73 @@
+import { Coupon } from "../../../types";
+import { useAdmin } from "../../hooks/useAdmin";
+
+interface Props {
+ coupons: Coupon[];
+ onCouponAdd: (coupon: Coupon) => void;
+}
+
+export const ManageCoupon = ({ coupons, onCouponAdd }: Props) => {
+ const {
+ newCoupon,
+ setNewCoupon,
+ handleAddCoupon
+ } = useAdmin();
+
+ return (
+
+
쿠폰 관리
+
+
+
+
현재 쿠폰 목록
+
+ {coupons.map((coupon, index) => (
+
+ {coupon.name} ({coupon.code}):
+ {coupon.discountType === 'amount' ? `${coupon.discountValue}원` : `${coupon.discountValue}%`} 할인
+
+ ))}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/refactoring/components/admin/ManageProduct.tsx b/src/refactoring/components/admin/ManageProduct.tsx
new file mode 100644
index 00000000..bfe52f0c
--- /dev/null
+++ b/src/refactoring/components/admin/ManageProduct.tsx
@@ -0,0 +1,192 @@
+import { Product } from "../../../types.ts";
+import { useAdmin } from "../../hooks/useAdmin.ts";
+
+interface Props {
+ products: Product[];
+ onProductUpdate: (product: Product) => void;
+ onProductAdd: (product: Product) => void;
+}
+
+export const ManageProduct = ({products, onProductAdd, onProductUpdate}: Props) => {
+ const {
+ newProduct,
+ showNewProductForm,
+ newDiscount,
+
+ setNewProduct,
+ setShowNewProductForm,
+ setNewDiscount,
+
+ editingProduct,
+ openProductIds,
+
+ editComplete,
+ handlePriceUpdate,
+ handleStockUpdate,
+ handleAddDiscount,
+ handleEditProduct,
+ handleAddNewProduct,
+ handleProductNameUpdate,
+ handleRemoveDiscount,
+ toggleProductAccordion
+ } = useAdmin();
+
+ 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(products, product.id, parseInt(e.target.value), onProductUpdate)}
+ 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}% 할인
+
+ ))}
+
+
+ )}
+
+ )}
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/refactoring/components/carts/CartList.tsx b/src/refactoring/components/carts/CartList.tsx
new file mode 100644
index 00000000..802bf3e9
--- /dev/null
+++ b/src/refactoring/components/carts/CartList.tsx
@@ -0,0 +1,53 @@
+import { Product } from "../../../types";
+import { getMaxDiscount } from "../../utils/discount";
+
+interface Props {
+ product: Product;
+ addToCart: (product: Product) => void;
+ getRemainingStock: (product: Product) => number;
+}
+
+export const CartList = ({
+ product,
+ addToCart,
+ getRemainingStock
+}: Props) => {
+ return (
+
+
+ {product.name}
+ {product.price.toLocaleString()}원
+
+
+ 0 ? 'text-green-600' : 'text-red-600'}`}>
+ 재고: {getRemainingStock(product)}개
+
+ {product.discounts.length > 0 && (
+
+ 최대 {(getMaxDiscount(product.discounts) * 100).toFixed(0)}% 할인
+
+ )}
+
+ {product.discounts.length > 0 && (
+
+ {product.discounts.map((discount, index) => (
+ -
+ {discount.quantity}개 이상: {(discount.rate * 100).toFixed(0)}% 할인
+
+ ))}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/refactoring/components/carts/CartProduct.tsx b/src/refactoring/components/carts/CartProduct.tsx
new file mode 100644
index 00000000..196855c2
--- /dev/null
+++ b/src/refactoring/components/carts/CartProduct.tsx
@@ -0,0 +1,52 @@
+import { CartItem } from "../../../types.ts";
+import { getAppliedDiscount } from "../../utils/discount.ts";
+
+interface Props {
+ item: CartItem;
+ onUpdateQuantity: (productId: string, newQuantity: number) => void;
+ onRemoveFromCart: (productId: string) => void;
+}
+
+export const CartProduct = ({
+ item,
+ onUpdateQuantity,
+ onRemoveFromCart
+}: Props) => {
+ return (
+
+
+ {item.product.name}
+
+
+ {item.product.price}원 x {item.quantity}
+
+ {getAppliedDiscount(item) > 0 && (
+
+ ({(getAppliedDiscount(item) * 100).toFixed(0)}% 할인 적용)
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/refactoring/hooks/useAdmin.ts b/src/refactoring/hooks/useAdmin.ts
new file mode 100644
index 00000000..7585f226
--- /dev/null
+++ b/src/refactoring/hooks/useAdmin.ts
@@ -0,0 +1,143 @@
+import { useState } from "react";
+import { Discount, Product, Coupon } from "../../types.ts";
+
+export const useAdmin = () => {
+ 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 [newCoupon, setNewCoupon] = useState({
+ name: '',
+ code: '',
+ discountType: 'percentage',
+ discountValue: 0
+ });
+
+ const handleAddCoupon = (onCouponAdd: (coupon: Coupon) => void) => {
+ onCouponAdd(newCoupon);
+ setNewCoupon({
+ name: '',
+ code: '',
+ discountType: 'percentage',
+ discountValue: 0
+ });
+ };
+
+ // 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 editComplete = (onProductUpdate: (product: Product) => void) => {
+ if (editingProduct) {
+ onProductUpdate(editingProduct);
+ setEditingProduct(null);
+ }
+ };
+
+ const handleStockUpdate = (products: Product[], productId: string, newStock: number, onProductUpdate: (product: Product) => void) => {
+ const updatedProduct = products.find(p => p.id === productId);
+ if (updatedProduct) {
+ const newProduct = { ...updatedProduct, stock: newStock };
+ onProductUpdate(newProduct);
+ setEditingProduct(newProduct);
+ }
+ };
+
+ const handleAddDiscount = (products: Product[], productId: string, onProductUpdate: (product: Product) => void) => {
+ 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 = (products: Product[], productId: string, index: number, onProductUpdate: (product: Product) => void) => {
+ 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 = (onProductAdd: (product: Product) => void) => {
+ const productWithId = { ...newProduct, id: Date.now().toString() };
+ onProductAdd(productWithId);
+ setNewProduct({
+ name: '',
+ price: 0,
+ stock: 0,
+ discounts: []
+ });
+ setShowNewProductForm(false);
+ };
+
+ const toggleProductAccordion = (productId: string) => {
+ setOpenProductIds(prev => {
+ const newSet = new Set(prev);
+ if (newSet.has(productId)) {
+ newSet.delete(productId);
+ } else {
+ newSet.add(productId);
+ }
+ return newSet;
+ });
+ };
+
+ return {
+ newProduct,
+ showNewProductForm,
+ newDiscount,
+ newCoupon,
+
+ setNewProduct,
+ setShowNewProductForm,
+ setNewDiscount,
+ setNewCoupon,
+
+ editingProduct,
+ openProductIds,
+
+ editComplete,
+ handlePriceUpdate,
+ handleStockUpdate,
+ handleAddDiscount,
+ handleEditProduct,
+ handleAddNewProduct,
+ handleProductNameUpdate,
+ handleRemoveDiscount,
+ handleAddCoupon,
+
+ toggleProductAccordion
+ }
+}
\ No newline at end of file
diff --git a/src/refactoring/hooks/useCart.ts b/src/refactoring/hooks/useCart.ts
index 67bf9208..3a010c74 100644
--- a/src/refactoring/hooks/useCart.ts
+++ b/src/refactoring/hooks/useCart.ts
@@ -1,33 +1,68 @@
-// useCart.ts
import { useState } from "react";
-import { CartItem, Coupon, Product } from "../../types";
-import { calculateCartTotal, updateCartItemQuantity } from "../models/cart";
+import { CartItem, Coupon, Product } from "../../types.ts";
+import { calculateCartTotal, updateCartItemQuantity } from "../utils/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(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) => {};
+ // 카트 물품 업데이트 로직
+ const updateQuantity = (productId: string, newQuantity: number) => {
+ setCart(prevCart => updateCartItemQuantity(prevCart, productId, newQuantity));
+ };
- const applyCoupon = (coupon: Coupon) => {};
+ const calculateTotal = () => {
+ return calculateCartTotal(cart, selectedCoupon);
+ }
+
+ const { totalBeforeDiscount, totalAfterDiscount, totalDiscount } = calculateTotal();
+
+ // 물품 쿠폰 추가 로직
+ const applyCoupon = (coupon: Coupon) => {
+ setSelectedCoupon(coupon);
+ };
- const calculateTotal = () => ({
- totalBeforeDiscount: 0,
- totalAfterDiscount: 0,
- totalDiscount: 0,
- });
+ const getRemainingStock = (product: Product): number => {
+ const cartItem = cart.find(item => item.product.id === product.id);
+ return product.stock - (cartItem?.quantity || 0);
+ };
return {
cart,
+ selectedCoupon,
+
+ totalBeforeDiscount,
+ totalAfterDiscount,
+ totalDiscount,
+
addToCart,
+ applyCoupon,
removeFromCart,
updateQuantity,
- applyCoupon,
calculateTotal,
- selectedCoupon,
+ getRemainingStock
};
};
diff --git a/src/refactoring/hooks/useCoupon.ts b/src/refactoring/hooks/useCoupon.ts
index dc1b9078..ee879c7f 100644
--- a/src/refactoring/hooks/useCoupon.ts
+++ b/src/refactoring/hooks/useCoupon.ts
@@ -1,6 +1,17 @@
import { Coupon } from "../../types.ts";
-import { useState } from "react";
+import { useState, useEffect } from "react";
export const useCoupons = (initialCoupons: Coupon[]) => {
- return { coupons: [], addCoupon: () => undefined };
+ const [coupons, setCoupons] = useState(initialCoupons);
+
+ useEffect(() => {
+ if (initialCoupons) setCoupons(initialCoupons);
+ }, [initialCoupons]);
+
+ const addCoupon = (coupon: Coupon) => setCoupons([...coupons, coupon])
+
+ return {
+ coupons,
+ addCoupon
+ };
};
diff --git a/src/refactoring/hooks/useProduct.ts b/src/refactoring/hooks/useProduct.ts
index 028bacb6..feffcee2 100644
--- a/src/refactoring/hooks/useProduct.ts
+++ b/src/refactoring/hooks/useProduct.ts
@@ -1,6 +1,30 @@
-import { useState } from 'react';
-import { Product } from '../../types.ts';
+import { useEffect, useState } from "react";
+import { Product } from "../../types.ts";
+// useProducts : 상품의 엔티티 상태(init/update/add)를 담당하는 커스텀 훅
export const useProducts = (initialProducts: Product[]) => {
- return { products: [], updateProduct: () => undefined, addProduct: () => undefined };
+ const [products, setProducts] = useState(initialProducts);
+
+ useEffect(() => {
+ // 예외처리 및 상태 init
+ if (initialProducts) setProducts(initialProducts);
+ }, [initialProducts]);
+
+ // 상품 업데이트
+ const updateProduct = (product: Product) => {
+ setProducts((prevProducts) =>
+ prevProducts.map((prod) =>
+ prod.id === product.id ? { ...prod, ...product } : prod
+ )
+ );
+ };
+
+ // 상품 추가
+ const addProduct = (product: Product) => setProducts([...products, product])
+
+ return {
+ products,
+ updateProduct,
+ addProduct
+ };
};
diff --git a/src/refactoring/main.tsx b/src/refactoring/main.tsx
index e63eef4a..1f17221f 100644
--- a/src/refactoring/main.tsx
+++ b/src/refactoring/main.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App.tsx'
+import React from "react"
+import ReactDOM from "react-dom/client"
+import App from "./App.tsx"
-ReactDOM.createRoot(document.getElementById('root')!).render(
+ReactDOM.createRoot(document.getElementById("root")!).render(
- ,
+
)
diff --git a/src/refactoring/models/cart.ts b/src/refactoring/models/cart.ts
deleted file mode 100644
index d8f4c2ea..00000000
--- a/src/refactoring/models/cart.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { CartItem, Coupon } from "../../types";
-
-export const calculateItemTotal = (item: CartItem) => {
- return 0;
-};
-
-export const getMaxApplicableDiscount = (item: CartItem) => {
- return 0;
-};
-
-export const calculateCartTotal = (
- cart: CartItem[],
- selectedCoupon: Coupon | null
-) => {
- return {
- totalBeforeDiscount: 0,
- totalAfterDiscount: 0,
- totalDiscount: 0,
- };
-};
-
-export const updateCartItemQuantity = (
- cart: CartItem[],
- productId: string,
- newQuantity: number
-): CartItem[] => {
- return [];
-};
diff --git a/src/refactoring/models/datas.ts b/src/refactoring/models/datas.ts
new file mode 100644
index 00000000..3504805d
--- /dev/null
+++ b/src/refactoring/models/datas.ts
@@ -0,0 +1,40 @@
+import { Coupon, 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
+ }
+];
\ No newline at end of file
diff --git a/src/refactoring/utils/cart.ts b/src/refactoring/utils/cart.ts
new file mode 100644
index 00000000..9074dc49
--- /dev/null
+++ b/src/refactoring/utils/cart.ts
@@ -0,0 +1,110 @@
+import { CartItem, Coupon } from "../../types.ts";
+
+export const calculateItemTotal = (item: CartItem) => {
+ if (!item) return 0;
+
+ const { discounts } = item.product;
+ const { quantity } = item;
+
+ // 요구사항 1: 할인 없이 총액을 계산해야 합니다 | price * quantity
+ const total = item.product.price * item.quantity;
+
+ // 요구사항 2: 수량에 따라 올바른 할인을 적용해야 합니다 | reduce
+ const rateByQuantity = discounts.reduce((acc, prevProduct) => {
+ // 수량과 일치하는 할인율을 찾아서 해당 할인율 적용
+ if (quantity === prevProduct.quantity) {
+ const discount = prevProduct.rate;
+ return acc * (1 - discount); // 할인율 적용 로직
+ }
+ return acc;
+ }, total)
+
+ return rateByQuantity;
+};
+
+export const getMaxApplicableDiscount = (item: CartItem) => {
+ if (!item) return 0;
+
+ const { discounts } = item.product;
+ const { quantity } = item;
+
+ // return Math.max(...discounts.map(prevRate => prevRate.rate));
+ // 위 방법도 간결하고 좋지만 0을 반환하지 못해 요구사항 1을 충족시키지 못한다
+
+ // 요구사항 2: 적용 가능한 가장 높은 할인율을 반환해야 합니다 | reduce
+ return discounts.reduce((max, prevProduct) => {
+ if (prevProduct.quantity === quantity) {
+ return Math.max(max, prevProduct.rate);
+ }
+ return max;
+ }, 0); // 요구사항 1: 할인이 적용되지 않으면 0을 반환해야 합니다 | reduce
+};
+
+export const calculateCartTotal = (
+ cart: CartItem[],
+ selectedCoupon: Coupon | null
+) => {
+ // 기본값을 const로 설정
+ const { totalBeforeDiscount, totalAfterDiscount } = cart.reduce(
+ (acc, item) => {
+ const { price } = item.product;
+ const { quantity } = item;
+
+ const itemTotal = price * quantity;
+ const maxDiscountRate = item.product.discounts.reduce((maxRate, d) =>
+ quantity >= d.quantity && d.rate > maxRate ? d.rate : maxRate, 0
+ );
+
+ const discountAmount = itemTotal * maxDiscountRate;
+ acc.totalBeforeDiscount += itemTotal;
+ acc.totalAfterDiscount += itemTotal - discountAmount;
+ acc.totalDiscount += discountAmount;
+
+ return acc;
+ },
+ { totalBeforeDiscount: 0, totalAfterDiscount: 0, totalDiscount: 0 }
+ );
+
+ // 쿠폰 적용 로직 개선
+ const finalTotalAfterDiscount = selectedCoupon
+ ? calculateCoupon(totalAfterDiscount, selectedCoupon.discountType, selectedCoupon.discountValue)
+ : totalAfterDiscount;
+
+ const finalTotalDiscount = totalBeforeDiscount - finalTotalAfterDiscount;
+
+ return {
+ totalBeforeDiscount: Math.round(totalBeforeDiscount),
+ totalAfterDiscount: Math.round(finalTotalAfterDiscount),
+ totalDiscount: Math.round(finalTotalDiscount)
+ };
+}
+
+const calculateCoupon = (initValue: number, discountType: 'amount' | 'percentage', discountValue: number): number => {
+ // 요구사항 2. 금액쿠폰을 올바르게 적용해야 합니다
+ // 요구사항 3. 퍼센트 쿠폰을 올바르게 적용해야 합니다
+ return discountType === 'amount'
+ ? Math.max(0, initValue - discountValue)
+ : initValue * (1 - discountValue / 100);
+};
+
+export const updateCartItemQuantity = (
+ cart: CartItem[],
+ productId: string,
+ newQuantity: number
+): CartItem[] => {
+ if (!cart || !productId || newQuantity < 0) return cart;
+
+ // 요구사항 1: 수량을 올바르게 업데이트해야 합니다
+ const newCart = cart
+ .map((value) => {
+ if (value.product.id === productId && value.quantity !== newQuantity) {
+ // 요구사항 3: 재고 한도를 초과해서는 안 됩니다
+ const maxQuantity = Math.min(newQuantity, value.product.stock);
+ return { ...value, quantity: maxQuantity};
+ }
+ return value;
+ })
+ .filter(value => value.quantity !== 0); // 요구사항 2: 수량이 0으로 설정된 경우 항목을 제거해야 합니다
+
+ return newCart;
+};
\ No newline at end of file
diff --git a/src/refactoring/utils/discount.ts b/src/refactoring/utils/discount.ts
new file mode 100644
index 00000000..74f401cc
--- /dev/null
+++ b/src/refactoring/utils/discount.ts
@@ -0,0 +1,16 @@
+import { CartItem } from "../../types.ts";
+
+export const getMaxDiscount = (discounts: { quantity: number; rate: number }[]): number => {
+ return discounts.reduce((max, discount) => Math.max(max, discount.rate), 0);
+}
+
+export const getAppliedDiscount = (item: CartItem): number => {
+ const { discounts } = item.product;
+ const { quantity } = item;
+ const appliedDiscount = 0;
+
+ // reduce 개선
+ return discounts.reduce((maxDiscount, discount) => {
+ return quantity >= discount.quantity ? Math.max(maxDiscount, discount.rate) : maxDiscount;
+ }, appliedDiscount);
+};
\ No newline at end of file