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}% 할인 -
- ))} - -
- )} -
- )} -
- ))} -
-
-
-

쿠폰 관리

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

현재 쿠폰 목록

-
- {coupons.map((coupon, index) => ( -
- {coupon.name} ({coupon.code}): - {coupon.discountType === 'amount' ? `${coupon.discountValue}원` : `${coupon.discountValue}%`} 할인 -
- ))} -
-
-
-
+ +
); diff --git a/src/refactoring/components/CartPage.tsx b/src/refactoring/components/CartPage.tsx index 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 ( +
+

쿠폰 관리

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

현재 쿠폰 목록

+
+ {coupons.map((coupon, index) => ( +
+ {coupon.name} ({coupon.code}): + {coupon.discountType === 'amount' ? `${coupon.discountValue}원` : `${coupon.discountValue}%`} 할인 +
+ ))} +
+
+
+
+ ) +} \ 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