From 183e586e72f9a2f20aa0e564f825435cf64cfb29 Mon Sep 17 00:00:00 2001 From: annkimm Date: Sun, 3 Aug 2025 23:44:16 +0900 Subject: [PATCH 01/16] init: setting --- .eslintrc.cjs | 1 + .prettierrc | 9 + package-lock.json | 4489 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- pnpm-lock.yaml | 2831 ---------------------------- 5 files changed, 4503 insertions(+), 2832 deletions(-) create mode 100644 .prettierrc create mode 100644 package-lock.json delete mode 100644 pnpm-lock.yaml diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d6c95379..9a405c2f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,6 +5,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', + 'plugin:prettier/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..aab466a2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "useTabs": false, + "tabWidth": 2, + "printWidth": 80, + "arrowParens": "always" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..be8770e9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4489 @@ +{ + "name": "assignment-5", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "assignment-5", + "version": "0.0.0", + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.4", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "@vitejs/plugin-react-swc": "^3.11.0", + "@vitest/ui": "^3.2.4", + "eslint": "^9.32.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.3", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "jsdom": "^26.1.0", + "prettier": "3.6.2", + "typescript": "5.8.2", + "vite": "^7.0.6", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", + "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", + "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", + "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", + "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", + "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", + "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", + "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", + "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", + "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", + "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", + "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", + "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "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.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 79034acb..74dc2b0a 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,13 @@ "@vitejs/plugin-react-swc": "^3.11.0", "@vitest/ui": "^3.2.4", "eslint": "^9.32.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.3", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "jsdom": "^26.1.0", - "typescript": "^5.9.2", + "prettier": "3.6.2", + "typescript": "5.8.2", "vite": "^7.0.6", "vitest": "^3.2.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 2dddaf85..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2831 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - react: - specifier: ^19.1.1 - version: 19.1.1 - react-dom: - specifier: ^19.1.1 - version: 19.1.1(react@19.1.1) - devDependencies: - '@testing-library/jest-dom': - specifier: ^6.6.4 - version: 6.6.4 - '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@testing-library/user-event': - specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.0) - '@types/react': - specifier: ^19.1.9 - version: 19.1.9 - '@types/react-dom': - specifier: ^19.1.7 - version: 19.1.7(@types/react@19.1.9) - '@typescript-eslint/eslint-plugin': - specifier: ^8.38.0 - version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2))(eslint@9.32.0)(typescript@5.9.2) - '@typescript-eslint/parser': - specifier: ^8.38.0 - version: 8.38.0(eslint@9.32.0)(typescript@5.9.2) - '@vitejs/plugin-react-swc': - specifier: ^3.11.0 - version: 3.11.0(vite@7.0.6) - '@vitest/ui': - specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4) - eslint: - specifier: ^9.32.0 - version: 9.32.0 - eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.32.0) - eslint-plugin-react-refresh: - specifier: ^0.4.20 - version: 0.4.20(eslint@9.32.0) - jsdom: - specifier: ^26.1.0 - version: 26.1.0 - typescript: - specifier: ^5.9.2 - version: 5.9.2 - vite: - specifier: ^7.0.6 - version: 7.0.6 - vitest: - specifier: ^3.2.4 - version: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0) - -packages: - - '@adobe/css-tools@4.4.0': - resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} - - '@asamuzakjp/css-color@3.1.2': - resolution: {integrity: sha512-nwgc7jPn3LpZ4JWsoHtuwBsad1qSSLDDX634DdG0PBJofIuIEtSWk4KkRmuXyu178tjuHAbwiMNNzwqIyLYxZw==} - - '@babel/code-frame@7.25.7': - resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.25.7': - resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.25.7': - resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} - engines: {node: '>=6.9.0'} - - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} - engines: {node: '>=18'} - - '@csstools/css-calc@2.1.2': - resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} - engines: {node: '>=18'} - peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 - - '@csstools/css-color-parser@3.0.8': - resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} - 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.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.3.0': - resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.15.1': - resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.32.0': - resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.3.4': - resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} - engines: {node: '>=18.18.0'} - - '@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/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@polka/url@1.0.0-next.28': - resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} - - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} - cpu: [x64] - os: [win32] - - '@swc/core-darwin-arm64@1.13.3': - resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.13.3': - resolution: {integrity: sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.13.3': - resolution: {integrity: sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.13.3': - resolution: {integrity: sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-arm64-musl@1.13.3': - resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-x64-gnu@1.13.3': - resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-linux-x64-musl@1.13.3': - resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-win32-arm64-msvc@1.13.3': - resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.13.3': - resolution: {integrity: sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.13.3': - resolution: {integrity: sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.13.3': - resolution: {integrity: sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '>=0.5.17' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} - - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - - '@testing-library/jest-dom@6.6.4': - resolution: {integrity: sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/react-dom@19.1.7': - resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} - peerDependencies: - '@types/react': ^19.0.0 - - '@types/react@19.1.9': - resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} - - '@typescript-eslint/eslint-plugin@8.38.0': - resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.38.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/parser@8.38.0': - resolution: {integrity: sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/project-service@8.38.0': - resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/scope-manager@8.38.0': - resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.38.0': - resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/type-utils@8.38.0': - resolution: {integrity: sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/types@8.38.0': - resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.38.0': - resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/utils@8.38.0': - resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/visitor-keys@8.38.0': - resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vitejs/plugin-react-swc@3.11.0': - resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} - peerDependencies: - vite: ^4 || ^5 || ^6 || ^7 - - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - - '@vitest/ui@3.2.4': - resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} - peerDependencies: - vitest: 3.2.4 - - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - 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==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chai@5.2.1: - resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} - engines: {node: '>=18'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - - cssstyle@4.3.0: - resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} - 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'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - 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'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - - eslint-plugin-react-refresh@0.4.20: - resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} - peerDependencies: - eslint: '>=8.40' - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.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.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.32.0: - resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} - engines: {node: '>=12.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - 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'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - 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'} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - 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'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - - loupe@3.2.0: - resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - 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-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - 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} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} - peerDependencies: - react: ^19.1.1 - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} - engines: {node: '>=0.10.0'} - - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} - 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.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - sirv@3.0.1: - resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} - engines: {node: '>=18'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - 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==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.3: - resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} - engines: {node: '>=14.0.0'} - - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} - - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} - hasBin: true - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} - engines: {node: '>=16'} - - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} - engines: {node: '>=14.17'} - hasBin: true - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.0.6: - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - 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.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - 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@3.1.2': - dependencies: - '@csstools/css-calc': 2.1.2(@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.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - lru-cache: 10.4.3 - - '@babel/code-frame@7.25.7': - dependencies: - '@babel/highlight': 7.25.7 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.25.7': {} - - '@babel/highlight@7.25.7': - dependencies: - '@babel/helper-validator-identifier': 7.25.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/runtime@7.25.7': - dependencies: - regenerator-runtime: 0.14.1 - - '@csstools/color-helpers@5.0.2': {} - - '@csstools/css-calc@2.1.2(@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.8(@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.2 - '@csstools/css-calc': 2.1.2(@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.25.8': - optional: true - - '@esbuild/android-arm64@0.25.8': - optional: true - - '@esbuild/android-arm@0.25.8': - optional: true - - '@esbuild/android-x64@0.25.8': - optional: true - - '@esbuild/darwin-arm64@0.25.8': - optional: true - - '@esbuild/darwin-x64@0.25.8': - optional: true - - '@esbuild/freebsd-arm64@0.25.8': - optional: true - - '@esbuild/freebsd-x64@0.25.8': - optional: true - - '@esbuild/linux-arm64@0.25.8': - optional: true - - '@esbuild/linux-arm@0.25.8': - optional: true - - '@esbuild/linux-ia32@0.25.8': - optional: true - - '@esbuild/linux-loong64@0.25.8': - optional: true - - '@esbuild/linux-mips64el@0.25.8': - optional: true - - '@esbuild/linux-ppc64@0.25.8': - optional: true - - '@esbuild/linux-riscv64@0.25.8': - optional: true - - '@esbuild/linux-s390x@0.25.8': - optional: true - - '@esbuild/linux-x64@0.25.8': - optional: true - - '@esbuild/netbsd-arm64@0.25.8': - optional: true - - '@esbuild/netbsd-x64@0.25.8': - optional: true - - '@esbuild/openbsd-arm64@0.25.8': - optional: true - - '@esbuild/openbsd-x64@0.25.8': - optional: true - - '@esbuild/openharmony-arm64@0.25.8': - optional: true - - '@esbuild/sunos-x64@0.25.8': - optional: true - - '@esbuild/win32-arm64@0.25.8': - optional: true - - '@esbuild/win32-ia32@0.25.8': - optional: true - - '@esbuild/win32-x64@0.25.8': - optional: true - - '@eslint-community/eslint-utils@4.4.0(eslint@9.32.0)': - dependencies: - eslint: 9.32.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0)': - dependencies: - eslint: 9.32.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.11.1': {} - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/config-array@0.21.0': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.3.7 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.3.0': {} - - '@eslint/core@0.15.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.3.7 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.32.0': {} - - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.3.4': - dependencies: - '@eslint/core': 0.15.1 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.6': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@polka/url@1.0.0-next.28': {} - - '@rolldown/pluginutils@1.0.0-beta.27': {} - - '@rollup/rollup-android-arm-eabi@4.46.2': - optional: true - - '@rollup/rollup-android-arm64@4.46.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.46.2': - optional: true - - '@rollup/rollup-darwin-x64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.46.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.46.2': - optional: true - - '@swc/core-darwin-arm64@1.13.3': - optional: true - - '@swc/core-darwin-x64@1.13.3': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.13.3': - optional: true - - '@swc/core-linux-arm64-gnu@1.13.3': - optional: true - - '@swc/core-linux-arm64-musl@1.13.3': - optional: true - - '@swc/core-linux-x64-gnu@1.13.3': - optional: true - - '@swc/core-linux-x64-musl@1.13.3': - optional: true - - '@swc/core-win32-arm64-msvc@1.13.3': - optional: true - - '@swc/core-win32-ia32-msvc@1.13.3': - optional: true - - '@swc/core-win32-x64-msvc@1.13.3': - optional: true - - '@swc/core@1.13.3': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 - optionalDependencies: - '@swc/core-darwin-arm64': 1.13.3 - '@swc/core-darwin-x64': 1.13.3 - '@swc/core-linux-arm-gnueabihf': 1.13.3 - '@swc/core-linux-arm64-gnu': 1.13.3 - '@swc/core-linux-arm64-musl': 1.13.3 - '@swc/core-linux-x64-gnu': 1.13.3 - '@swc/core-linux-x64-musl': 1.13.3 - '@swc/core-win32-arm64-msvc': 1.13.3 - '@swc/core-win32-ia32-msvc': 1.13.3 - '@swc/core-win32-x64-msvc': 1.13.3 - - '@swc/counter@0.1.3': {} - - '@swc/types@0.1.23': - dependencies: - '@swc/counter': 0.1.3 - - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.25.7 - '@babel/runtime': 7.25.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@6.6.4': - dependencies: - '@adobe/css-tools': 4.4.0 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - picocolors: 1.1.1 - redent: 3.0.0 - - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': - dependencies: - '@babel/runtime': 7.25.7 - '@testing-library/dom': 10.4.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - optionalDependencies: - '@types/react': 19.1.9 - '@types/react-dom': 19.1.7(@types/react@19.1.9) - - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': - dependencies: - '@testing-library/dom': 10.4.0 - - '@types/aria-query@5.0.4': {} - - '@types/chai@5.2.2': - dependencies: - '@types/deep-eql': 4.0.2 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.6': {} - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/react-dom@19.1.7(@types/react@19.1.9)': - dependencies: - '@types/react': 19.1.9 - - '@types/react@19.1.9': - dependencies: - csstype: 3.1.3 - - '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2))(eslint@9.32.0)(typescript@5.9.2)': - dependencies: - '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 8.38.0(eslint@9.32.0)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/type-utils': 8.38.0(eslint@9.32.0)(typescript@5.9.2) - '@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.38.0 - eslint: 9.32.0 - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.38.0(eslint@9.32.0)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.3.7 - eslint: 9.32.0 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.38.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.9.2) - '@typescript-eslint/types': 8.38.0 - debug: 4.3.7 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - - '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - - '@typescript-eslint/type-utils@8.38.0(eslint@9.32.0)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.9.2) - debug: 4.3.7 - eslint: 9.32.0 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.38.0': {} - - '@typescript-eslint/typescript-estree@8.38.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/project-service': 8.38.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.9.2) - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.3.7 - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.38.0(eslint@9.32.0)(typescript@5.9.2)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0) - '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.9.2) - eslint: 9.32.0 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - eslint-visitor-keys: 4.2.1 - - '@vitejs/plugin-react-swc@3.11.0(vite@7.0.6)': - dependencies: - '@rolldown/pluginutils': 1.0.0-beta.27 - '@swc/core': 1.13.3 - vite: 7.0.6 - transitivePeerDependencies: - - '@swc/helpers' - - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.2 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.4(vite@7.0.6)': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 7.0.6 - - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.0.0 - - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 - pathe: 2.0.3 - - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.3 - - '@vitest/ui@3.2.4(vitest@3.2.4)': - dependencies: - '@vitest/utils': 3.2.4 - fflate: 0.8.2 - flatted: 3.3.3 - pathe: 2.0.3 - sirv: 3.0.1 - tinyglobby: 0.2.14 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0) - - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.0 - tinyrainbow: 2.0.0 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - agent-base@7.1.3: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - argparse@2.0.1: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - aria-query@5.3.2: {} - - assertion-error@2.0.1: {} - - balanced-match@1.0.2: {} - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - cac@6.7.14: {} - - callsites@3.1.0: {} - - chai@5.2.1: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.2 - pathval: 2.0.0 - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - check-error@2.1.1: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css.escape@1.5.1: {} - - cssstyle@4.3.0: - dependencies: - '@asamuzakjp/css-color': 3.1.2 - rrweb-cssom: 0.8.0 - - csstype@3.1.3: {} - - data-urls@5.0.0: - dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - - debug@4.3.7: - dependencies: - ms: 2.1.3 - - debug@4.4.1: - dependencies: - ms: 2.1.3 - - decimal.js@10.5.0: {} - - deep-eql@5.0.2: {} - - deep-is@0.1.4: {} - - dequal@2.0.3: {} - - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - - entities@4.5.0: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.25.8: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react-hooks@5.2.0(eslint@9.32.0): - dependencies: - eslint: 9.32.0 - - eslint-plugin-react-refresh@0.4.20(eslint@9.32.0): - dependencies: - eslint: 9.32.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.32.0: - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.32.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.0 - '@eslint/core': 0.15.1 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.32.0 - '@eslint/plugin-kit': 0.3.4 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.3.7 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.6 - - esutils@2.0.3: {} - - expect-type@1.2.2: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - fdir@6.4.6(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - fdir@6.4.6(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - fflate@0.8.2: {} - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - - flatted@3.3.1: {} - - flatted@3.3.3: {} - - fsevents@2.3.3: - optional: true - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - graphemer@1.4.0: {} - - has-flag@3.0.0: {} - - 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: {} - - ignore@7.0.5: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-potential-custom-element-name@1.0.1: {} - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-tokens@9.0.1: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsdom@26.1.0: - dependencies: - cssstyle: 4.3.0 - data-urls: 5.0.0 - decimal.js: 10.5.0 - 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.20 - parse5: 7.2.1 - rrweb-cssom: 0.8.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 5.1.2 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.1 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - lodash@4.17.21: {} - - loupe@3.1.2: {} - - loupe@3.2.0: {} - - lru-cache@10.4.3: {} - - lz-string@1.5.0: {} - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - min-indent@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - mrmime@2.0.0: {} - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - nwsapi@2.2.20: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse5@7.2.1: - dependencies: - entities: 4.5.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - pathe@2.0.3: {} - - pathval@2.0.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - - picomatch@4.0.3: {} - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-dom@19.1.1(react@19.1.1): - dependencies: - react: 19.1.1 - scheduler: 0.26.0 - - react-is@17.0.2: {} - - react@19.1.1: {} - - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - - regenerator-runtime@0.14.1: {} - - resolve-from@4.0.0: {} - - reusify@1.0.4: {} - - rollup@4.46.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 - 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.26.0: {} - - semver@7.6.3: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - sirv@3.0.1: - dependencies: - '@polka/url': 1.0.0-next.28 - mrmime: 2.0.0 - totalist: 3.0.1 - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@3.9.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@3.1.1: {} - - strip-literal@3.0.0: - dependencies: - js-tokens: 9.0.1 - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - symbol-tree@3.2.4: {} - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.14: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.3: {} - - tldts-core@6.1.86: {} - - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - totalist@3.0.1: {} - - tough-cookie@5.1.2: - dependencies: - tldts: 6.1.86 - - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - - ts-api-utils@2.1.0(typescript@5.9.2): - dependencies: - typescript: 5.9.2 - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript@5.9.2: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - vite-node@3.2.4: - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.0.6 - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.0.6: - dependencies: - esbuild: 0.25.8 - fdir: 6.4.6(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.46.2 - tinyglobby: 0.2.14 - optionalDependencies: - fsevents: 2.3.3 - - vitest@3.2.4(@vitest/ui@3.2.4)(jsdom@26.1.0): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.6) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - debug: 4.4.1 - expect-type: 1.2.2 - magic-string: 0.30.17 - pathe: 2.0.3 - picomatch: 4.0.2 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.0.6 - vite-node: 3.2.4 - why-is-node-running: 2.3.0 - optionalDependencies: - '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - 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.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - ws@8.18.1: {} - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - - yocto-queue@0.1.0: {} From dfbd430cc9c8466658009c4868352897fb63879c Mon Sep 17 00:00:00 2001 From: annkimm Date: Sun, 3 Aug 2025 23:47:54 +0900 Subject: [PATCH 02/16] feat: separate to admin and cart from app --- src/basic/App.tsx | 1220 ++------------------- src/basic/components/AdminPage.tsx | 693 ++++++++++++ src/basic/components/CartPage.tsx | 407 +++++++ src/basic/components/icons/index.tsx | 12 + src/basic/components/ui/Header.tsx | 77 ++ src/basic/components/ui/Notifications.tsx | 54 + src/basic/components/ui/ProductList.tsx | 105 ++ src/basic/constants/index.ts | 61 ++ src/basic/hooks/useCart.ts | 200 ++++ src/basic/hooks/useCoupons.ts | 70 ++ src/basic/hooks/useProducts.ts | 82 ++ src/basic/models/cart.ts | 92 ++ src/basic/models/coupon.ts | 0 src/basic/models/discount.ts | 0 src/basic/models/product.ts | 0 src/basic/types/index.ts | 12 + src/basic/utils/formatters.ts | 7 + src/basic/utils/hooks/useDebounce.ts | 11 + src/basic/utils/hooks/useLocalStorage.ts | 15 + src/basic/utils/hooks/useValidate.ts | 0 src/basic/utils/validators.ts | 8 + 21 files changed, 2026 insertions(+), 1100 deletions(-) create mode 100644 src/basic/components/AdminPage.tsx create mode 100644 src/basic/components/CartPage.tsx create mode 100644 src/basic/components/icons/index.tsx create mode 100644 src/basic/components/ui/Header.tsx create mode 100644 src/basic/components/ui/Notifications.tsx create mode 100644 src/basic/components/ui/ProductList.tsx create mode 100644 src/basic/constants/index.ts create mode 100644 src/basic/hooks/useCart.ts create mode 100644 src/basic/hooks/useCoupons.ts create mode 100644 src/basic/hooks/useProducts.ts create mode 100644 src/basic/models/cart.ts create mode 100644 src/basic/models/coupon.ts create mode 100644 src/basic/models/discount.ts create mode 100644 src/basic/models/product.ts create mode 100644 src/basic/types/index.ts create mode 100644 src/basic/utils/formatters.ts create mode 100644 src/basic/utils/hooks/useDebounce.ts create mode 100644 src/basic/utils/hooks/useLocalStorage.ts create mode 100644 src/basic/utils/hooks/useValidate.ts create mode 100644 src/basic/utils/validators.ts diff --git a/src/basic/App.tsx b/src/basic/App.tsx index a4369fe1..8d311d5b 100644 --- a/src/basic/App.tsx +++ b/src/basic/App.tsx @@ -1,1124 +1,144 @@ -import { useState, useCallback, useEffect } from 'react'; -import { CartItem, Coupon, Product } from '../types'; - -interface ProductWithUI extends Product { - description?: string; - isRecommended?: boolean; -} - -interface Notification { - id: string; - message: string; - type: 'error' | 'success' | 'warning'; -} - -// 초기 데이터 -const initialProducts: ProductWithUI[] = [ - { - id: 'p1', - name: '상품1', - price: 10000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.1 }, - { quantity: 20, rate: 0.2 } - ], - description: '최고급 품질의 프리미엄 상품입니다.' - }, - { - id: 'p2', - name: '상품2', - price: 20000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.15 } - ], - description: '다양한 기능을 갖춘 실용적인 상품입니다.', - isRecommended: true - }, - { - id: 'p3', - name: '상품3', - price: 30000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.2 }, - { quantity: 30, rate: 0.25 } - ], - description: '대용량과 고성능을 자랑하는 상품입니다.' - } -]; - -const initialCoupons: Coupon[] = [ - { - name: '5000원 할인', - code: 'AMOUNT5000', - discountType: 'amount', - discountValue: 5000 - }, - { - name: '10% 할인', - code: 'PERCENT10', - discountType: 'percentage', - discountValue: 10 - } -]; +import { useState, useCallback, useEffect } from 'react' +import { Notification } from './types' +import { AdminPage } from './components/AdminPage' +import { useProducts } from './hooks/useProducts' +import { CartPage } from './components/CartPage' +import { getRemainingStock } from './models/cart' +import { useCart } from './hooks/useCart' +import { useCoupons } from './hooks/useCoupons' +import { Header } from './components/ui/Header' +import { Notifications } from './components/ui/Notifications' const App = () => { - - const [products, setProducts] = useState(() => { - const saved = localStorage.getItem('products'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return initialProducts; - } - } - return initialProducts; - }); - - const [cart, setCart] = useState(() => { - const saved = localStorage.getItem('cart'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return []; - } - } - return []; - }); - - const [coupons, setCoupons] = useState(() => { - const saved = localStorage.getItem('coupons'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return initialCoupons; - } - } - return initialCoupons; - }); - - const [selectedCoupon, setSelectedCoupon] = useState(null); - const [isAdmin, setIsAdmin] = useState(false); - const [notifications, setNotifications] = useState([]); - const [showCouponForm, setShowCouponForm] = useState(false); - const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products'); - const [showProductForm, setShowProductForm] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); - - // Admin - const [editingProduct, setEditingProduct] = useState(null); - const [productForm, setProductForm] = useState({ - name: '', - price: 0, - stock: 0, - description: '', - discounts: [] as Array<{ quantity: number; rate: number }> - }); - - const [couponForm, setCouponForm] = useState({ - name: '', - code: '', - discountType: 'amount' as 'amount' | 'percentage', - discountValue: 0 - }); - - - const formatPrice = (price: number, productId?: string): string => { - if (productId) { - const product = products.find(p => p.id === productId); - if (product && getRemainingStock(product) <= 0) { - return 'SOLD OUT'; - } - } - - if (isAdmin) { - return `${price.toLocaleString()}원`; - } - - return `₩${price.toLocaleString()}`; - }; - - const getMaxApplicableDiscount = (item: CartItem): number => { - const { discounts } = item.product; - const { quantity } = item; - - const baseDiscount = discounts.reduce((maxDiscount, discount) => { - return quantity >= discount.quantity && discount.rate > maxDiscount - ? discount.rate - : maxDiscount; - }, 0); - - const hasBulkPurchase = cart.some(cartItem => cartItem.quantity >= 10); - if (hasBulkPurchase) { - return Math.min(baseDiscount + 0.05, 0.5); // 대량 구매 시 추가 5% 할인 - } - - return baseDiscount; - }; - - const calculateItemTotal = (item: CartItem): number => { - const { price } = item.product; - const { quantity } = item; - const discount = getMaxApplicableDiscount(item); - - return Math.round(price * quantity * (1 - discount)); - }; - - const calculateCartTotal = (): { - totalBeforeDiscount: number; - totalAfterDiscount: number; - } => { - let totalBeforeDiscount = 0; - let totalAfterDiscount = 0; - - cart.forEach(item => { - const itemPrice = item.product.price * item.quantity; - totalBeforeDiscount += itemPrice; - totalAfterDiscount += calculateItemTotal(item); - }); - - if (selectedCoupon) { - if (selectedCoupon.discountType === 'amount') { - totalAfterDiscount = Math.max(0, totalAfterDiscount - selectedCoupon.discountValue); - } else { - totalAfterDiscount = Math.round(totalAfterDiscount * (1 - selectedCoupon.discountValue / 100)); - } - } - - return { - totalBeforeDiscount: Math.round(totalBeforeDiscount), - totalAfterDiscount: Math.round(totalAfterDiscount) - }; - }; - - const getRemainingStock = (product: Product): number => { - const cartItem = cart.find(item => item.product.id === product.id); - const remaining = product.stock - (cartItem?.quantity || 0); - - return remaining; - }; - - const addNotification = useCallback((message: string, type: 'error' | 'success' | 'warning' = 'success') => { - const id = Date.now().toString(); - setNotifications(prev => [...prev, { id, message, type }]); - - setTimeout(() => { - setNotifications(prev => prev.filter(n => n.id !== id)); - }, 3000); - }, []); - - const [totalItemCount, setTotalItemCount] = useState(0); - + const [notifications, setNotifications] = useState([]) + const addNotification = useCallback( + (message: string, type: 'error' | 'success' | 'warning' = 'success') => { + const id = Date.now().toString() + setNotifications((prev) => [...prev, { id, message, type }]) + + setTimeout(() => { + setNotifications((prev) => prev.filter((n) => n.id !== id)) + }, 3000) + }, + [], + ) + + const { products, addProduct, updateProduct, deleteProduct } = + useProducts(addNotification) + const { + cart, + setCart, + removeFromCart, + updateQuantity, + selectedCoupon, + setSelectedCoupon, + addToCart, + applyCoupon, + calculateCartTotal, + calculateItemTotal, + } = useCart(addNotification) + const { coupons, addCoupon, deleteCoupon } = useCoupons( + addNotification, + selectedCoupon, + setSelectedCoupon, + ) + const [isAdmin, setIsAdmin] = useState(false) + const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') + const [searchTerm, setSearchTerm] = useState('') + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') + const [totalItemCount, setTotalItemCount] = useState(0) useEffect(() => { - const count = cart.reduce((sum, item) => sum + item.quantity, 0); - setTotalItemCount(count); - }, [cart]); - - useEffect(() => { - localStorage.setItem('products', JSON.stringify(products)); - }, [products]); - - useEffect(() => { - localStorage.setItem('coupons', JSON.stringify(coupons)); - }, [coupons]); - - useEffect(() => { - if (cart.length > 0) { - localStorage.setItem('cart', JSON.stringify(cart)); - } else { - localStorage.removeItem('cart'); - } - }, [cart]); + const count = cart.reduce((sum, item) => sum + item.quantity, 0) + setTotalItemCount(count) + }, [cart]) useEffect(() => { const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm); - }, 500); - return () => clearTimeout(timer); - }, [searchTerm]); + setDebouncedSearchTerm(searchTerm) + }, 500) + return () => clearTimeout(timer) + }, [searchTerm]) - const addToCart = useCallback((product: ProductWithUI) => { - const remainingStock = getRemainingStock(product); - if (remainingStock <= 0) { - addNotification('재고가 부족합니다!', 'error'); - return; - } - - setCart(prevCart => { - const existingItem = prevCart.find(item => item.product.id === product.id); - - if (existingItem) { - const newQuantity = existingItem.quantity + 1; - - if (newQuantity > product.stock) { - addNotification(`재고는 ${product.stock}개까지만 있습니다.`, 'error'); - return prevCart; - } - - return prevCart.map(item => - item.product.id === product.id - ? { ...item, quantity: newQuantity } - : item - ); + const formatPrice = (price: number, productId?: string): string => { + if (productId) { + const product = products.find((p) => p.id === productId) + if (product && getRemainingStock(product, cart) <= 0) { + return 'SOLD OUT' } - - return [...prevCart, { product, quantity: 1 }]; - }); - - addNotification('장바구니에 담았습니다', 'success'); - }, [cart, addNotification, getRemainingStock]); - - const removeFromCart = useCallback((productId: string) => { - setCart(prevCart => prevCart.filter(item => item.product.id !== productId)); - }, []); - - const updateQuantity = useCallback((productId: string, newQuantity: number) => { - if (newQuantity <= 0) { - removeFromCart(productId); - return; - } - - const product = products.find(p => p.id === productId); - if (!product) return; - - const maxStock = product.stock; - if (newQuantity > maxStock) { - addNotification(`재고는 ${maxStock}개까지만 있습니다.`, 'error'); - return; } - setCart(prevCart => - prevCart.map(item => - item.product.id === productId - ? { ...item, quantity: newQuantity } - : item - ) - ); - }, [products, removeFromCart, addNotification, getRemainingStock]); - - const applyCoupon = useCallback((coupon: Coupon) => { - const currentTotal = calculateCartTotal().totalAfterDiscount; - - if (currentTotal < 10000 && coupon.discountType === 'percentage') { - addNotification('percentage 쿠폰은 10,000원 이상 구매 시 사용 가능합니다.', 'error'); - return; + if (isAdmin) { + return `${price.toLocaleString()}원` } - setSelectedCoupon(coupon); - addNotification('쿠폰이 적용되었습니다.', 'success'); - }, [addNotification, calculateCartTotal]); + return `₩${price.toLocaleString()}` + } const completeOrder = useCallback(() => { - const orderNumber = `ORD-${Date.now()}`; - addNotification(`주문이 완료되었습니다. 주문번호: ${orderNumber}`, 'success'); - setCart([]); - setSelectedCoupon(null); - }, [addNotification]); - - const addProduct = useCallback((newProduct: Omit) => { - const product: ProductWithUI = { - ...newProduct, - id: `p${Date.now()}` - }; - setProducts(prev => [...prev, product]); - addNotification('상품이 추가되었습니다.', 'success'); - }, [addNotification]); - - const updateProduct = useCallback((productId: string, updates: Partial) => { - setProducts(prev => - prev.map(product => - product.id === productId - ? { ...product, ...updates } - : product - ) - ); - addNotification('상품이 수정되었습니다.', 'success'); - }, [addNotification]); - - const deleteProduct = useCallback((productId: string) => { - setProducts(prev => prev.filter(p => p.id !== productId)); - addNotification('상품이 삭제되었습니다.', 'success'); - }, [addNotification]); - - const addCoupon = useCallback((newCoupon: Coupon) => { - const existingCoupon = coupons.find(c => c.code === newCoupon.code); - if (existingCoupon) { - addNotification('이미 존재하는 쿠폰 코드입니다.', 'error'); - return; - } - setCoupons(prev => [...prev, newCoupon]); - addNotification('쿠폰이 추가되었습니다.', 'success'); - }, [coupons, addNotification]); - - const deleteCoupon = useCallback((couponCode: string) => { - setCoupons(prev => prev.filter(c => c.code !== couponCode)); - if (selectedCoupon?.code === couponCode) { - setSelectedCoupon(null); - } - addNotification('쿠폰이 삭제되었습니다.', 'success'); - }, [selectedCoupon, addNotification]); - - const handleProductSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (editingProduct && editingProduct !== 'new') { - updateProduct(editingProduct, productForm); - setEditingProduct(null); - } else { - addProduct({ - ...productForm, - discounts: productForm.discounts - }); - } - setProductForm({ name: '', price: 0, stock: 0, description: '', discounts: [] }); - setEditingProduct(null); - setShowProductForm(false); - }; - - const handleCouponSubmit = (e: React.FormEvent) => { - e.preventDefault(); - addCoupon(couponForm); - setCouponForm({ - name: '', - code: '', - discountType: 'amount', - discountValue: 0 - }); - setShowCouponForm(false); - }; - - const startEditProduct = (product: ProductWithUI) => { - setEditingProduct(product.id); - setProductForm({ - name: product.name, - price: product.price, - stock: product.stock, - description: product.description || '', - discounts: product.discounts || [] - }); - setShowProductForm(true); - }; + const orderNumber = `ORD-${Date.now()}` + addNotification( + `주문이 완료되었습니다. 주문번호: ${orderNumber}`, + 'success', + ) + setCart([]) + setSelectedCoupon(null) + }, [addNotification, setCart, setSelectedCoupon]) + + const totals = calculateCartTotal() + + const headerState = { + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, + } - const totals = calculateCartTotal(); + const adminState = { + activeTab, + setActiveTab, + products, + formatPrice, + deleteProduct, + addNotification, + coupons, + deleteCoupon, + updateProduct, + addProduct, + addCoupon, + } - const filteredProducts = debouncedSearchTerm - ? products.filter(product => - product.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) || - (product.description && product.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) - ) - : products; + const cartState = { + products, + cart, + coupons, + totals, + debouncedSearchTerm, + selectedCoupon, + addToCart, + removeFromCart, + formatPrice, + calculateItemTotal, + updateQuantity, + applyCoupon, + setSelectedCoupon, + completeOrder, + } return (
- {notifications.length > 0 && ( -
- {notifications.map(notif => ( -
- {notif.message} - -
- ))} -
- )} -
-
-
-
-

SHOP

- {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} - {!isAdmin && ( -
- setSearchTerm(e.target.value)} - placeholder="상품 검색..." - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" - /> -
- )} -
- -
-
-
- + +
- {isAdmin ? ( -
-
-

관리자 대시보드

-

상품과 쿠폰을 관리할 수 있습니다

-
-
- -
- - {activeTab === 'products' ? ( -
-
-
-

상품 목록

- -
-
- -
- - - - - - - - - - - - {(activeTab === 'products' ? products : products).map(product => ( - - - - - - - - ))} - -
상품명가격재고설명작업
{product.name}{formatPrice(product.price, product.id)} - 10 ? 'bg-green-100 text-green-800' : - product.stock > 0 ? 'bg-yellow-100 text-yellow-800' : - 'bg-red-100 text-red-800' - }`}> - {product.stock}개 - - {product.description || '-'} - - -
-
- {showProductForm && ( -
-
-

- {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} -

-
-
- - setProductForm({ ...productForm, name: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - required - /> -
-
- - setProductForm({ ...productForm, description: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - /> -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ ...productForm, price: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = e.target.value; - if (value === '') { - setProductForm({ ...productForm, price: 0 }); - } else if (parseInt(value) < 0) { - addNotification('가격은 0보다 커야 합니다', 'error'); - setProductForm({ ...productForm, price: 0 }); - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ ...productForm, stock: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = e.target.value; - if (value === '') { - setProductForm({ ...productForm, stock: 0 }); - } else if (parseInt(value) < 0) { - addNotification('재고는 0보다 커야 합니다', 'error'); - setProductForm({ ...productForm, stock: 0 }); - } else if (parseInt(value) > 9999) { - addNotification('재고는 9999개를 초과할 수 없습니다', 'error'); - setProductForm({ ...productForm, stock: 9999 }); - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
-
- -
- {productForm.discounts.map((discount, index) => ( -
- { - const newDiscounts = [...productForm.discounts]; - newDiscounts[index].quantity = parseInt(e.target.value) || 0; - setProductForm({ ...productForm, discounts: newDiscounts }); - }} - className="w-20 px-2 py-1 border rounded" - min="1" - placeholder="수량" - /> - 개 이상 구매 시 - { - const newDiscounts = [...productForm.discounts]; - newDiscounts[index].rate = (parseInt(e.target.value) || 0) / 100; - setProductForm({ ...productForm, discounts: newDiscounts }); - }} - className="w-16 px-2 py-1 border rounded" - min="0" - max="100" - placeholder="%" - /> - % 할인 - -
- ))} - -
-
- -
- - -
-
-
- )} -
- ) : ( -
-
-

쿠폰 관리

-
-
-
- {coupons.map(coupon => ( -
-
-
-

{coupon.name}

-

{coupon.code}

-
- - {coupon.discountType === 'amount' - ? `${coupon.discountValue.toLocaleString()}원 할인` - : `${coupon.discountValue}% 할인`} - -
-
- -
-
- ))} - -
- -
-
- - {showCouponForm && ( -
-
-

새 쿠폰 생성

-
-
- - setCouponForm({ ...couponForm, name: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder="신규 가입 쿠폰" - required - /> -
-
- - setCouponForm({ ...couponForm, code: e.target.value.toUpperCase() })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" - placeholder="WELCOME2024" - required - /> -
-
- - -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setCouponForm({ ...couponForm, discountValue: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value) || 0; - if (couponForm.discountType === 'percentage') { - if (value > 100) { - addNotification('할인율은 100%를 초과할 수 없습니다', 'error'); - setCouponForm({ ...couponForm, discountValue: 100 }); - } else if (value < 0) { - setCouponForm({ ...couponForm, discountValue: 0 }); - } - } else { - if (value > 100000) { - addNotification('할인 금액은 100,000원을 초과할 수 없습니다', 'error'); - setCouponForm({ ...couponForm, discountValue: 100000 }); - } else if (value < 0) { - setCouponForm({ ...couponForm, discountValue: 0 }); - } - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder={couponForm.discountType === 'amount' ? '5000' : '10'} - required - /> -
-
-
- - -
-
-
- )} -
-
- )} -
- ) : ( -
-
- {/* 상품 목록 */} -
-
-

전체 상품

-
- 총 {products.length}개 상품 -
-
- {filteredProducts.length === 0 ? ( -
-

"{debouncedSearchTerm}"에 대한 검색 결과가 없습니다.

-
- ) : ( -
- {filteredProducts.map(product => { - const remainingStock = getRemainingStock(product); - - return ( -
- {/* 상품 이미지 영역 (placeholder) */} -
-
- - - -
- {product.isRecommended && ( - - BEST - - )} - {product.discounts.length > 0 && ( - - ~{Math.max(...product.discounts.map(d => d.rate)) * 100}% - - )} -
- - {/* 상품 정보 */} -
-

{product.name}

- {product.description && ( -

{product.description}

- )} - - {/* 가격 정보 */} -
-

{formatPrice(product.price, product.id)}

- {product.discounts.length > 0 && ( -

- {product.discounts[0].quantity}개 이상 구매시 할인 {product.discounts[0].rate * 100}% -

- )} -
- - {/* 재고 상태 */} -
- {remainingStock <= 5 && remainingStock > 0 && ( -

품절임박! {remainingStock}개 남음

- )} - {remainingStock > 5 && ( -

재고 {remainingStock}개

- )} -
- - {/* 장바구니 버튼 */} - -
-
- ); - })} -
- )} -
-
- -
-
-
-

- - - - 장바구니 -

- {cart.length === 0 ? ( -
- - - -

장바구니가 비어있습니다

-
- ) : ( -
- {cart.map(item => { - const itemTotal = calculateItemTotal(item); - const originalPrice = item.product.price * item.quantity; - const hasDiscount = itemTotal < originalPrice; - const discountRate = hasDiscount ? Math.round((1 - itemTotal / originalPrice) * 100) : 0; - - return ( -
-
-

{item.product.name}

- -
-
-
- - {item.quantity} - -
-
- {hasDiscount && ( - -{discountRate}% - )} -

- {Math.round(itemTotal).toLocaleString()}원 -

-
-
-
- ); - })} -
- )} -
- - {cart.length > 0 && ( - <> -
-
-

쿠폰 할인

- -
- {coupons.length > 0 && ( - - )} -
- -
-

결제 정보

-
-
- 상품 금액 - {totals.totalBeforeDiscount.toLocaleString()}원 -
- {totals.totalBeforeDiscount - totals.totalAfterDiscount > 0 && ( -
- 할인 금액 - -{(totals.totalBeforeDiscount - totals.totalAfterDiscount).toLocaleString()}원 -
- )} -
- 결제 예정 금액 - {totals.totalAfterDiscount.toLocaleString()}원 -
-
- - - -
-

* 실제 결제는 이루어지지 않습니다

-
-
- - )} -
-
-
- )} + {isAdmin ? : }
- ); -}; + ) +} -export default App; \ No newline at end of file +export default App diff --git a/src/basic/components/AdminPage.tsx b/src/basic/components/AdminPage.tsx new file mode 100644 index 00000000..a3e13f54 --- /dev/null +++ b/src/basic/components/AdminPage.tsx @@ -0,0 +1,693 @@ +// TODO: 관리자 페이지 컴포넌트 +// 힌트: +// 1. 탭 UI로 상품 관리와 쿠폰 관리 분리 +// 2. 상품 추가/수정/삭제 기능 +// 3. 쿠폰 생성 기능 +// 4. 할인 규칙 설정 +// +// 필요한 hooks: +// - useProducts: 상품 CRUD +// - useCoupons: 쿠폰 CRUD +// +// 하위 컴포넌트: +// - ProductForm: 새 상품 추가 폼 +// - ProductAccordion: 상품 정보 표시 및 수정 +// - CouponForm: 새 쿠폰 추가 폼 +// - CouponList: 쿠폰 목록 표시 + +import { useState } from 'react' +import { Coupon } from '../../types' +import { ProductWithUI } from '../types' + +export function AdminPage({ + activeTab, + setActiveTab, + products, + formatPrice, + deleteProduct, + addNotification, + coupons, + deleteCoupon, + updateProduct, + addProduct, + addCoupon, +}: { + activeTab: 'products' | 'coupons' + setActiveTab: React.Dispatch> + products: ProductWithUI[] + formatPrice: (price: number, productId?: string) => string + deleteProduct: (productId: string) => void + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void + coupons: Coupon[] + deleteCoupon: (couponCode: string) => void + updateProduct: (productId: string, updates: Partial) => void + addProduct: (newProduct: Omit) => void + addCoupon: (newCoupon: Coupon) => void +}) { + // TODO: 구현 + const [showCouponForm, setShowCouponForm] = useState(false) + const [showProductForm, setShowProductForm] = useState(false) + const [editingProduct, setEditingProduct] = useState(null) + const [productForm, setProductForm] = useState({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [] as Array<{ quantity: number; rate: number }>, + }) + + const [couponForm, setCouponForm] = useState({ + name: '', + code: '', + discountType: 'amount' as 'amount' | 'percentage', + discountValue: 0, + }) + + const handleProductSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (editingProduct && editingProduct !== 'new') { + updateProduct(editingProduct, productForm) + setEditingProduct(null) + } else { + addProduct({ + ...productForm, + discounts: productForm.discounts, + }) + } + setProductForm({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + setEditingProduct(null) + setShowProductForm(false) + } + + const handleCouponSubmit = (e: React.FormEvent) => { + e.preventDefault() + addCoupon(couponForm) + setCouponForm({ + name: '', + code: '', + discountType: 'amount', + discountValue: 0, + }) + setShowCouponForm(false) + } + + const startEditProduct = (product: ProductWithUI) => { + setEditingProduct(product.id) + setProductForm({ + name: product.name, + price: product.price, + stock: product.stock, + description: product.description || '', + discounts: product.discounts || [], + }) + setShowProductForm(true) + } + + return ( + <> +
+
+

관리자 대시보드

+

상품과 쿠폰을 관리할 수 있습니다

+
+
+ +
+ + {activeTab === 'products' ? ( +
+
+
+

상품 목록

+ +
+
+ +
+ + + + + + + + + + + + {(activeTab === 'products' ? products : products).map( + (product) => ( + + + + + + + + ), + )} + +
+ 상품명 + + 가격 + + 재고 + + 설명 + + 작업 +
+ {product.name} + + {formatPrice(product.price, product.id)} + + 10 + ? 'bg-green-100 text-green-800' + : product.stock > 0 + ? 'bg-yellow-100 text-yellow-800' + : 'bg-red-100 text-red-800' + }`} + > + {product.stock}개 + + + {product.description || '-'} + + + +
+
+ {showProductForm && ( +
+
+

+ {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} +

+
+
+ + + setProductForm({ + ...productForm, + name: e.target.value, + }) + } + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + required + /> +
+
+ + + setProductForm({ + ...productForm, + description: e.target.value, + }) + } + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + /> +
+
+ + { + const value = e.target.value + if (value === '' || /^\d+$/.test(value)) { + setProductForm({ + ...productForm, + price: value === '' ? 0 : parseInt(value), + }) + } + }} + onBlur={(e) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, price: 0 }) + } else if (parseInt(value) < 0) { + addNotification('가격은 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, price: 0 }) + } + }} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+ + { + const value = e.target.value + if (value === '' || /^\d+$/.test(value)) { + setProductForm({ + ...productForm, + stock: value === '' ? 0 : parseInt(value), + }) + } + }} + onBlur={(e) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) < 0) { + addNotification('재고는 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) > 9999) { + addNotification( + '재고는 9999개를 초과할 수 없습니다', + 'error', + ) + setProductForm({ ...productForm, stock: 9999 }) + } + }} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+
+ +
+ {productForm.discounts.map((discount, index) => ( +
+ { + const newDiscounts = [...productForm.discounts] + newDiscounts[index].quantity = + parseInt(e.target.value) || 0 + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + }} + className="w-20 px-2 py-1 border rounded" + min="1" + placeholder="수량" + /> + 개 이상 구매 시 + { + const newDiscounts = [...productForm.discounts] + newDiscounts[index].rate = + (parseInt(e.target.value) || 0) / 100 + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + }} + className="w-16 px-2 py-1 border rounded" + min="0" + max="100" + placeholder="%" + /> + % 할인 + +
+ ))} + +
+
+ +
+ + +
+
+
+ )} +
+ ) : ( +
+
+

쿠폰 관리

+
+
+
+ {coupons.map((coupon) => ( +
+
+
+

+ {coupon.name} +

+

+ {coupon.code} +

+
+ + {coupon.discountType === 'amount' + ? `${coupon.discountValue.toLocaleString()}원 할인` + : `${coupon.discountValue}% 할인`} + +
+
+ +
+
+ ))} + +
+ +
+
+ + {showCouponForm && ( +
+
+

+ 새 쿠폰 생성 +

+
+
+ + + setCouponForm({ + ...couponForm, + name: e.target.value, + }) + } + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder="신규 가입 쿠폰" + required + /> +
+
+ + + setCouponForm({ + ...couponForm, + code: e.target.value.toUpperCase(), + }) + } + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" + placeholder="WELCOME2024" + required + /> +
+
+ + +
+
+ + { + const value = e.target.value + if (value === '' || /^\d+$/.test(value)) { + setCouponForm({ + ...couponForm, + discountValue: + value === '' ? 0 : parseInt(value), + }) + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value) || 0 + if (couponForm.discountType === 'percentage') { + if (value > 100) { + addNotification( + '할인율은 100%를 초과할 수 없습니다', + 'error', + ) + setCouponForm({ + ...couponForm, + discountValue: 100, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } else { + if (value > 100000) { + addNotification( + '할인 금액은 100,000원을 초과할 수 없습니다', + 'error', + ) + setCouponForm({ + ...couponForm, + discountValue: 100000, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } + }} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder={ + couponForm.discountType === 'amount' ? '5000' : '10' + } + required + /> +
+
+
+ + +
+
+
+ )} +
+
+ )} +
+ + ) +} diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx new file mode 100644 index 00000000..f31bb6ce --- /dev/null +++ b/src/basic/components/CartPage.tsx @@ -0,0 +1,407 @@ +// TODO: 장바구니 페이지 컴포넌트 +// 힌트: +// 1. 상품 목록 표시 (검색 기능 포함) +// 2. 장바구니 관리 +// 3. 쿠폰 적용 +// 4. 주문 처리 +// +// 필요한 hooks: +// - useProducts: 상품 목록 관리 +// - useCart: 장바구니 상태 관리 +// - useCoupons: 쿠폰 목록 관리 +// - useDebounce: 검색어 디바운싱 +// +// 하위 컴포넌트: +// - SearchBar: 검색 입력 +// - ProductList: 상품 목록 표시 +// - Cart: 장바구니 표시 및 결제 + +import { CartItem, Coupon } from '../../types' +import { getRemainingStock } from '../models/cart' +import { ProductWithUI } from '../types' + +export function CartPage({ + products, + cart, + coupons, + totals, + debouncedSearchTerm, + selectedCoupon, + addToCart, + removeFromCart, + formatPrice, + calculateItemTotal, + updateQuantity, + applyCoupon, + setSelectedCoupon, + completeOrder, +}: { + products: ProductWithUI[] + cart: CartItem[] + coupons: Coupon[] + totals: { + totalBeforeDiscount: number + totalAfterDiscount: number + } + debouncedSearchTerm: string + selectedCoupon: Coupon | null + addToCart: (product: ProductWithUI) => void + formatPrice: (price: number, productId?: string) => string + removeFromCart: (productId: string) => void + calculateItemTotal: (item: CartItem, cart: CartItem[]) => number + updateQuantity: ( + productId: string, + newQuantity: number, + products: ProductWithUI[], + ) => void + applyCoupon: (coupon: Coupon) => void + setSelectedCoupon: React.Dispatch> + completeOrder: () => void +}) { + // TODO: 구현 + + const filteredProducts = debouncedSearchTerm + ? products.filter( + (product) => + product.name + .toLowerCase() + .includes(debouncedSearchTerm.toLowerCase()) || + (product.description && + product.description + .toLowerCase() + .includes(debouncedSearchTerm.toLowerCase())), + ) + : products + + return ( + <> +
+
+ {/* 상품 목록 */} +
+
+

+ 전체 상품 +

+
+ 총 {products.length}개 상품 +
+
+ {filteredProducts.length === 0 ? ( +
+

+ "{debouncedSearchTerm}"에 대한 검색 결과가 없습니다. +

+
+ ) : ( +
+ {filteredProducts.map((product) => { + const remainingStock = getRemainingStock(product, cart) + + return ( +
+ {/* 상품 이미지 영역 (placeholder) */} +
+
+ + + +
+ {product.isRecommended && ( + + BEST + + )} + {product.discounts.length > 0 && ( + + ~ + {Math.max(...product.discounts.map((d) => d.rate)) * + 100} + % + + )} +
+ + {/* 상품 정보 */} +
+

+ {product.name} +

+ {product.description && ( +

+ {product.description} +

+ )} + + {/* 가격 정보 */} +
+

+ {formatPrice(product.price, product.id)} +

+ {product.discounts.length > 0 && ( +

+ {product.discounts[0].quantity}개 이상 구매시 할인{' '} + {product.discounts[0].rate * 100}% +

+ )} +
+ + {/* 재고 상태 */} +
+ {remainingStock <= 5 && remainingStock > 0 && ( +

+ 품절임박! {remainingStock}개 남음 +

+ )} + {remainingStock > 5 && ( +

+ 재고 {remainingStock}개 +

+ )} +
+ + {/* 장바구니 버튼 */} + +
+
+ ) + })} +
+ )} +
+
+ +
+
+
+

+ + + + 장바구니 +

+ {cart.length === 0 ? ( +
+ + + +

+ 장바구니가 비어있습니다 +

+
+ ) : ( +
+ {cart.map((item) => { + const itemTotal = calculateItemTotal(item, cart) + const originalPrice = item.product.price * item.quantity + const hasDiscount = itemTotal < originalPrice + const discountRate = hasDiscount + ? Math.round((1 - itemTotal / originalPrice) * 100) + : 0 + + return ( +
+
+

+ {item.product.name} +

+ +
+
+
+ + + {item.quantity} + + +
+
+ {hasDiscount && ( + + -{discountRate}% + + )} +

+ {Math.round(itemTotal).toLocaleString()}원 +

+
+
+
+ ) + })} +
+ )} +
+ + {cart.length > 0 && ( + <> +
+
+

+ 쿠폰 할인 +

+ +
+ {coupons.length > 0 && ( + + )} +
+ +
+

결제 정보

+
+
+ 상품 금액 + + {totals.totalBeforeDiscount.toLocaleString()}원 + +
+ {totals.totalBeforeDiscount - totals.totalAfterDiscount > + 0 && ( +
+ 할인 금액 + + - + {( + totals.totalBeforeDiscount - + totals.totalAfterDiscount + ).toLocaleString()} + 원 + +
+ )} +
+ 결제 예정 금액 + + {totals.totalAfterDiscount.toLocaleString()}원 + +
+
+ + + +
+

* 실제 결제는 이루어지지 않습니다

+
+
+ + )} +
+
+
+ + ) +} diff --git a/src/basic/components/icons/index.tsx b/src/basic/components/icons/index.tsx new file mode 100644 index 00000000..1609d774 --- /dev/null +++ b/src/basic/components/icons/index.tsx @@ -0,0 +1,12 @@ +// TODO: SVG 아이콘 컴포넌트들 +// 구현할 아이콘: +// - CartIcon: 장바구니 아이콘 +// - AdminIcon: 관리자 아이콘 +// - PlusIcon: 플러스 아이콘 +// - MinusIcon: 마이너스 아이콘 +// - TrashIcon: 삭제 아이콘 +// - ChevronDownIcon: 아래 화살표 +// - ChevronUpIcon: 위 화살표 +// - CheckIcon: 체크 아이콘 + +// TODO: 구현 \ No newline at end of file diff --git a/src/basic/components/ui/Header.tsx b/src/basic/components/ui/Header.tsx new file mode 100644 index 00000000..c16850f0 --- /dev/null +++ b/src/basic/components/ui/Header.tsx @@ -0,0 +1,77 @@ +import { CartItem } from '../../../types' + +export const Header = ({ + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, +}: { + isAdmin: boolean + searchTerm: string + cart: CartItem[] + totalItemCount: number + setSearchTerm: React.Dispatch> + setIsAdmin: React.Dispatch> +}) => { + return ( + <> +
+
+
+
+

SHOP

+ {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} + {!isAdmin && ( +
+ setSearchTerm(e.target.value)} + placeholder="상품 검색..." + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+ )} +
+ +
+
+
+ + ) +} diff --git a/src/basic/components/ui/Notifications.tsx b/src/basic/components/ui/Notifications.tsx new file mode 100644 index 00000000..d9c1eb1d --- /dev/null +++ b/src/basic/components/ui/Notifications.tsx @@ -0,0 +1,54 @@ +import { Notification } from '../../types' + +export const Notifications = ({ + notifications, + setNotifications, +}: { + notifications: Notification[] + setNotifications: React.Dispatch> +}) => { + return ( + <> + {notifications.length > 0 && ( +
+ {notifications.map((notif) => ( +
+ {notif.message} + +
+ ))} +
+ )} + + ) +} diff --git a/src/basic/components/ui/ProductList.tsx b/src/basic/components/ui/ProductList.tsx new file mode 100644 index 00000000..56ae8678 --- /dev/null +++ b/src/basic/components/ui/ProductList.tsx @@ -0,0 +1,105 @@ +import { CartItem } from '../../../types' +import { getRemainingStock } from '../../models/cart' +import { ProductWithUI } from '../../types' + +export function ProductList({ + filteredProducts, + cart, +}: { + filteredProducts: ProductWithUI[] + cart: CartItem[] +}) { + return ( +
+ {filteredProducts.map((product) => { + const remainingStock = getRemainingStock(product, cart) + + return ( +
+ {/* 상품 이미지 영역 (placeholder) */} +
+
+ + + +
+ {product.isRecommended && ( + + BEST + + )} + {product.discounts.length > 0 && ( + + ~{Math.max(...product.discounts.map((d) => d.rate)) * 100}% + + )} +
+ + {/* 상품 정보 */} +
+

{product.name}

+ {product.description && ( +

+ {product.description} +

+ )} + + {/* 가격 정보 */} +
+

+ {formatPrice(product.price, product.id)} +

+ {product.discounts.length > 0 && ( +

+ {product.discounts[0].quantity}개 이상 구매시 할인{' '} + {product.discounts[0].rate * 100}% +

+ )} +
+ + {/* 재고 상태 */} +
+ {remainingStock <= 5 && remainingStock > 0 && ( +

+ 품절임박! {remainingStock}개 남음 +

+ )} + {remainingStock > 5 && ( +

+ 재고 {remainingStock}개 +

+ )} +
+ + {/* 장바구니 버튼 */} + +
+
+ ) + })} +
+ ) +} diff --git a/src/basic/constants/index.ts b/src/basic/constants/index.ts new file mode 100644 index 00000000..2733a6f2 --- /dev/null +++ b/src/basic/constants/index.ts @@ -0,0 +1,61 @@ +// TODO: 초기 데이터 상수 +// 정의할 상수들: +// - initialProducts: 초기 상품 목록 (상품1, 상품2, 상품3 + 설명 필드 포함) +// - initialCoupons: 초기 쿠폰 목록 (5000원 할인, 10% 할인) +// +// 참고: origin/App.tsx의 초기 데이터 구조를 참조 + +import { Coupon } from '../../types' +import { ProductWithUI } from '../types' + +// TODO: 구현 + +// 초기 데이터 +export const initialProducts: ProductWithUI[] = [ + { + id: 'p1', + name: '상품1', + price: 10000, + stock: 20, + discounts: [ + { quantity: 10, rate: 0.1 }, + { quantity: 20, rate: 0.2 }, + ], + description: '최고급 품질의 프리미엄 상품입니다.', + }, + { + id: 'p2', + name: '상품2', + price: 20000, + stock: 20, + discounts: [{ quantity: 10, rate: 0.15 }], + description: '다양한 기능을 갖춘 실용적인 상품입니다.', + isRecommended: true, + }, + { + id: 'p3', + name: '상품3', + price: 30000, + stock: 20, + discounts: [ + { quantity: 10, rate: 0.2 }, + { quantity: 30, rate: 0.25 }, + ], + description: '대용량과 고성능을 자랑하는 상품입니다.', + }, +] + +export const initialCoupons: Coupon[] = [ + { + name: '5000원 할인', + code: 'AMOUNT5000', + discountType: 'amount', + discountValue: 5000, + }, + { + name: '10% 할인', + code: 'PERCENT10', + discountType: 'percentage', + discountValue: 10, + }, +] diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts new file mode 100644 index 00000000..08fddf19 --- /dev/null +++ b/src/basic/hooks/useCart.ts @@ -0,0 +1,200 @@ +// TODO: 장바구니 관리 Hook +// 힌트: +// 1. 장바구니 상태 관리 (localStorage 연동) +// 2. 상품 추가/삭제/수량 변경 +// 3. 쿠폰 적용 +// 4. 총액 계산 +// 5. 재고 확인 +// +// 사용할 모델 함수: +// - cartModel.addItemToCart +// - cartModel.removeItemFromCart +// - cartModel.updateCartItemQuantity +// - cartModel.calculateCartTotal +// - cartModel.getRemainingStock +// +// 반환할 값: +// - cart: 장바구니 아이템 배열 +// - selectedCoupon: 선택된 쿠폰 +// - addToCart: 상품 추가 함수 +// - removeFromCart: 상품 제거 함수 +// - updateQuantity: 수량 변경 함수 +// - applyCoupon: 쿠폰 적용 함수 +// - calculateTotal: 총액 계산 함수 +// - getRemainingStock: 재고 확인 함수 +// - clearCart: 장바구니 비우기 함수 + +import { useCallback, useEffect, useState } from 'react' +import { CartItem, Coupon } from '../../types' +import { ProductWithUI } from '../types' +import { getRemainingStock, calculateItemTotal } from '../models/cart' + +export function useCart( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + // TODO: 구현 + + const [selectedCoupon, setSelectedCoupon] = useState(null) + const [cart, setCart] = useState(() => { + const saved = localStorage.getItem('cart') + if (saved) { + try { + return JSON.parse(saved) + } catch { + return [] + } + } + return [] + }) + + const addToCart = useCallback( + (product: ProductWithUI) => { + const remainingStock = getRemainingStock(product, cart) + if (remainingStock <= 0) { + addNotification('재고가 부족합니다!', 'error') + return + } + + // const cartItem = cart.find(item => item.product.id === product.id) + + setCart((prevCart) => { + const existingItem = prevCart.find( + (item) => item.product.id === product.id, + ) + + if (existingItem) { + const newQuantity = existingItem.quantity + 1 + + if (newQuantity > product.stock) { + addNotification( + `재고는 ${product.stock}개까지만 있습니다.`, + 'error', + ) + return prevCart + } + + return prevCart.map((item) => + item.product.id === product.id + ? { ...item, quantity: newQuantity } + : item, + ) + } + + return [...prevCart, { product, quantity: 1 }] + }) + + addNotification('장바구니에 담았습니다', 'success') + }, + [addNotification, cart], + ) + + const removeFromCart = useCallback( + (productId: string) => { + setCart((prevCart) => + prevCart.filter((item) => item.product.id !== productId), + ) + }, + [setCart], + ) + + const updateQuantity = useCallback( + (productId: string, newQuantity: number, products: ProductWithUI[]) => { + if (newQuantity <= 0) { + removeFromCart(productId) + return + } + + const product = products.find((p) => p.id === productId) + if (!product) return + + const maxStock = product.stock + if (newQuantity > maxStock) { + addNotification(`재고는 ${maxStock}개까지만 있습니다.`, 'error') + return + } + + setCart((prevCart) => + prevCart.map((item) => + item.product.id === productId + ? { ...item, quantity: newQuantity } + : item, + ), + ) + }, + [setCart, removeFromCart, addNotification], + ) + + const calculateCartTotal = useCallback((): { + totalBeforeDiscount: number + totalAfterDiscount: number + } => { + let totalBeforeDiscount = 0 + let totalAfterDiscount = 0 + + cart.forEach((item) => { + const itemPrice = item.product.price * item.quantity + totalBeforeDiscount += itemPrice + totalAfterDiscount += calculateItemTotal(item, cart) + }) + + if (selectedCoupon) { + if (selectedCoupon.discountType === 'amount') { + totalAfterDiscount = Math.max( + 0, + totalAfterDiscount - selectedCoupon.discountValue, + ) + } else { + totalAfterDiscount = Math.round( + totalAfterDiscount * (1 - selectedCoupon.discountValue / 100), + ) + } + } + + return { + totalBeforeDiscount: Math.round(totalBeforeDiscount), + totalAfterDiscount: Math.round(totalAfterDiscount), + } + }, [cart, selectedCoupon]) + + const applyCoupon = useCallback( + (coupon: Coupon) => { + const currentTotal = calculateCartTotal().totalAfterDiscount + + if (currentTotal < 10000 && coupon.discountType === 'percentage') { + addNotification( + 'percentage 쿠폰은 10,000원 이상 구매 시 사용 가능합니다.', + 'error', + ) + return + } + + setSelectedCoupon(coupon) + addNotification('쿠폰이 적용되었습니다.', 'success') + }, + [addNotification, calculateCartTotal, setSelectedCoupon], + ) + + useEffect(() => { + if (cart.length > 0) { + localStorage.setItem('cart', JSON.stringify(cart)) + } else { + localStorage.removeItem('cart') + } + }, [cart]) + + return { + cart, + setCart, + addToCart, + removeFromCart, + updateQuantity, + selectedCoupon, + setSelectedCoupon, + applyCoupon, + calculateCartTotal, + calculateItemTotal, + } +} diff --git a/src/basic/hooks/useCoupons.ts b/src/basic/hooks/useCoupons.ts new file mode 100644 index 00000000..632b9383 --- /dev/null +++ b/src/basic/hooks/useCoupons.ts @@ -0,0 +1,70 @@ +// TODO: 쿠폰 관리 Hook +// 힌트: +// 1. 쿠폰 목록 상태 관리 (localStorage 연동 고려) +// 2. 쿠폰 추가/삭제 +// +// 반환할 값: +// - coupons: 쿠폰 배열 +// - addCoupon: 새 쿠폰 추가 +// - removeCoupon: 쿠폰 삭제 + +import { useCallback, useEffect, useState } from 'react' +import { initialCoupons } from '../constants' +import { Coupon } from '../../types' + +export function useCoupons( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, + selectedCoupon: Coupon | null, + setSelectedCoupon: React.Dispatch>, +) { + // TODO: 구현 + + const [coupons, setCoupons] = useState(() => { + const saved = localStorage.getItem('coupons') + if (saved) { + try { + return JSON.parse(saved) + } catch { + return initialCoupons + } + } + return initialCoupons + }) + + useEffect(() => { + localStorage.setItem('coupons', JSON.stringify(coupons)) + }, [coupons]) + + const addCoupon = useCallback( + (newCoupon: Coupon) => { + const existingCoupon = coupons.find((c) => c.code === newCoupon.code) + if (existingCoupon) { + addNotification('이미 존재하는 쿠폰 코드입니다.', 'error') + return + } + setCoupons((prev) => [...prev, newCoupon]) + addNotification('쿠폰이 추가되었습니다.', 'success') + }, + [coupons, addNotification], + ) + + const deleteCoupon = useCallback( + (couponCode: string) => { + setCoupons((prev) => prev.filter((c) => c.code !== couponCode)) + if (selectedCoupon?.code === couponCode) { + setSelectedCoupon(null) + } + addNotification('쿠폰이 삭제되었습니다.', 'success') + }, + [setCoupons, selectedCoupon?.code, addNotification, setSelectedCoupon], + ) + + return { + coupons, + addCoupon, + deleteCoupon, + } +} diff --git a/src/basic/hooks/useProducts.ts b/src/basic/hooks/useProducts.ts new file mode 100644 index 00000000..b8e4ba66 --- /dev/null +++ b/src/basic/hooks/useProducts.ts @@ -0,0 +1,82 @@ +// TODO: 상품 관리 Hook +// 힌트: +// 1. 상품 목록 상태 관리 (localStorage 연동 고려) +// 2. 상품 CRUD 작업 +// 3. 재고 업데이트 +// 4. 할인 규칙 추가/삭제 +// +// 반환할 값: +// - products: 상품 배열 +// - updateProduct: 상품 정보 수정 +// - addProduct: 새 상품 추가 +// - updateProductStock: 재고 수정 +// - addProductDiscount: 할인 규칙 추가 +// - removeProductDiscount: 할인 규칙 삭제 + +import { useCallback, useEffect, useState } from 'react' +import { initialProducts } from '../constants' +import { ProductWithUI } from '../types' + +export function useProducts( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + // TODO: 구현 + + const [products, setProducts] = useState(() => { + const saved = localStorage.getItem('products') + if (saved) { + try { + return JSON.parse(saved) + } catch { + return initialProducts + } + } + return initialProducts + }) + + useEffect(() => { + localStorage.setItem('products', JSON.stringify(products)) + }, [products]) + + const addProduct = useCallback( + (newProduct: Omit) => { + const product: ProductWithUI = { + ...newProduct, + id: `p${Date.now()}`, + } + setProducts((prev) => [...prev, product]) + addNotification('상품이 추가되었습니다.', 'success') + }, + [addNotification], + ) + + const updateProduct = useCallback( + (productId: string, updates: Partial) => { + setProducts((prev) => + prev.map((product) => + product.id === productId ? { ...product, ...updates } : product, + ), + ) + addNotification('상품이 수정되었습니다.', 'success') + }, + [addNotification], + ) + + const deleteProduct = useCallback( + (productId: string) => { + setProducts((prev) => prev.filter((p) => p.id !== productId)) + addNotification('상품이 삭제되었습니다.', 'success') + }, + [addNotification, setProducts], + ) + + return { + products, + addProduct, + updateProduct, + deleteProduct, + } +} diff --git a/src/basic/models/cart.ts b/src/basic/models/cart.ts new file mode 100644 index 00000000..1c385d2b --- /dev/null +++ b/src/basic/models/cart.ts @@ -0,0 +1,92 @@ +// TODO: 장바구니 비즈니스 로직 (순수 함수) +// 힌트: 모든 함수는 순수 함수로 구현 (부작용 없음, 같은 입력에 항상 같은 출력) +// +// 구현할 함수들: +// 1. calculateItemTotal(item): 개별 아이템의 할인 적용 후 총액 계산 +// 2. getMaxApplicableDiscount(item): 적용 가능한 최대 할인율 계산 +// 3. calculateCartTotal(cart, coupon): 장바구니 총액 계산 (할인 전/후, 할인액) +// 4. updateCartItemQuantity(cart, productId, quantity): 수량 변경 +// 5. addItemToCart(cart, product): 상품 추가 +// 6. removeItemFromCart(cart, productId): 상품 제거 +// 7. getRemainingStock(product, cart): 남은 재고 계산 +// +// 원칙: +// - UI와 관련된 로직 없음 +// - 외부 상태에 의존하지 않음 +// - 모든 필요한 데이터는 파라미터로 전달받음 + +import { CartItem, Coupon, Product } from '../../types' +// TODO: 구현 + +export const getRemainingStock = ( + product: Product, + cart: CartItem[], +): number => { + const cartItem = cart.find((item) => item.product.id === product.id) + const remaining = product.stock - (cartItem?.quantity || 0) + + return remaining +} + +const getBasicDiscont = (item: CartItem) => { + const { discounts } = item.product + const { quantity } = item + + return discounts.reduce((maxDiscount, discount) => { + return quantity >= discount.quantity && discount.rate > maxDiscount + ? discount.rate + : maxDiscount + }, 0) +} + +export const getMaxApplicableDiscount = ( + item: CartItem, + cart: CartItem[], +): number => { + const baseDiscount = getBasicDiscont(item) + const hasBulkPurchase = cart.some((cartItem) => cartItem.quantity >= 10) + return hasBulkPurchase ? Math.min(baseDiscount + 0.05, 0.5) : baseDiscount +} + +export const calculateItemTotal = ( + item: CartItem, + cart: CartItem[], +): number => { + const { price } = item.product + const { quantity } = item + const discount = getMaxApplicableDiscount(item, cart) + + return Math.round(price * quantity * (1 - discount)) +} + +export const calculateCartTotal = ( + cart: CartItem[], + coupon: Coupon, +): { totalBeforeDiscount: number; totalAfterDiscount: number } => { + let totalBeforeDiscount = 0 + let totalAfterDiscount = 0 + + cart.forEach((item) => { + const itemPrice = item.product.price * item.quantity + totalBeforeDiscount += itemPrice + totalAfterDiscount += calculateItemTotal(item, cart) + }) + + if (coupon) { + if (coupon.discountType === 'amount') { + totalAfterDiscount = Math.max( + 0, + totalAfterDiscount - coupon.discountValue, + ) + } else { + totalAfterDiscount = Math.round( + totalAfterDiscount * (1 - coupon.discountValue / 100), + ) + } + } + + return { + totalBeforeDiscount: Math.round(totalBeforeDiscount), + totalAfterDiscount: Math.round(totalAfterDiscount), + } +} diff --git a/src/basic/models/coupon.ts b/src/basic/models/coupon.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/basic/models/discount.ts b/src/basic/models/discount.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/basic/models/product.ts b/src/basic/models/product.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/basic/types/index.ts b/src/basic/types/index.ts new file mode 100644 index 00000000..a7fc85de --- /dev/null +++ b/src/basic/types/index.ts @@ -0,0 +1,12 @@ +import { Product } from '../../types' + +export interface ProductWithUI extends Product { + description?: string + isRecommended?: boolean +} + +export interface Notification { + id: string + message: string + type: 'error' | 'success' | 'warning' +} diff --git a/src/basic/utils/formatters.ts b/src/basic/utils/formatters.ts new file mode 100644 index 00000000..ff157f5c --- /dev/null +++ b/src/basic/utils/formatters.ts @@ -0,0 +1,7 @@ +// TODO: 포맷팅 유틸리티 함수들 +// 구현할 함수: +// - formatPrice(price: number): string - 가격을 한국 원화 형식으로 포맷 +// - formatDate(date: Date): string - 날짜를 YYYY-MM-DD 형식으로 포맷 +// - formatPercentage(rate: number): string - 소수를 퍼센트로 변환 (0.1 → 10%) + +// TODO: 구현 \ No newline at end of file diff --git a/src/basic/utils/hooks/useDebounce.ts b/src/basic/utils/hooks/useDebounce.ts new file mode 100644 index 00000000..53c8a374 --- /dev/null +++ b/src/basic/utils/hooks/useDebounce.ts @@ -0,0 +1,11 @@ +// TODO: 디바운스 Hook +// 힌트: +// 1. 값이 변경되어도 지정된 시간 동안 대기 +// 2. 대기 시간 동안 값이 다시 변경되면 타이머 리셋 +// 3. 최종적으로 안정된 값만 반환 +// +// 사용 예시: 검색어 입력 디바운싱 + +export function useDebounce(value: T, delay: number): T { + // TODO: 구현 +} \ No newline at end of file diff --git a/src/basic/utils/hooks/useLocalStorage.ts b/src/basic/utils/hooks/useLocalStorage.ts new file mode 100644 index 00000000..5dc72c50 --- /dev/null +++ b/src/basic/utils/hooks/useLocalStorage.ts @@ -0,0 +1,15 @@ +// TODO: LocalStorage Hook +// 힌트: +// 1. localStorage와 React state 동기화 +// 2. 초기값 로드 시 에러 처리 +// 3. 저장 시 JSON 직렬화/역직렬화 +// 4. 빈 배열이나 undefined는 삭제 +// +// 반환값: [저장된 값, 값 설정 함수] + +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void] { + // TODO: 구현 +} \ No newline at end of file diff --git a/src/basic/utils/hooks/useValidate.ts b/src/basic/utils/hooks/useValidate.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/basic/utils/validators.ts b/src/basic/utils/validators.ts new file mode 100644 index 00000000..7d2dda44 --- /dev/null +++ b/src/basic/utils/validators.ts @@ -0,0 +1,8 @@ +// TODO: 검증 유틸리티 함수들 +// 구현할 함수: +// - isValidCouponCode(code: string): boolean - 쿠폰 코드 형식 검증 (4-12자 영문 대문자와 숫자) +// - isValidStock(stock: number): boolean - 재고 수량 검증 (0 이상) +// - isValidPrice(price: number): boolean - 가격 검증 (양수) +// - extractNumbers(value: string): string - 문자열에서 숫자만 추출 + +// TODO: 구현 \ No newline at end of file From d292fbadf0a0a380d8ec22aed86c8b737cc046fe Mon Sep 17 00:00:00 2001 From: annkimm Date: Tue, 5 Aug 2025 20:15:57 +0900 Subject: [PATCH 03/16] feat: complete componentization admin, cart page --- src/basic/App.tsx | 84 +- src/basic/components/AdminPage.tsx | 775 ++++--------------- src/basic/components/CartPage.tsx | 404 ++-------- src/basic/components/ui/AdminHeader.tsx | 27 + src/basic/components/ui/Cart.tsx | 235 ++++++ src/basic/components/ui/CartHeader.tsx | 69 ++ src/basic/components/ui/CouponForm.tsx | 99 +++ src/basic/components/ui/CouponList.tsx | 54 ++ src/basic/components/ui/ProductAccordion.tsx | 88 +++ src/basic/components/ui/ProductFom.tsx | 171 ++++ src/basic/components/ui/ProductList.tsx | 9 +- src/basic/hooks/useCart.ts | 23 +- src/basic/hooks/useForm.ts | 253 ++++++ src/basic/utils/formatters.ts | 18 +- src/basic/utils/validators.ts | 7 +- src/types.ts | 30 +- 16 files changed, 1291 insertions(+), 1055 deletions(-) create mode 100644 src/basic/components/ui/AdminHeader.tsx create mode 100644 src/basic/components/ui/Cart.tsx create mode 100644 src/basic/components/ui/CartHeader.tsx create mode 100644 src/basic/components/ui/CouponForm.tsx create mode 100644 src/basic/components/ui/CouponList.tsx create mode 100644 src/basic/components/ui/ProductAccordion.tsx create mode 100644 src/basic/components/ui/ProductFom.tsx create mode 100644 src/basic/hooks/useForm.ts diff --git a/src/basic/App.tsx b/src/basic/App.tsx index 8d311d5b..9dab233e 100644 --- a/src/basic/App.tsx +++ b/src/basic/App.tsx @@ -1,12 +1,10 @@ -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback } from 'react' import { Notification } from './types' import { AdminPage } from './components/AdminPage' import { useProducts } from './hooks/useProducts' import { CartPage } from './components/CartPage' -import { getRemainingStock } from './models/cart' import { useCart } from './hooks/useCart' import { useCoupons } from './hooks/useCoupons' -import { Header } from './components/ui/Header' import { Notifications } from './components/ui/Notifications' const App = () => { @@ -27,15 +25,15 @@ const App = () => { useProducts(addNotification) const { cart, - setCart, + completeOrder, removeFromCart, updateQuantity, selectedCoupon, setSelectedCoupon, addToCart, - applyCoupon, calculateCartTotal, - calculateItemTotal, + getRemainingStock, + handleSelectCoupon, } = useCart(addNotification) const { coupons, addCoupon, deleteCoupon } = useCoupons( addNotification, @@ -43,64 +41,12 @@ const App = () => { setSelectedCoupon, ) const [isAdmin, setIsAdmin] = useState(false) - const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') - const [searchTerm, setSearchTerm] = useState('') - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') - const [totalItemCount, setTotalItemCount] = useState(0) - - useEffect(() => { - const count = cart.reduce((sum, item) => sum + item.quantity, 0) - setTotalItemCount(count) - }, [cart]) - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm) - }, 500) - return () => clearTimeout(timer) - }, [searchTerm]) - - const formatPrice = (price: number, productId?: string): string => { - if (productId) { - const product = products.find((p) => p.id === productId) - if (product && getRemainingStock(product, cart) <= 0) { - return 'SOLD OUT' - } - } - - if (isAdmin) { - return `${price.toLocaleString()}원` - } - - return `₩${price.toLocaleString()}` - } - - const completeOrder = useCallback(() => { - const orderNumber = `ORD-${Date.now()}` - addNotification( - `주문이 완료되었습니다. 주문번호: ${orderNumber}`, - 'success', - ) - setCart([]) - setSelectedCoupon(null) - }, [addNotification, setCart, setSelectedCoupon]) - const totals = calculateCartTotal() - - const headerState = { + const adminProps = { isAdmin, - searchTerm, - cart, - totalItemCount, - setSearchTerm, setIsAdmin, - } - - const adminState = { - activeTab, - setActiveTab, products, - formatPrice, + cart, deleteProduct, addNotification, coupons, @@ -110,21 +56,20 @@ const App = () => { addCoupon, } - const cartState = { + const cartProps = { + isAdmin, + setIsAdmin, products, cart, coupons, - totals, - debouncedSearchTerm, + totals: calculateCartTotal(), selectedCoupon, addToCart, removeFromCart, - formatPrice, - calculateItemTotal, + handleSelectCoupon, updateQuantity, - applyCoupon, - setSelectedCoupon, completeOrder, + getRemainingStock, } return ( @@ -133,10 +78,7 @@ const App = () => { notifications={notifications} setNotifications={setNotifications} /> -
-
- {isAdmin ? : } -
+ {isAdmin ? : } ) } diff --git a/src/basic/components/AdminPage.tsx b/src/basic/components/AdminPage.tsx index a3e13f54..dcd00a94 100644 --- a/src/basic/components/AdminPage.tsx +++ b/src/basic/components/AdminPage.tsx @@ -1,4 +1,3 @@ -// TODO: 관리자 페이지 컴포넌트 // 힌트: // 1. 탭 UI로 상품 관리와 쿠폰 관리 분리 // 2. 상품 추가/수정/삭제 기능 @@ -8,22 +7,22 @@ // 필요한 hooks: // - useProducts: 상품 CRUD // - useCoupons: 쿠폰 CRUD -// -// 하위 컴포넌트: -// - ProductForm: 새 상품 추가 폼 -// - ProductAccordion: 상품 정보 표시 및 수정 -// - CouponForm: 새 쿠폰 추가 폼 -// - CouponList: 쿠폰 목록 표시 -import { useState } from 'react' -import { Coupon } from '../../types' +import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' +import { useCouponForm, useProductForm } from '../hooks/useForm' +import { useState } from 'react' +import { AdminHeader } from './ui/AdminHeader' +import { ProductFom } from './ui/ProductFom' +import { CouponForm } from './ui/CouponForm' +import { CouponList } from './ui/CouponList' +import { ProductAccordion } from './ui/ProductAccordion' export function AdminPage({ - activeTab, - setActiveTab, + isAdmin, + cart, + setIsAdmin, products, - formatPrice, deleteProduct, addNotification, coupons, @@ -32,10 +31,10 @@ export function AdminPage({ addProduct, addCoupon, }: { - activeTab: 'products' | 'coupons' - setActiveTab: React.Dispatch> + isAdmin: boolean + cart: CartItem[] + setIsAdmin: React.Dispatch> products: ProductWithUI[] - formatPrice: (price: number, productId?: string) => string deleteProduct: (productId: string) => void addNotification: ( message: string, @@ -48,646 +47,148 @@ export function AdminPage({ addCoupon: (newCoupon: Coupon) => void }) { // TODO: 구현 - const [showCouponForm, setShowCouponForm] = useState(false) - const [showProductForm, setShowProductForm] = useState(false) - const [editingProduct, setEditingProduct] = useState(null) - const [productForm, setProductForm] = useState({ - name: '', - price: 0, - stock: 0, - description: '', - discounts: [] as Array<{ quantity: number; rate: number }>, - }) + const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') + const { + couponForm, + handleEditCouponForm, + showCouponForm, + setShowCouponForm, + handleCouponSubmit, + handleDiscountValueValidation, + } = useCouponForm(addCoupon, addNotification) + const { + productForm, + startEditProduct, + handleProductSubmit, + showProductForm, + editingProduct, + handleAddOrCloseProductForm, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handlePriceValidation, + handleStockValidation, + } = useProductForm(addProduct, updateProduct, addNotification) - const [couponForm, setCouponForm] = useState({ - name: '', - code: '', - discountType: 'amount' as 'amount' | 'percentage', - discountValue: 0, - }) - - const handleProductSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (editingProduct && editingProduct !== 'new') { - updateProduct(editingProduct, productForm) - setEditingProduct(null) - } else { - addProduct({ - ...productForm, - discounts: productForm.discounts, - }) - } - setProductForm({ - name: '', - price: 0, - stock: 0, - description: '', - discounts: [], - }) - setEditingProduct(null) - setShowProductForm(false) + const productFormProps = { + productForm, + handleProductSubmit, + editingProduct, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handleAddOrCloseProductForm, + handlePriceValidation, + handleStockValidation, } - const handleCouponSubmit = (e: React.FormEvent) => { - e.preventDefault() - addCoupon(couponForm) - setCouponForm({ - name: '', - code: '', - discountType: 'amount', - discountValue: 0, - }) - setShowCouponForm(false) + const productAccordionProps = { + products, + cart, + startEditProduct, + deleteProduct, } - const startEditProduct = (product: ProductWithUI) => { - setEditingProduct(product.id) - setProductForm({ - name: product.name, - price: product.price, - stock: product.stock, - description: product.description || '', - discounts: product.discounts || [], - }) - setShowProductForm(true) + const cuponFormProps = { + couponForm, + handleCouponSubmit, + setShowCouponForm, + handleEditCouponForm, + handleDiscountValueValidation, } return ( <> -
-
-

관리자 대시보드

-

상품과 쿠폰을 관리할 수 있습니다

-
-
- -
- - {activeTab === 'products' ? ( -
-
-
-

상품 목록

- -
-
- -
- - - - - - - - - - - - {(activeTab === 'products' ? products : products).map( - (product) => ( - - - - - - - - ), - )} - -
- 상품명 - - 가격 - - 재고 - - 설명 - - 작업 -
- {product.name} - - {formatPrice(product.price, product.id)} - - 10 - ? 'bg-green-100 text-green-800' - : product.stock > 0 - ? 'bg-yellow-100 text-yellow-800' - : 'bg-red-100 text-red-800' - }`} - > - {product.stock}개 - - - {product.description || '-'} - - - -
-
- {showProductForm && ( -
-
-

- {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} -

-
-
- - - setProductForm({ - ...productForm, - name: e.target.value, - }) - } - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - required - /> -
-
- - - setProductForm({ - ...productForm, - description: e.target.value, - }) - } - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - /> -
-
- - { - const value = e.target.value - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ - ...productForm, - price: value === '' ? 0 : parseInt(value), - }) - } - }} - onBlur={(e) => { - const value = e.target.value - if (value === '') { - setProductForm({ ...productForm, price: 0 }) - } else if (parseInt(value) < 0) { - addNotification('가격은 0보다 커야 합니다', 'error') - setProductForm({ ...productForm, price: 0 }) - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
- - { - const value = e.target.value - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ - ...productForm, - stock: value === '' ? 0 : parseInt(value), - }) - } - }} - onBlur={(e) => { - const value = e.target.value - if (value === '') { - setProductForm({ ...productForm, stock: 0 }) - } else if (parseInt(value) < 0) { - addNotification('재고는 0보다 커야 합니다', 'error') - setProductForm({ ...productForm, stock: 0 }) - } else if (parseInt(value) > 9999) { - addNotification( - '재고는 9999개를 초과할 수 없습니다', - 'error', - ) - setProductForm({ ...productForm, stock: 9999 }) - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
-
- -
- {productForm.discounts.map((discount, index) => ( -
- { - const newDiscounts = [...productForm.discounts] - newDiscounts[index].quantity = - parseInt(e.target.value) || 0 - setProductForm({ - ...productForm, - discounts: newDiscounts, - }) - }} - className="w-20 px-2 py-1 border rounded" - min="1" - placeholder="수량" - /> - 개 이상 구매 시 - { - const newDiscounts = [...productForm.discounts] - newDiscounts[index].rate = - (parseInt(e.target.value) || 0) / 100 - setProductForm({ - ...productForm, - discounts: newDiscounts, - }) - }} - className="w-16 px-2 py-1 border rounded" - min="0" - max="100" - placeholder="%" - /> - % 할인 - -
- ))} - -
-
- -
- - -
-
-
- )} -
- ) : ( -
-
-

쿠폰 관리

-
-
-
- {coupons.map((coupon) => ( -
-
-
-

- {coupon.name} -

-

- {coupon.code} -

-
- - {coupon.discountType === 'amount' - ? `${coupon.discountValue.toLocaleString()}원 할인` - : `${coupon.discountValue}% 할인`} - -
-
- -
-
- ))} + +
+
+
+

+ 관리자 대시보드 +

+

+ 상품과 쿠폰을 관리할 수 있습니다 +

+
+
+ +
-
+ {activeTab === 'products' ? ( +
+
+
+

상품 목록

- {showCouponForm && ( -
-
-

- 새 쿠폰 생성 -

-
-
- - - setCouponForm({ - ...couponForm, - name: e.target.value, - }) - } - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder="신규 가입 쿠폰" - required - /> -
-
- - - setCouponForm({ - ...couponForm, - code: e.target.value.toUpperCase(), - }) - } - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" - placeholder="WELCOME2024" - required - /> -
-
- - -
-
- - { - const value = e.target.value - if (value === '' || /^\d+$/.test(value)) { - setCouponForm({ - ...couponForm, - discountValue: - value === '' ? 0 : parseInt(value), - }) - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value) || 0 - if (couponForm.discountType === 'percentage') { - if (value > 100) { - addNotification( - '할인율은 100%를 초과할 수 없습니다', - 'error', - ) - setCouponForm({ - ...couponForm, - discountValue: 100, - }) - } else if (value < 0) { - setCouponForm({ - ...couponForm, - discountValue: 0, - }) - } - } else { - if (value > 100000) { - addNotification( - '할인 금액은 100,000원을 초과할 수 없습니다', - 'error', - ) - setCouponForm({ - ...couponForm, - discountValue: 100000, - }) - } else if (value < 0) { - setCouponForm({ - ...couponForm, - discountValue: 0, - }) - } - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder={ - couponForm.discountType === 'amount' ? '5000' : '10' - } - required - /> -
-
-
- -
+ ) : ( +
+
+

쿠폰 관리

+
+
+
+ +
+ -
- + + +

새 쿠폰 추가

+ +
- )} -
-
- )} -
+ + {showCouponForm && } + + + )} + + ) } diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx index f31bb6ce..0c4539c1 100644 --- a/src/basic/components/CartPage.tsx +++ b/src/basic/components/CartPage.tsx @@ -16,26 +16,29 @@ // - ProductList: 상품 목록 표시 // - Cart: 장바구니 표시 및 결제 +import { useEffect, useState } from 'react' import { CartItem, Coupon } from '../../types' -import { getRemainingStock } from '../models/cart' import { ProductWithUI } from '../types' +import { CartHeader } from './ui/CartHeader' +import { ProductList } from './ui/ProductList' +import { Cart } from './ui/Cart' export function CartPage({ + isAdmin, + setIsAdmin, products, cart, coupons, totals, - debouncedSearchTerm, selectedCoupon, addToCart, removeFromCart, - formatPrice, - calculateItemTotal, updateQuantity, - applyCoupon, - setSelectedCoupon, completeOrder, + handleSelectCoupon, }: { + isAdmin: boolean + setIsAdmin: React.Dispatch> products: ProductWithUI[] cart: CartItem[] coupons: Coupon[] @@ -43,22 +46,36 @@ export function CartPage({ totalBeforeDiscount: number totalAfterDiscount: number } - debouncedSearchTerm: string selectedCoupon: Coupon | null addToCart: (product: ProductWithUI) => void - formatPrice: (price: number, productId?: string) => string removeFromCart: (productId: string) => void - calculateItemTotal: (item: CartItem, cart: CartItem[]) => number updateQuantity: ( productId: string, newQuantity: number, products: ProductWithUI[], ) => void - applyCoupon: (coupon: Coupon) => void - setSelectedCoupon: React.Dispatch> completeOrder: () => void + handleSelectCoupon: ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => void }) { // TODO: 구현 + const [searchTerm, setSearchTerm] = useState('') + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') + const [totalItemCount, setTotalItemCount] = useState(0) + + useEffect(() => { + const count = cart.reduce((sum, item) => sum + item.quantity, 0) + setTotalItemCount(count) + }, [cart]) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm) + }, 500) + return () => clearTimeout(timer) + }, [searchTerm]) const filteredProducts = debouncedSearchTerm ? products.filter( @@ -73,335 +90,62 @@ export function CartPage({ ) : products - return ( - <> -
-
- {/* 상품 목록 */} -
-
-

- 전체 상품 -

-
- 총 {products.length}개 상품 -
-
- {filteredProducts.length === 0 ? ( -
-

- "{debouncedSearchTerm}"에 대한 검색 결과가 없습니다. -

-
- ) : ( -
- {filteredProducts.map((product) => { - const remainingStock = getRemainingStock(product, cart) - - return ( -
- {/* 상품 이미지 영역 (placeholder) */} -
-
- - - -
- {product.isRecommended && ( - - BEST - - )} - {product.discounts.length > 0 && ( - - ~ - {Math.max(...product.discounts.map((d) => d.rate)) * - 100} - % - - )} -
- - {/* 상품 정보 */} -
-

- {product.name} -

- {product.description && ( -

- {product.description} -

- )} - - {/* 가격 정보 */} -
-

- {formatPrice(product.price, product.id)} -

- {product.discounts.length > 0 && ( -

- {product.discounts[0].quantity}개 이상 구매시 할인{' '} - {product.discounts[0].rate * 100}% -

- )} -
+ const headerState = { + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, + } - {/* 재고 상태 */} -
- {remainingStock <= 5 && remainingStock > 0 && ( -

- 품절임박! {remainingStock}개 남음 -

- )} - {remainingStock > 5 && ( -

- 재고 {remainingStock}개 -

- )} -
+ const cartProps = { + cart, + coupons, + products, + selectedCoupon, + removeFromCart, + updateQuantity, + totals, + completeOrder, + handleSelectCoupon, + } - {/* 장바구니 버튼 */} - -
-
- ) - })} + return ( + <> + +
+
+
+ {/* 상품 목록 */} +
+
+

+ 전체 상품 +

+
+ 총 {products.length}개 상품 +
- )} -
-
- -
-
-
-

- - - - 장바구니 -

- {cart.length === 0 ? ( -
- - - -

- 장바구니가 비어있습니다 + {filteredProducts.length === 0 ? ( +

+

+ "{debouncedSearchTerm}"에 대한 검색 결과가 없습니다.

) : ( -
- {cart.map((item) => { - const itemTotal = calculateItemTotal(item, cart) - const originalPrice = item.product.price * item.quantity - const hasDiscount = itemTotal < originalPrice - const discountRate = hasDiscount - ? Math.round((1 - itemTotal / originalPrice) * 100) - : 0 - - return ( -
-
-

- {item.product.name} -

- -
-
-
- - - {item.quantity} - - -
-
- {hasDiscount && ( - - -{discountRate}% - - )} -

- {Math.round(itemTotal).toLocaleString()}원 -

-
-
-
- ) - })} -
+ )}
- - {cart.length > 0 && ( - <> -
-
-

- 쿠폰 할인 -

- -
- {coupons.length > 0 && ( - - )} -
- -
-

결제 정보

-
-
- 상품 금액 - - {totals.totalBeforeDiscount.toLocaleString()}원 - -
- {totals.totalBeforeDiscount - totals.totalAfterDiscount > - 0 && ( -
- 할인 금액 - - - - {( - totals.totalBeforeDiscount - - totals.totalAfterDiscount - ).toLocaleString()} - 원 - -
- )} -
- 결제 예정 금액 - - {totals.totalAfterDiscount.toLocaleString()}원 - -
-
- - - -
-

* 실제 결제는 이루어지지 않습니다

-
-
- - )}
+ +
-
+
) } diff --git a/src/basic/components/ui/AdminHeader.tsx b/src/basic/components/ui/AdminHeader.tsx new file mode 100644 index 00000000..c1535b1b --- /dev/null +++ b/src/basic/components/ui/AdminHeader.tsx @@ -0,0 +1,27 @@ +export const AdminHeader = ({ + isAdmin, + setIsAdmin, +}: { + isAdmin: boolean + setIsAdmin: React.Dispatch> +}) => { + return ( +
+
+
+
+

SHOP

+
+ +
+
+
+ ) +} diff --git a/src/basic/components/ui/Cart.tsx b/src/basic/components/ui/Cart.tsx new file mode 100644 index 00000000..9a352ad1 --- /dev/null +++ b/src/basic/components/ui/Cart.tsx @@ -0,0 +1,235 @@ +import { CartItem, Coupon } from '../../../types' +import { calculateItemTotal } from '../../models/cart' +import { ProductWithUI } from '../../types' + +export const Cart = ({ + cart, + coupons, + products, + selectedCoupon, + removeFromCart, + updateQuantity, + totals, + completeOrder, + handleSelectCoupon, +}: { + cart: CartItem[] + coupons: Coupon[] + products: ProductWithUI[] + selectedCoupon: Coupon | null + totals: { + totalBeforeDiscount: number + totalAfterDiscount: number + } + removeFromCart: (productId: string) => void + updateQuantity: ( + productId: string, + newQuantity: number, + products: ProductWithUI[], + ) => void + completeOrder: () => void + handleSelectCoupon: ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => void +}) => { + return ( +
+
+
+

+ + + + 장바구니 +

+ {cart.length === 0 ? ( +
+ + + +

장바구니가 비어있습니다

+
+ ) : ( +
+ {cart.map((item) => { + const itemTotal = calculateItemTotal(item, cart) + const originalPrice = item.product.price * item.quantity + const hasDiscount = itemTotal < originalPrice + const discountRate = hasDiscount + ? Math.round((1 - itemTotal / originalPrice) * 100) + : 0 + + return ( +
+
+

+ {item.product.name} +

+ +
+
+
+ + + {item.quantity} + + +
+
+ {hasDiscount && ( + + -{discountRate}% + + )} +

+ {Math.round(itemTotal).toLocaleString()}원 +

+
+
+
+ ) + })} +
+ )} +
+ + {cart.length > 0 && ( + <> +
+
+

+ 쿠폰 할인 +

+ +
+ {coupons.length > 0 && ( + + )} +
+ +
+

결제 정보

+
+
+ 상품 금액 + + {totals.totalBeforeDiscount.toLocaleString()}원 + +
+ {totals.totalBeforeDiscount - totals.totalAfterDiscount > 0 && ( +
+ 할인 금액 + + - + {( + totals.totalBeforeDiscount - totals.totalAfterDiscount + ).toLocaleString()} + 원 + +
+ )} +
+ 결제 예정 금액 + + {totals.totalAfterDiscount.toLocaleString()}원 + +
+
+ + + +
+

* 실제 결제는 이루어지지 않습니다

+
+
+ + )} +
+
+ ) +} diff --git a/src/basic/components/ui/CartHeader.tsx b/src/basic/components/ui/CartHeader.tsx new file mode 100644 index 00000000..f0e47a30 --- /dev/null +++ b/src/basic/components/ui/CartHeader.tsx @@ -0,0 +1,69 @@ +import { CartItem } from '../../../types' + +export const CartHeader = ({ + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, +}: { + isAdmin: boolean + searchTerm: string + cart: CartItem[] + totalItemCount: number + setSearchTerm: React.Dispatch> + setIsAdmin: React.Dispatch> +}) => { + return ( + <> +
+
+
+
+

SHOP

+ {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} +
+ setSearchTerm(e.target.value)} + placeholder="상품 검색..." + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+ +
+
+
+ + ) +} diff --git a/src/basic/components/ui/CouponForm.tsx b/src/basic/components/ui/CouponForm.tsx new file mode 100644 index 00000000..60dd818c --- /dev/null +++ b/src/basic/components/ui/CouponForm.tsx @@ -0,0 +1,99 @@ +import { FocusEvent } from 'react' +import { Coupon } from '../../../types' + +export function CouponForm({ + couponForm, + handleCouponSubmit, + setShowCouponForm, + handleEditCouponForm, + handleDiscountValueValidation, +}: { + couponForm: Coupon + setShowCouponForm: React.Dispatch> + handleCouponSubmit: (e: React.FormEvent) => void + handleEditCouponForm: ( + e: React.ChangeEvent, + key: string, + ) => void + handleDiscountValueValidation: (e: FocusEvent) => void +}) { + return ( +
+
+

새 쿠폰 생성

+
+
+ + handleEditCouponForm(e, 'name')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder="신규 가입 쿠폰" + required + /> +
+
+ + handleEditCouponForm(e, 'code')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" + placeholder="WELCOME2024" + required + /> +
+
+ + +
+
+ + handleEditCouponForm(e, 'discountValue')} + onBlur={handleDiscountValueValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder={couponForm.discountType === 'amount' ? '5000' : '10'} + required + /> +
+
+
+ + +
+
+
+ ) +} diff --git a/src/basic/components/ui/CouponList.tsx b/src/basic/components/ui/CouponList.tsx new file mode 100644 index 00000000..168eac2e --- /dev/null +++ b/src/basic/components/ui/CouponList.tsx @@ -0,0 +1,54 @@ +import { Coupon } from '../../../types' + +export const CouponList = ({ + coupons, + deleteCoupon, +}: { + coupons: Coupon[] + deleteCoupon: (couponCode: string) => void +}) => { + return ( + <> + {coupons.map((coupon) => ( +
+
+
+

{coupon.name}

+

+ {coupon.code} +

+
+ + {coupon.discountType === 'amount' + ? `${coupon.discountValue.toLocaleString()}원 할인` + : `${coupon.discountValue}% 할인`} + +
+
+ +
+
+ ))} + + ) +} diff --git a/src/basic/components/ui/ProductAccordion.tsx b/src/basic/components/ui/ProductAccordion.tsx new file mode 100644 index 00000000..b16a22db --- /dev/null +++ b/src/basic/components/ui/ProductAccordion.tsx @@ -0,0 +1,88 @@ +import { CartItem } from '../../../types' +import { getRemainingStock } from '../../models/cart' +import { ProductWithUI } from '../../types' +import { formatPrice } from '../../utils/formatters' + +export const ProductAccordion = ({ + products, + cart, + startEditProduct, + deleteProduct, +}: { + products: ProductWithUI[] + cart: CartItem[] + startEditProduct: (product: ProductWithUI) => void + deleteProduct: (productId: string) => void +}) => { + return ( +
+ + + + + + + + + + + + {products.map((product) => ( + + + + + + + + ))} + +
+ 상품명 + + 가격 + + 재고 + + 설명 + + 작업 +
+ {product.name} + + {formatPrice( + product.price, + true, + getRemainingStock(product, cart) <= 0, + )} + + 10 + ? 'bg-green-100 text-green-800' + : product.stock > 0 + ? 'bg-yellow-100 text-yellow-800' + : 'bg-red-100 text-red-800' + }`} + > + {product.stock}개 + + + {product.description || '-'} + + + +
+
+ ) +} diff --git a/src/basic/components/ui/ProductFom.tsx b/src/basic/components/ui/ProductFom.tsx new file mode 100644 index 00000000..4855303e --- /dev/null +++ b/src/basic/components/ui/ProductFom.tsx @@ -0,0 +1,171 @@ +import React, { FocusEvent } from 'react' +import { ProductForm } from '../../../types' + +export function ProductFom({ + productForm, + handleProductSubmit, + editingProduct, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handleAddOrCloseProductForm, + handlePriceValidation, + handleStockValidation, +}: { + productForm: ProductForm + handleProductSubmit: (e: React.FormEvent) => void + editingProduct: string | null + handleAddDiscount: () => void + handleDeleteDiscount: (index: number) => void + handleEditProuctForm: ( + e: React.ChangeEvent, + key: string, + index?: number, + ) => void + handleAddOrCloseProductForm: ( + type: string | null, + isShowProductForm: boolean, + ) => void + handlePriceValidation: (e: FocusEvent) => void + handleStockValidation: (e: FocusEvent) => void +}) { + return ( + <> +
+
+

+ {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} +

+
+
+ + handleEditProuctForm(e, 'name')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + required + /> +
+
+ + handleEditProuctForm(e, 'description')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + /> +
+
+ + handleEditProuctForm(e, 'price')} + onBlur={handlePriceValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+ + handleEditProuctForm(e, 'stock')} + onBlur={handleStockValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+
+ +
+ {productForm.discounts.map((discount, index) => ( +
+ handleEditProuctForm(e, 'quantity', index)} + className="w-20 px-2 py-1 border rounded" + min="1" + placeholder="수량" + /> + 개 이상 구매 시 + handleEditProuctForm(e, 'rate', index)} + className="w-16 px-2 py-1 border rounded" + min="0" + max="100" + placeholder="%" + /> + % 할인 + +
+ ))} + +
+
+ +
+ + +
+
+
+ + ) +} diff --git a/src/basic/components/ui/ProductList.tsx b/src/basic/components/ui/ProductList.tsx index 56ae8678..fcd549b1 100644 --- a/src/basic/components/ui/ProductList.tsx +++ b/src/basic/components/ui/ProductList.tsx @@ -1,13 +1,16 @@ import { CartItem } from '../../../types' import { getRemainingStock } from '../../models/cart' import { ProductWithUI } from '../../types' +import { formatPrice } from '../../utils/formatters' export function ProductList({ filteredProducts, cart, + addToCart, }: { filteredProducts: ProductWithUI[] cart: CartItem[] + addToCart: (product: ProductWithUI) => void }) { return (
@@ -60,7 +63,11 @@ export function ProductList({ {/* 가격 정보 */}

- {formatPrice(product.price, product.id)} + {formatPrice( + product.price, + false, + getRemainingStock(product, cart) <= 0, + )}

{product.discounts.length > 0 && (

diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 08fddf19..225b8909 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -177,6 +177,25 @@ export function useCart( [addNotification, calculateCartTotal, setSelectedCoupon], ) + const completeOrder = useCallback(() => { + const orderNumber = `ORD-${Date.now()}` + addNotification( + `주문이 완료되었습니다. 주문번호: ${orderNumber}`, + 'success', + ) + setCart([]) + setSelectedCoupon(null) + }, [addNotification, setCart, setSelectedCoupon]) + + const handleSelectCoupon = ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => { + const coupon = coupons.find((c) => c.code === e.target.value) + if (coupon) applyCoupon(coupon) + else setSelectedCoupon(null) + } + useEffect(() => { if (cart.length > 0) { localStorage.setItem('cart', JSON.stringify(cart)) @@ -187,7 +206,6 @@ export function useCart( return { cart, - setCart, addToCart, removeFromCart, updateQuantity, @@ -196,5 +214,8 @@ export function useCart( applyCoupon, calculateCartTotal, calculateItemTotal, + completeOrder, + getRemainingStock, + handleSelectCoupon, } } diff --git a/src/basic/hooks/useForm.ts b/src/basic/hooks/useForm.ts new file mode 100644 index 00000000..684361d0 --- /dev/null +++ b/src/basic/hooks/useForm.ts @@ -0,0 +1,253 @@ +import { useState, FocusEvent } from 'react' +import { ProductWithUI } from '../types' +import { Coupon, Discount, ProductForm } from '../../types' +import { isValidStrNumber } from '../utils/validators' + +export function useCouponForm( + addCoupon: (newCoupon: Coupon) => void, + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + const [showCouponForm, setShowCouponForm] = useState(false) + const [couponForm, setCouponForm] = useState({ + name: '', + code: '', + discountType: 'amount' as 'amount' | 'percentage', + discountValue: 0, + }) + + const handleEditCouponForm = ( + e: React.ChangeEvent, + key: string, + ) => { + const { value } = e.target + if (key === 'discountValue') { + if (value === '' || isValidStrNumber(value)) { + setCouponForm((prev) => ({ + ...prev, + [key]: value === '' ? 0 : parseInt(value), + })) + } + } else { + setCouponForm((prev) => ({ + ...prev, + [key]: key === 'code' ? value.toUpperCase() : value, + })) + } + } + + const handleCouponSubmit = (e: React.FormEvent) => { + e.preventDefault() + addCoupon(couponForm) + setCouponForm({ + name: '', + code: '', + discountType: 'amount', + discountValue: 0, + }) + setShowCouponForm(false) + } + + const handleDiscountValueValidation = (e: FocusEvent) => { + const value = parseInt(e.target.value) || 0 + + if (couponForm.discountType === 'percentage') { + if (value > 100) { + addNotification('할인율은 100%를 초과할 수 없습니다', 'error') + setCouponForm({ + ...couponForm, + discountValue: 100, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } else { + if (value > 100000) { + addNotification('할인 금액은 100,000원을 초과할 수 없습니다', 'error') + setCouponForm({ + ...couponForm, + discountValue: 100000, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } + } + + return { + couponForm, + setCouponForm, + showCouponForm, + setShowCouponForm, + handleCouponSubmit, + handleEditCouponForm, + handleDiscountValueValidation, + } +} + +export function useProductForm( + addProduct: (newProduct: Omit) => void, + updateProduct: (productId: string, updates: Partial) => void, + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + const [showProductForm, setShowProductForm] = useState(false) + const [editingProduct, setEditingProduct] = useState(null) + const [productForm, setProductForm] = useState({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [] as Array<{ quantity: number; rate: number }>, + }) + + const startEditProduct = (product: ProductWithUI) => { + setEditingProduct(product.id) + setProductForm({ + name: product.name, + price: product.price, + stock: product.stock, + description: product.description || '', + discounts: product.discounts || [], + }) + setShowProductForm(true) + } + + const handleAddOrCloseProductForm = ( + type: string | null, + isShowProductForm: boolean, + ) => { + setEditingProduct(type) + setProductForm({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + setShowProductForm(isShowProductForm) + } + + const handleProductSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (editingProduct && editingProduct !== 'new') { + updateProduct(editingProduct, productForm) + setEditingProduct(null) + } else { + addProduct({ + ...productForm, + discounts: productForm.discounts, + }) + } + setProductForm({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + setEditingProduct(null) + setShowProductForm(false) + } + + const handleEditProuctForm = ( + e: React.ChangeEvent, + key: string, + index?: number, + ) => { + const { value } = e.target + + if (['name', 'description'].includes(key)) { + setProductForm((prev) => ({ + ...prev, + [key]: value, + })) + } + + if ( + ['price', 'stock'].includes(key) && + (value === '' || isValidStrNumber(value)) + ) { + setProductForm({ + ...productForm, + [key]: value === '' ? 0 : parseInt(value), + }) + } + + if (index && ['quantity', 'rate'].includes(key)) { + const newDiscounts = [...productForm.discounts] + const value = parseInt(e.target.value) || 0 + newDiscounts[index][key as keyof Discount] = + value / (key === 'rate' ? 100 : 1) + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + } + } + + const handleDeleteDiscount = (index: number) => { + const newDiscounts = productForm.discounts.filter((_, i) => i !== index) + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + } + + const handleAddDiscount = () => { + setProductForm({ + ...productForm, + discounts: [...productForm.discounts, { quantity: 10, rate: 0.1 }], + }) + } + + const handlePriceValidation = (e: FocusEvent) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, price: 0 }) + } else if (parseInt(value) < 0) { + addNotification('가격은 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, price: 0 }) + } + } + + const handleStockValidation = (e: FocusEvent) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) < 0) { + addNotification('재고는 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) > 9999) { + addNotification('재고는 9999개를 초과할 수 없습니다', 'error') + setProductForm({ ...productForm, stock: 9999 }) + } + } + + return { + productForm, + editingProduct, + showProductForm, + setEditingProduct, + setProductForm, + setShowProductForm, + startEditProduct, + handleProductSubmit, + handleAddOrCloseProductForm, + handleDeleteDiscount, + handleAddDiscount, + handleEditProuctForm, + handlePriceValidation, + handleStockValidation, + } +} diff --git a/src/basic/utils/formatters.ts b/src/basic/utils/formatters.ts index ff157f5c..f3e19f5a 100644 --- a/src/basic/utils/formatters.ts +++ b/src/basic/utils/formatters.ts @@ -4,4 +4,20 @@ // - formatDate(date: Date): string - 날짜를 YYYY-MM-DD 형식으로 포맷 // - formatPercentage(rate: number): string - 소수를 퍼센트로 변환 (0.1 → 10%) -// TODO: 구현 \ No newline at end of file +// TODO: 구현 + +export const formatPrice = ( + price: number, + isAdmin: boolean, + isSoldOut: boolean, +): string => { + if (isSoldOut) { + return 'SOLD OUT' + } + + if (isAdmin) { + return `${price.toLocaleString()}원` + } + + return `₩${price.toLocaleString()}` +} diff --git a/src/basic/utils/validators.ts b/src/basic/utils/validators.ts index 7d2dda44..8b88abea 100644 --- a/src/basic/utils/validators.ts +++ b/src/basic/utils/validators.ts @@ -5,4 +5,9 @@ // - isValidPrice(price: number): boolean - 가격 검증 (양수) // - extractNumbers(value: string): string - 문자열에서 숫자만 추출 -// TODO: 구현 \ No newline at end of file +// TODO: 구현 + +// 문자열 숫자 확인 +export function isValidStrNumber(value: string) { + return /^\d+$/.test(value) +} diff --git a/src/types.ts b/src/types.ts index 5489e296..7fed750a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,24 +1,28 @@ export interface Product { - id: string; - name: string; - price: number; - stock: number; - discounts: Discount[]; + id: string + name: string + price: number + stock: number + discounts: Discount[] +} + +export interface ProductForm extends Omit { + description: string } export interface Discount { - quantity: number; - rate: number; + quantity: number + rate: number } export interface CartItem { - product: Product; - quantity: number; + product: Product + quantity: number } export interface Coupon { - name: string; - code: string; - discountType: 'amount' | 'percentage'; - discountValue: number; + name: string + code: string + discountType: 'amount' | 'percentage' + discountValue: number } From d544c4b5c2eedf2755ed1db966fceb1dd1ea3ebe Mon Sep 17 00:00:00 2001 From: annkimm Date: Tue, 5 Aug 2025 21:53:56 +0900 Subject: [PATCH 04/16] feat: add useDebouce and useLocalStorage --- src/basic/App.tsx | 10 ++++- src/basic/components/CartPage.tsx | 30 +++------------ src/basic/hooks/useCart.ts | 25 ++---------- src/basic/hooks/useCoupons.ts | 24 ++++-------- src/basic/hooks/useForm.ts | 2 +- src/basic/hooks/useProducts.ts | 40 ++++++++++--------- src/basic/utils/hooks/useDebounce.ts | 15 +++++++- src/basic/utils/hooks/useLocalStorage.ts | 49 +++++++++++++++++++++++- 8 files changed, 108 insertions(+), 87 deletions(-) diff --git a/src/basic/App.tsx b/src/basic/App.tsx index 9dab233e..2c7dbe25 100644 --- a/src/basic/App.tsx +++ b/src/basic/App.tsx @@ -21,8 +21,13 @@ const App = () => { [], ) - const { products, addProduct, updateProduct, deleteProduct } = - useProducts(addNotification) + const { + products, + addProduct, + updateProduct, + deleteProduct, + getFilteredProducts, + } = useProducts(addNotification) const { cart, completeOrder, @@ -70,6 +75,7 @@ const App = () => { updateQuantity, completeOrder, getRemainingStock, + getFilteredProducts, } return ( diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx index 0c4539c1..164b37a6 100644 --- a/src/basic/components/CartPage.tsx +++ b/src/basic/components/CartPage.tsx @@ -10,11 +10,6 @@ // - useCart: 장바구니 상태 관리 // - useCoupons: 쿠폰 목록 관리 // - useDebounce: 검색어 디바운싱 -// -// 하위 컴포넌트: -// - SearchBar: 검색 입력 -// - ProductList: 상품 목록 표시 -// - Cart: 장바구니 표시 및 결제 import { useEffect, useState } from 'react' import { CartItem, Coupon } from '../../types' @@ -22,6 +17,7 @@ import { ProductWithUI } from '../types' import { CartHeader } from './ui/CartHeader' import { ProductList } from './ui/ProductList' import { Cart } from './ui/Cart' +import { useDebounce } from '../utils/hooks/useDebounce' export function CartPage({ isAdmin, @@ -36,6 +32,7 @@ export function CartPage({ updateQuantity, completeOrder, handleSelectCoupon, + getFilteredProducts, }: { isAdmin: boolean setIsAdmin: React.Dispatch> @@ -59,36 +56,19 @@ export function CartPage({ e: React.ChangeEvent, coupons: Coupon[], ) => void + getFilteredProducts: (searchTerm: string) => ProductWithUI[] }) { // TODO: 구현 const [searchTerm, setSearchTerm] = useState('') - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') const [totalItemCount, setTotalItemCount] = useState(0) + const debouncedSearchTerm = useDebounce(searchTerm, 500) useEffect(() => { const count = cart.reduce((sum, item) => sum + item.quantity, 0) setTotalItemCount(count) }, [cart]) - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm) - }, 500) - return () => clearTimeout(timer) - }, [searchTerm]) - - const filteredProducts = debouncedSearchTerm - ? products.filter( - (product) => - product.name - .toLowerCase() - .includes(debouncedSearchTerm.toLowerCase()) || - (product.description && - product.description - .toLowerCase() - .includes(debouncedSearchTerm.toLowerCase())), - ) - : products + const filteredProducts = getFilteredProducts(debouncedSearchTerm) const headerState = { isAdmin, diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 225b8909..33e78749 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -24,10 +24,11 @@ // - getRemainingStock: 재고 확인 함수 // - clearCart: 장바구니 비우기 함수 -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' import { getRemainingStock, calculateItemTotal } from '../models/cart' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' export function useCart( addNotification: ( @@ -38,17 +39,7 @@ export function useCart( // TODO: 구현 const [selectedCoupon, setSelectedCoupon] = useState(null) - const [cart, setCart] = useState(() => { - const saved = localStorage.getItem('cart') - if (saved) { - try { - return JSON.parse(saved) - } catch { - return [] - } - } - return [] - }) + const [cart, setCart] = useLocalStorage('cart', []) const addToCart = useCallback( (product: ProductWithUI) => { @@ -88,7 +79,7 @@ export function useCart( addNotification('장바구니에 담았습니다', 'success') }, - [addNotification, cart], + [addNotification, cart, setCart], ) const removeFromCart = useCallback( @@ -196,14 +187,6 @@ export function useCart( else setSelectedCoupon(null) } - useEffect(() => { - if (cart.length > 0) { - localStorage.setItem('cart', JSON.stringify(cart)) - } else { - localStorage.removeItem('cart') - } - }, [cart]) - return { cart, addToCart, diff --git a/src/basic/hooks/useCoupons.ts b/src/basic/hooks/useCoupons.ts index 632b9383..be549be4 100644 --- a/src/basic/hooks/useCoupons.ts +++ b/src/basic/hooks/useCoupons.ts @@ -8,9 +8,10 @@ // - addCoupon: 새 쿠폰 추가 // - removeCoupon: 쿠폰 삭제 -import { useCallback, useEffect, useState } from 'react' +import { useCallback } from 'react' import { initialCoupons } from '../constants' import { Coupon } from '../../types' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' export function useCoupons( addNotification: ( @@ -22,21 +23,10 @@ export function useCoupons( ) { // TODO: 구현 - const [coupons, setCoupons] = useState(() => { - const saved = localStorage.getItem('coupons') - if (saved) { - try { - return JSON.parse(saved) - } catch { - return initialCoupons - } - } - return initialCoupons - }) - - useEffect(() => { - localStorage.setItem('coupons', JSON.stringify(coupons)) - }, [coupons]) + const [coupons, setCoupons] = useLocalStorage( + 'coupons', + initialCoupons, + ) const addCoupon = useCallback( (newCoupon: Coupon) => { @@ -48,7 +38,7 @@ export function useCoupons( setCoupons((prev) => [...prev, newCoupon]) addNotification('쿠폰이 추가되었습니다.', 'success') }, - [coupons, addNotification], + [coupons, setCoupons, addNotification], ) const deleteCoupon = useCallback( diff --git a/src/basic/hooks/useForm.ts b/src/basic/hooks/useForm.ts index 684361d0..4f338a52 100644 --- a/src/basic/hooks/useForm.ts +++ b/src/basic/hooks/useForm.ts @@ -108,7 +108,7 @@ export function useProductForm( price: 0, stock: 0, description: '', - discounts: [] as Array<{ quantity: number; rate: number }>, + discounts: [], }) const startEditProduct = (product: ProductWithUI) => { diff --git a/src/basic/hooks/useProducts.ts b/src/basic/hooks/useProducts.ts index b8e4ba66..e6c95e39 100644 --- a/src/basic/hooks/useProducts.ts +++ b/src/basic/hooks/useProducts.ts @@ -13,9 +13,10 @@ // - addProductDiscount: 할인 규칙 추가 // - removeProductDiscount: 할인 규칙 삭제 -import { useCallback, useEffect, useState } from 'react' +import { useCallback } from 'react' import { initialProducts } from '../constants' import { ProductWithUI } from '../types' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' export function useProducts( addNotification: ( @@ -25,21 +26,10 @@ export function useProducts( ) { // TODO: 구현 - const [products, setProducts] = useState(() => { - const saved = localStorage.getItem('products') - if (saved) { - try { - return JSON.parse(saved) - } catch { - return initialProducts - } - } - return initialProducts - }) - - useEffect(() => { - localStorage.setItem('products', JSON.stringify(products)) - }, [products]) + const [products, setProducts] = useLocalStorage( + 'products', + initialProducts, + ) const addProduct = useCallback( (newProduct: Omit) => { @@ -50,7 +40,7 @@ export function useProducts( setProducts((prev) => [...prev, product]) addNotification('상품이 추가되었습니다.', 'success') }, - [addNotification], + [addNotification, setProducts], ) const updateProduct = useCallback( @@ -62,7 +52,7 @@ export function useProducts( ) addNotification('상품이 수정되었습니다.', 'success') }, - [addNotification], + [addNotification, setProducts], ) const deleteProduct = useCallback( @@ -73,10 +63,24 @@ export function useProducts( [addNotification, setProducts], ) + const getFilteredProducts = useCallback( + (searchTerm: string) => { + if (!searchTerm) return products + + return products.filter( + (product) => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) || + product.description?.toLowerCase().includes(searchTerm.toLowerCase()), + ) + }, + [products], + ) + return { products, addProduct, updateProduct, deleteProduct, + getFilteredProducts, } } diff --git a/src/basic/utils/hooks/useDebounce.ts b/src/basic/utils/hooks/useDebounce.ts index 53c8a374..c3bcdea1 100644 --- a/src/basic/utils/hooks/useDebounce.ts +++ b/src/basic/utils/hooks/useDebounce.ts @@ -6,6 +6,19 @@ // // 사용 예시: 검색어 입력 디바운싱 +import { useEffect, useState } from 'react' + export function useDebounce(value: T, delay: number): T { // TODO: 구현 -} \ No newline at end of file + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(timer) + }, [value, delay]) + + return debouncedValue +} diff --git a/src/basic/utils/hooks/useLocalStorage.ts b/src/basic/utils/hooks/useLocalStorage.ts index 5dc72c50..1d80db6c 100644 --- a/src/basic/utils/hooks/useLocalStorage.ts +++ b/src/basic/utils/hooks/useLocalStorage.ts @@ -7,9 +7,54 @@ // // 반환값: [저장된 값, 값 설정 함수] +import { useState } from 'react' + export function useLocalStorage( key: string, - initialValue: T + initialValue: T, ): [T, (value: T | ((val: T) => T)) => void] { // TODO: 구현 -} \ No newline at end of file + const [localValue, setLocalValue] = useState(() => { + const saved = localStorage.getItem(key) + if (saved) { + try { + return JSON.parse(saved) + } catch { + return initialValue + } + } + return initialValue + }) + + const saveLocalStorage = (val: T | ((val: T) => T)) => { + // 타입 가드로 확실하게 구분 + const isUpdater = (val: T | ((prev: T) => T)): val is (prev: T) => T => { + return typeof val === 'function' + } + if (isUpdater(val)) { + // 함수형 업데이트: 함수를 실행한 결과를 localStorage에 저장 + setLocalValue((prev) => { + const result = val(prev) + if (result === null || result === undefined) { + localStorage.removeItem(key) + return initialValue + } else { + localStorage.setItem(key, JSON.stringify(result)) + return result + } + }) + } else if ( + [null, undefined].includes(val as never) || + (Array.isArray(val) && val.length === 0) + ) { + setLocalValue(initialValue) + localStorage.removeItem(key) + } else { + // 직접 값: 값을 그대로 localStorage에 저장 + setLocalValue(val) + localStorage.setItem(key, JSON.stringify(val)) + } + } + + return [localValue, saveLocalStorage] +} From 7de3c6200ff8eda81beeccb68ad96de574ef76a1 Mon Sep 17 00:00:00 2001 From: annkimm Date: Tue, 5 Aug 2025 23:28:20 +0900 Subject: [PATCH 05/16] refactor: remove model function from component --- src/basic/App.tsx | 3 +++ src/basic/components/AdminPage.tsx | 3 +++ src/basic/components/CartPage.tsx | 7 +++++- src/basic/components/ui/Cart.tsx | 5 ++-- src/basic/components/ui/ProductAccordion.tsx | 6 ++--- src/basic/components/ui/ProductList.tsx | 10 ++++---- src/basic/hooks/useCart.ts | 25 ++++++++++++++++---- src/basic/utils/hooks/useValidate.ts | 0 8 files changed, 43 insertions(+), 16 deletions(-) delete mode 100644 src/basic/utils/hooks/useValidate.ts diff --git a/src/basic/App.tsx b/src/basic/App.tsx index 2c7dbe25..dfe5a6b4 100644 --- a/src/basic/App.tsx +++ b/src/basic/App.tsx @@ -39,6 +39,7 @@ const App = () => { calculateCartTotal, getRemainingStock, handleSelectCoupon, + calculateTotal, } = useCart(addNotification) const { coupons, addCoupon, deleteCoupon } = useCoupons( addNotification, @@ -59,6 +60,7 @@ const App = () => { updateProduct, addProduct, addCoupon, + getRemainingStock, } const cartProps = { @@ -76,6 +78,7 @@ const App = () => { completeOrder, getRemainingStock, getFilteredProducts, + calculateTotal, } return ( diff --git a/src/basic/components/AdminPage.tsx b/src/basic/components/AdminPage.tsx index dcd00a94..bed0a2ae 100644 --- a/src/basic/components/AdminPage.tsx +++ b/src/basic/components/AdminPage.tsx @@ -30,6 +30,7 @@ export function AdminPage({ updateProduct, addProduct, addCoupon, + getRemainingStock, }: { isAdmin: boolean cart: CartItem[] @@ -45,6 +46,7 @@ export function AdminPage({ updateProduct: (productId: string, updates: Partial) => void addProduct: (newProduct: Omit) => void addCoupon: (newCoupon: Coupon) => void + getRemainingStock: (product: ProductWithUI) => number }) { // TODO: 구현 const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') @@ -87,6 +89,7 @@ export function AdminPage({ cart, startEditProduct, deleteProduct, + getRemainingStock, } const cuponFormProps = { diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx index 164b37a6..b6e98273 100644 --- a/src/basic/components/CartPage.tsx +++ b/src/basic/components/CartPage.tsx @@ -33,6 +33,8 @@ export function CartPage({ completeOrder, handleSelectCoupon, getFilteredProducts, + getRemainingStock, + calculateTotal, }: { isAdmin: boolean setIsAdmin: React.Dispatch> @@ -57,6 +59,8 @@ export function CartPage({ coupons: Coupon[], ) => void getFilteredProducts: (searchTerm: string) => ProductWithUI[] + getRemainingStock: (product: ProductWithUI) => number + calculateTotal: (item: CartItem) => number }) { // TODO: 구현 const [searchTerm, setSearchTerm] = useState('') @@ -89,6 +93,7 @@ export function CartPage({ totals, completeOrder, handleSelectCoupon, + calculateTotal, } return ( @@ -116,8 +121,8 @@ export function CartPage({ ) : ( )}

diff --git a/src/basic/components/ui/Cart.tsx b/src/basic/components/ui/Cart.tsx index 9a352ad1..1c73d756 100644 --- a/src/basic/components/ui/Cart.tsx +++ b/src/basic/components/ui/Cart.tsx @@ -1,5 +1,4 @@ import { CartItem, Coupon } from '../../../types' -import { calculateItemTotal } from '../../models/cart' import { ProductWithUI } from '../../types' export const Cart = ({ @@ -12,6 +11,7 @@ export const Cart = ({ totals, completeOrder, handleSelectCoupon, + calculateTotal, }: { cart: CartItem[] coupons: Coupon[] @@ -32,6 +32,7 @@ export const Cart = ({ e: React.ChangeEvent, coupons: Coupon[], ) => void + calculateTotal: (item: CartItem) => number }) => { return (
@@ -73,7 +74,7 @@ export const Cart = ({ ) : (
{cart.map((item) => { - const itemTotal = calculateItemTotal(item, cart) + const itemTotal = calculateTotal(item) const originalPrice = item.product.price * item.quantity const hasDiscount = itemTotal < originalPrice const discountRate = hasDiscount diff --git a/src/basic/components/ui/ProductAccordion.tsx b/src/basic/components/ui/ProductAccordion.tsx index b16a22db..436e778a 100644 --- a/src/basic/components/ui/ProductAccordion.tsx +++ b/src/basic/components/ui/ProductAccordion.tsx @@ -1,18 +1,18 @@ import { CartItem } from '../../../types' -import { getRemainingStock } from '../../models/cart' import { ProductWithUI } from '../../types' import { formatPrice } from '../../utils/formatters' export const ProductAccordion = ({ products, - cart, startEditProduct, deleteProduct, + getRemainingStock, }: { products: ProductWithUI[] cart: CartItem[] startEditProduct: (product: ProductWithUI) => void deleteProduct: (productId: string) => void + getRemainingStock: (product: ProductWithUI) => number }) => { return (
@@ -46,7 +46,7 @@ export const ProductAccordion = ({ {formatPrice( product.price, true, - getRemainingStock(product, cart) <= 0, + getRemainingStock(product) <= 0, )} diff --git a/src/basic/components/ui/ProductList.tsx b/src/basic/components/ui/ProductList.tsx index fcd549b1..6ab28abc 100644 --- a/src/basic/components/ui/ProductList.tsx +++ b/src/basic/components/ui/ProductList.tsx @@ -1,21 +1,19 @@ -import { CartItem } from '../../../types' -import { getRemainingStock } from '../../models/cart' import { ProductWithUI } from '../../types' import { formatPrice } from '../../utils/formatters' export function ProductList({ filteredProducts, - cart, addToCart, + getRemainingStock, }: { filteredProducts: ProductWithUI[] - cart: CartItem[] addToCart: (product: ProductWithUI) => void + getRemainingStock: (product: ProductWithUI) => number }) { return (
{filteredProducts.map((product) => { - const remainingStock = getRemainingStock(product, cart) + const remainingStock = getRemainingStock(product) return (
{product.discounts.length > 0 && ( diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 33e78749..84567e76 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -27,7 +27,10 @@ import { useCallback, useState } from 'react' import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' -import { getRemainingStock, calculateItemTotal } from '../models/cart' +import { + getRemainingStock as _getRemainingStock, + calculateItemTotal, +} from '../models/cart' import { useLocalStorage } from '../utils/hooks/useLocalStorage' export function useCart( @@ -41,9 +44,23 @@ export function useCart( const [selectedCoupon, setSelectedCoupon] = useState(null) const [cart, setCart] = useLocalStorage('cart', []) + const getRemainingStock = useCallback( + (product: ProductWithUI) => { + return _getRemainingStock(product, cart) + }, + [cart], + ) + + const calculateTotal = useCallback( + (item: CartItem) => { + return calculateItemTotal(item, cart) + }, + [cart], + ) + const addToCart = useCallback( (product: ProductWithUI) => { - const remainingStock = getRemainingStock(product, cart) + const remainingStock = getRemainingStock(product) if (remainingStock <= 0) { addNotification('재고가 부족합니다!', 'error') return @@ -79,7 +96,7 @@ export function useCart( addNotification('장바구니에 담았습니다', 'success') }, - [addNotification, cart, setCart], + [addNotification, getRemainingStock, setCart], ) const removeFromCart = useCallback( @@ -196,7 +213,7 @@ export function useCart( setSelectedCoupon, applyCoupon, calculateCartTotal, - calculateItemTotal, + calculateTotal, completeOrder, getRemainingStock, handleSelectCoupon, diff --git a/src/basic/utils/hooks/useValidate.ts b/src/basic/utils/hooks/useValidate.ts deleted file mode 100644 index e69de29b..00000000 From 8ba8a2b3fb393a2b853f0b417464e313e914c3bf Mon Sep 17 00:00:00 2001 From: annkimm Date: Tue, 5 Aug 2025 23:57:14 +0900 Subject: [PATCH 06/16] refactor: separate folder cart, admin --- src/basic/components/AdminPage.tsx | 10 +-- src/basic/components/CartPage.tsx | 6 +- src/basic/components/ui/Header.tsx | 77 ------------------- .../components/ui/{ => admin}/AdminHeader.tsx | 0 .../components/ui/{ => admin}/CouponForm.tsx | 2 +- .../components/ui/{ => admin}/CouponList.tsx | 2 +- .../ui/{ => admin}/ProductAccordion.tsx | 6 +- .../components/ui/{ => admin}/ProductFom.tsx | 2 +- src/basic/components/ui/{ => cart}/Cart.tsx | 4 +- .../components/ui/{ => cart}/CartHeader.tsx | 2 +- .../components/ui/{ => cart}/ProductList.tsx | 4 +- 11 files changed, 19 insertions(+), 96 deletions(-) delete mode 100644 src/basic/components/ui/Header.tsx rename src/basic/components/ui/{ => admin}/AdminHeader.tsx (100%) rename src/basic/components/ui/{ => admin}/CouponForm.tsx (98%) rename src/basic/components/ui/{ => admin}/CouponList.tsx (97%) rename src/basic/components/ui/{ => admin}/ProductAccordion.tsx (95%) rename src/basic/components/ui/{ => admin}/ProductFom.tsx (99%) rename src/basic/components/ui/{ => cart}/Cart.tsx (98%) rename src/basic/components/ui/{ => cart}/CartHeader.tsx (98%) rename src/basic/components/ui/{ => cart}/ProductList.tsx (97%) diff --git a/src/basic/components/AdminPage.tsx b/src/basic/components/AdminPage.tsx index bed0a2ae..22bb3896 100644 --- a/src/basic/components/AdminPage.tsx +++ b/src/basic/components/AdminPage.tsx @@ -12,11 +12,11 @@ import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' import { useCouponForm, useProductForm } from '../hooks/useForm' import { useState } from 'react' -import { AdminHeader } from './ui/AdminHeader' -import { ProductFom } from './ui/ProductFom' -import { CouponForm } from './ui/CouponForm' -import { CouponList } from './ui/CouponList' -import { ProductAccordion } from './ui/ProductAccordion' +import { AdminHeader } from './ui/admin/AdminHeader' +import { ProductFom } from './ui/admin/ProductFom' +import { CouponForm } from './ui/admin/CouponForm' +import { CouponList } from './ui/admin/CouponList' +import { ProductAccordion } from './ui/admin/ProductAccordion' export function AdminPage({ isAdmin, diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx index b6e98273..8850e63e 100644 --- a/src/basic/components/CartPage.tsx +++ b/src/basic/components/CartPage.tsx @@ -14,9 +14,9 @@ import { useEffect, useState } from 'react' import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' -import { CartHeader } from './ui/CartHeader' -import { ProductList } from './ui/ProductList' -import { Cart } from './ui/Cart' +import { CartHeader } from './ui/cart/CartHeader' +import { ProductList } from './ui/cart/ProductList' +import { Cart } from './ui/cart/Cart' import { useDebounce } from '../utils/hooks/useDebounce' export function CartPage({ diff --git a/src/basic/components/ui/Header.tsx b/src/basic/components/ui/Header.tsx deleted file mode 100644 index c16850f0..00000000 --- a/src/basic/components/ui/Header.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { CartItem } from '../../../types' - -export const Header = ({ - isAdmin, - searchTerm, - cart, - totalItemCount, - setSearchTerm, - setIsAdmin, -}: { - isAdmin: boolean - searchTerm: string - cart: CartItem[] - totalItemCount: number - setSearchTerm: React.Dispatch> - setIsAdmin: React.Dispatch> -}) => { - return ( - <> -
-
-
-
-

SHOP

- {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} - {!isAdmin && ( -
- setSearchTerm(e.target.value)} - placeholder="상품 검색..." - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" - /> -
- )} -
- -
-
-
- - ) -} diff --git a/src/basic/components/ui/AdminHeader.tsx b/src/basic/components/ui/admin/AdminHeader.tsx similarity index 100% rename from src/basic/components/ui/AdminHeader.tsx rename to src/basic/components/ui/admin/AdminHeader.tsx diff --git a/src/basic/components/ui/CouponForm.tsx b/src/basic/components/ui/admin/CouponForm.tsx similarity index 98% rename from src/basic/components/ui/CouponForm.tsx rename to src/basic/components/ui/admin/CouponForm.tsx index 60dd818c..be350f62 100644 --- a/src/basic/components/ui/CouponForm.tsx +++ b/src/basic/components/ui/admin/CouponForm.tsx @@ -1,5 +1,5 @@ import { FocusEvent } from 'react' -import { Coupon } from '../../../types' +import { Coupon } from '../../../../types' export function CouponForm({ couponForm, diff --git a/src/basic/components/ui/CouponList.tsx b/src/basic/components/ui/admin/CouponList.tsx similarity index 97% rename from src/basic/components/ui/CouponList.tsx rename to src/basic/components/ui/admin/CouponList.tsx index 168eac2e..cfd64353 100644 --- a/src/basic/components/ui/CouponList.tsx +++ b/src/basic/components/ui/admin/CouponList.tsx @@ -1,4 +1,4 @@ -import { Coupon } from '../../../types' +import { Coupon } from '../../../../types' export const CouponList = ({ coupons, diff --git a/src/basic/components/ui/ProductAccordion.tsx b/src/basic/components/ui/admin/ProductAccordion.tsx similarity index 95% rename from src/basic/components/ui/ProductAccordion.tsx rename to src/basic/components/ui/admin/ProductAccordion.tsx index 436e778a..e55146cb 100644 --- a/src/basic/components/ui/ProductAccordion.tsx +++ b/src/basic/components/ui/admin/ProductAccordion.tsx @@ -1,6 +1,6 @@ -import { CartItem } from '../../../types' -import { ProductWithUI } from '../../types' -import { formatPrice } from '../../utils/formatters' +import { CartItem } from '../../../../types' +import { ProductWithUI } from '../../../types' +import { formatPrice } from '../../../utils/formatters' export const ProductAccordion = ({ products, diff --git a/src/basic/components/ui/ProductFom.tsx b/src/basic/components/ui/admin/ProductFom.tsx similarity index 99% rename from src/basic/components/ui/ProductFom.tsx rename to src/basic/components/ui/admin/ProductFom.tsx index 4855303e..f7bad090 100644 --- a/src/basic/components/ui/ProductFom.tsx +++ b/src/basic/components/ui/admin/ProductFom.tsx @@ -1,5 +1,5 @@ import React, { FocusEvent } from 'react' -import { ProductForm } from '../../../types' +import { ProductForm } from '../../../../types' export function ProductFom({ productForm, diff --git a/src/basic/components/ui/Cart.tsx b/src/basic/components/ui/cart/Cart.tsx similarity index 98% rename from src/basic/components/ui/Cart.tsx rename to src/basic/components/ui/cart/Cart.tsx index 1c73d756..d36f7f31 100644 --- a/src/basic/components/ui/Cart.tsx +++ b/src/basic/components/ui/cart/Cart.tsx @@ -1,5 +1,5 @@ -import { CartItem, Coupon } from '../../../types' -import { ProductWithUI } from '../../types' +import { CartItem, Coupon } from '../../../../types' +import { ProductWithUI } from '../../../types' export const Cart = ({ cart, diff --git a/src/basic/components/ui/CartHeader.tsx b/src/basic/components/ui/cart/CartHeader.tsx similarity index 98% rename from src/basic/components/ui/CartHeader.tsx rename to src/basic/components/ui/cart/CartHeader.tsx index f0e47a30..013d5aad 100644 --- a/src/basic/components/ui/CartHeader.tsx +++ b/src/basic/components/ui/cart/CartHeader.tsx @@ -1,4 +1,4 @@ -import { CartItem } from '../../../types' +import { CartItem } from '../../../../types' export const CartHeader = ({ isAdmin, diff --git a/src/basic/components/ui/ProductList.tsx b/src/basic/components/ui/cart/ProductList.tsx similarity index 97% rename from src/basic/components/ui/ProductList.tsx rename to src/basic/components/ui/cart/ProductList.tsx index 6ab28abc..eeb33b5e 100644 --- a/src/basic/components/ui/ProductList.tsx +++ b/src/basic/components/ui/cart/ProductList.tsx @@ -1,5 +1,5 @@ -import { ProductWithUI } from '../../types' -import { formatPrice } from '../../utils/formatters' +import { ProductWithUI } from '../../../types' +import { formatPrice } from '../../../utils/formatters' export function ProductList({ filteredProducts, From be473d8393c32cd20d2a5cf8c355dc946868cfd8 Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 00:02:00 +0900 Subject: [PATCH 07/16] feat: add totalItemCount in userCart from page --- src/basic/App.tsx | 2 ++ src/basic/components/CartPage.tsx | 11 +++-------- src/basic/hooks/useCart.ts | 10 +++++++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/basic/App.tsx b/src/basic/App.tsx index dfe5a6b4..6c957579 100644 --- a/src/basic/App.tsx +++ b/src/basic/App.tsx @@ -40,6 +40,7 @@ const App = () => { getRemainingStock, handleSelectCoupon, calculateTotal, + totalItemCount, } = useCart(addNotification) const { coupons, addCoupon, deleteCoupon } = useCoupons( addNotification, @@ -79,6 +80,7 @@ const App = () => { getRemainingStock, getFilteredProducts, calculateTotal, + totalItemCount, } return ( diff --git a/src/basic/components/CartPage.tsx b/src/basic/components/CartPage.tsx index 8850e63e..daaf5461 100644 --- a/src/basic/components/CartPage.tsx +++ b/src/basic/components/CartPage.tsx @@ -11,7 +11,7 @@ // - useCoupons: 쿠폰 목록 관리 // - useDebounce: 검색어 디바운싱 -import { useEffect, useState } from 'react' +import { useState } from 'react' import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' import { CartHeader } from './ui/cart/CartHeader' @@ -35,6 +35,7 @@ export function CartPage({ getFilteredProducts, getRemainingStock, calculateTotal, + totalItemCount, }: { isAdmin: boolean setIsAdmin: React.Dispatch> @@ -61,17 +62,11 @@ export function CartPage({ getFilteredProducts: (searchTerm: string) => ProductWithUI[] getRemainingStock: (product: ProductWithUI) => number calculateTotal: (item: CartItem) => number + totalItemCount: number }) { // TODO: 구현 const [searchTerm, setSearchTerm] = useState('') - const [totalItemCount, setTotalItemCount] = useState(0) const debouncedSearchTerm = useDebounce(searchTerm, 500) - - useEffect(() => { - const count = cart.reduce((sum, item) => sum + item.quantity, 0) - setTotalItemCount(count) - }, [cart]) - const filteredProducts = getFilteredProducts(debouncedSearchTerm) const headerState = { diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 84567e76..6c15d771 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -24,7 +24,7 @@ // - getRemainingStock: 재고 확인 함수 // - clearCart: 장바구니 비우기 함수 -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { CartItem, Coupon } from '../../types' import { ProductWithUI } from '../types' import { @@ -204,6 +204,13 @@ export function useCart( else setSelectedCoupon(null) } + const [totalItemCount, setTotalItemCount] = useState(0) + + useEffect(() => { + const count = cart.reduce((sum, item) => sum + item.quantity, 0) + setTotalItemCount(count) + }, [cart]) + return { cart, addToCart, @@ -217,5 +224,6 @@ export function useCart( completeOrder, getRemainingStock, handleSelectCoupon, + totalItemCount, } } From 3d50a3bea0c940af3aeb0a839351cce1dd583544 Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 17:42:29 +0900 Subject: [PATCH 08/16] feat: add product entity --- src/basic/components/icons/index.tsx | 12 ----- src/basic/hooks/useCart.ts | 71 +++++----------------------- src/basic/hooks/useProducts.ts | 9 +--- src/basic/models/cart.ts | 49 +++++++++++++++++-- src/basic/models/product.ts | 32 +++++++++++++ 5 files changed, 91 insertions(+), 82 deletions(-) delete mode 100644 src/basic/components/icons/index.tsx diff --git a/src/basic/components/icons/index.tsx b/src/basic/components/icons/index.tsx deleted file mode 100644 index 1609d774..00000000 --- a/src/basic/components/icons/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: SVG 아이콘 컴포넌트들 -// 구현할 아이콘: -// - CartIcon: 장바구니 아이콘 -// - AdminIcon: 관리자 아이콘 -// - PlusIcon: 플러스 아이콘 -// - MinusIcon: 마이너스 아이콘 -// - TrashIcon: 삭제 아이콘 -// - ChevronDownIcon: 아래 화살표 -// - ChevronUpIcon: 위 화살표 -// - CheckIcon: 체크 아이콘 - -// TODO: 구현 \ No newline at end of file diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 6c15d771..68fc324d 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -30,6 +30,8 @@ import { ProductWithUI } from '../types' import { getRemainingStock as _getRemainingStock, calculateItemTotal, + calculateCartTotal as _calculateCartTotal, + addItemToCart, } from '../models/cart' import { useLocalStorage } from '../utils/hooks/useLocalStorage' @@ -60,43 +62,18 @@ export function useCart( const addToCart = useCallback( (product: ProductWithUI) => { - const remainingStock = getRemainingStock(product) + const remainingStock = _getRemainingStock(product, cart) if (remainingStock <= 0) { addNotification('재고가 부족합니다!', 'error') return } - // const cartItem = cart.find(item => item.product.id === product.id) + const { carts, message, type } = addItemToCart(cart, product) - setCart((prevCart) => { - const existingItem = prevCart.find( - (item) => item.product.id === product.id, - ) - - if (existingItem) { - const newQuantity = existingItem.quantity + 1 - - if (newQuantity > product.stock) { - addNotification( - `재고는 ${product.stock}개까지만 있습니다.`, - 'error', - ) - return prevCart - } - - return prevCart.map((item) => - item.product.id === product.id - ? { ...item, quantity: newQuantity } - : item, - ) - } - - return [...prevCart, { product, quantity: 1 }] - }) - - addNotification('장바구니에 담았습니다', 'success') + setCart(carts) + addNotification(message, type) }, - [addNotification, getRemainingStock, setCart], + [addNotification, cart, setCart], ) const removeFromCart = useCallback( @@ -139,37 +116,15 @@ export function useCart( totalBeforeDiscount: number totalAfterDiscount: number } => { - let totalBeforeDiscount = 0 - let totalAfterDiscount = 0 - - cart.forEach((item) => { - const itemPrice = item.product.price * item.quantity - totalBeforeDiscount += itemPrice - totalAfterDiscount += calculateItemTotal(item, cart) - }) - - if (selectedCoupon) { - if (selectedCoupon.discountType === 'amount') { - totalAfterDiscount = Math.max( - 0, - totalAfterDiscount - selectedCoupon.discountValue, - ) - } else { - totalAfterDiscount = Math.round( - totalAfterDiscount * (1 - selectedCoupon.discountValue / 100), - ) - } - } - - return { - totalBeforeDiscount: Math.round(totalBeforeDiscount), - totalAfterDiscount: Math.round(totalAfterDiscount), - } + return _calculateCartTotal(cart, selectedCoupon) }, [cart, selectedCoupon]) const applyCoupon = useCallback( (coupon: Coupon) => { - const currentTotal = calculateCartTotal().totalAfterDiscount + const currentTotal = _calculateCartTotal( + cart, + selectedCoupon, + ).totalAfterDiscount if (currentTotal < 10000 && coupon.discountType === 'percentage') { addNotification( @@ -182,7 +137,7 @@ export function useCart( setSelectedCoupon(coupon) addNotification('쿠폰이 적용되었습니다.', 'success') }, - [addNotification, calculateCartTotal, setSelectedCoupon], + [addNotification, cart, selectedCoupon], ) const completeOrder = useCallback(() => { diff --git a/src/basic/hooks/useProducts.ts b/src/basic/hooks/useProducts.ts index e6c95e39..60ea56c0 100644 --- a/src/basic/hooks/useProducts.ts +++ b/src/basic/hooks/useProducts.ts @@ -17,6 +17,7 @@ import { useCallback } from 'react' import { initialProducts } from '../constants' import { ProductWithUI } from '../types' import { useLocalStorage } from '../utils/hooks/useLocalStorage' +import { getFilteredProducts as _getFilteredProducts } from '../models/product' export function useProducts( addNotification: ( @@ -65,13 +66,7 @@ export function useProducts( const getFilteredProducts = useCallback( (searchTerm: string) => { - if (!searchTerm) return products - - return products.filter( - (product) => - product.name.toLowerCase().includes(searchTerm.toLowerCase()) || - product.description?.toLowerCase().includes(searchTerm.toLowerCase()), - ) + return _getFilteredProducts(searchTerm, products) }, [products], ) diff --git a/src/basic/models/cart.ts b/src/basic/models/cart.ts index 1c385d2b..77afd104 100644 --- a/src/basic/models/cart.ts +++ b/src/basic/models/cart.ts @@ -7,8 +7,7 @@ // 3. calculateCartTotal(cart, coupon): 장바구니 총액 계산 (할인 전/후, 할인액) // 4. updateCartItemQuantity(cart, productId, quantity): 수량 변경 // 5. addItemToCart(cart, product): 상품 추가 -// 6. removeItemFromCart(cart, productId): 상품 제거 -// 7. getRemainingStock(product, cart): 남은 재고 계산 +// 6. getRemainingStock(product, cart): 남은 재고 계산 // // 원칙: // - UI와 관련된 로직 없음 @@ -16,6 +15,7 @@ // - 모든 필요한 데이터는 파라미터로 전달받음 import { CartItem, Coupon, Product } from '../../types' +import { ProductWithUI } from '../types' // TODO: 구현 export const getRemainingStock = ( @@ -28,7 +28,7 @@ export const getRemainingStock = ( return remaining } -const getBasicDiscont = (item: CartItem) => { +const getBasicDiscount = (item: CartItem) => { const { discounts } = item.product const { quantity } = item @@ -43,7 +43,7 @@ export const getMaxApplicableDiscount = ( item: CartItem, cart: CartItem[], ): number => { - const baseDiscount = getBasicDiscont(item) + const baseDiscount = getBasicDiscount(item) const hasBulkPurchase = cart.some((cartItem) => cartItem.quantity >= 10) return hasBulkPurchase ? Math.min(baseDiscount + 0.05, 0.5) : baseDiscount } @@ -61,7 +61,7 @@ export const calculateItemTotal = ( export const calculateCartTotal = ( cart: CartItem[], - coupon: Coupon, + coupon: Coupon | null, ): { totalBeforeDiscount: number; totalAfterDiscount: number } => { let totalBeforeDiscount = 0 let totalAfterDiscount = 0 @@ -90,3 +90,42 @@ export const calculateCartTotal = ( totalAfterDiscount: Math.round(totalAfterDiscount), } } + +export const addItemToCart = ( + carts: CartItem[], + product: ProductWithUI, +): { + carts: CartItem[] + message: string + type: 'error' | 'success' // 명확한 타입 +} => { + const existingItem = carts.find((item) => item.product.id === product.id) + + if (existingItem) { + const newQuantity = existingItem.quantity + 1 + + if (newQuantity > product.stock) { + return { + carts: carts, + message: `재고는 ${product.stock}개까지만 있습니다.`, + type: 'error', + } + } + + return { + carts: carts.map((item) => + item.product.id === product.id + ? { ...item, quantity: newQuantity } + : item, + ), + message: '장바구니에 담았습니다', + type: 'success', + } + } + + return { + carts: [...carts, { product, quantity: 1 }], + message: '장바구니에 담았습니다', + type: 'success', + } +} diff --git a/src/basic/models/product.ts b/src/basic/models/product.ts index e69de29b..d22fb56b 100644 --- a/src/basic/models/product.ts +++ b/src/basic/models/product.ts @@ -0,0 +1,32 @@ +import { ProductWithUI } from '../types' + +export const isRecommendedProduct = (product: ProductWithUI) => { + return product.isRecommended +} + +export const isStock = (product: ProductWithUI) => { + return product.stock > 0 +} + +export const isDiscounts = (product: ProductWithUI) => { + return product.discounts.length > 0 +} + +export const getMaxDiscountRate = (product: ProductWithUI) => { + if (product.discounts.length === 0) return 0 + const rates = product.discounts.map((item) => item.rate) + return Math.max(...rates) +} + +export const getFilteredProducts = ( + searchTerm: string, + products: ProductWithUI[], +) => { + if (!searchTerm) return products + + return products.filter( + (product) => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) || + product.description?.toLowerCase().includes(searchTerm.toLowerCase()), + ) +} From b15729675b7de9c29befd0b0b36d3251053066d6 Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 18:27:06 +0900 Subject: [PATCH 09/16] feat: add model coupon --- src/basic/models/cart.ts | 12 ++---------- src/basic/models/coupon.ts | 14 ++++++++++++++ src/basic/models/product.ts | 12 ++++++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/basic/models/cart.ts b/src/basic/models/cart.ts index 77afd104..cc2a8679 100644 --- a/src/basic/models/cart.ts +++ b/src/basic/models/cart.ts @@ -16,6 +16,7 @@ import { CartItem, Coupon, Product } from '../../types' import { ProductWithUI } from '../types' +import { calculateDiscountedTotal } from './coupon' // TODO: 구현 export const getRemainingStock = ( @@ -73,16 +74,7 @@ export const calculateCartTotal = ( }) if (coupon) { - if (coupon.discountType === 'amount') { - totalAfterDiscount = Math.max( - 0, - totalAfterDiscount - coupon.discountValue, - ) - } else { - totalAfterDiscount = Math.round( - totalAfterDiscount * (1 - coupon.discountValue / 100), - ) - } + totalAfterDiscount = calculateDiscountedTotal(totalAfterDiscount, coupon) } return { diff --git a/src/basic/models/coupon.ts b/src/basic/models/coupon.ts index e69de29b..9b43cb7f 100644 --- a/src/basic/models/coupon.ts +++ b/src/basic/models/coupon.ts @@ -0,0 +1,14 @@ +import { Coupon } from '../../types' + +export const calculateDiscountedTotal = (total: number, coupon: Coupon) => { + let totalAfterDiscount = total + if (coupon.discountType === 'amount') { + totalAfterDiscount = Math.max(0, totalAfterDiscount - coupon.discountValue) + } else { + totalAfterDiscount = Math.round( + totalAfterDiscount * (1 - coupon.discountValue / 100), + ) + } + + return totalAfterDiscount +} diff --git a/src/basic/models/product.ts b/src/basic/models/product.ts index d22fb56b..c2fe29a2 100644 --- a/src/basic/models/product.ts +++ b/src/basic/models/product.ts @@ -18,6 +18,18 @@ export const getMaxDiscountRate = (product: ProductWithUI) => { return Math.max(...rates) } +export const isRemainingRange = ( + min: number, + max: number, + remainingStock: number, +) => { + return remainingStock <= max && remainingStock > min +} + +export const isOutOfStock = (remainingStock: number) => { + return remainingStock <= 0 +} + export const getFilteredProducts = ( searchTerm: string, products: ProductWithUI[], From c1d7db0f5699a3b78b254d943966806ca30efb51 Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 19:00:58 +0900 Subject: [PATCH 10/16] feat: add more const --- src/basic/components/ui/admin/ProductFom.tsx | 5 +++-- src/basic/components/ui/cart/Cart.tsx | 5 ++++- src/basic/components/ui/cart/ProductList.tsx | 21 ++++++++++++-------- src/basic/constants/index.ts | 16 ++++++++++++++- src/basic/hooks/useCart.ts | 6 +++++- src/basic/hooks/useForm.ts | 19 +++++++++++------- src/basic/models/coupon.ts | 3 ++- 7 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/basic/components/ui/admin/ProductFom.tsx b/src/basic/components/ui/admin/ProductFom.tsx index f7bad090..8357215a 100644 --- a/src/basic/components/ui/admin/ProductFom.tsx +++ b/src/basic/components/ui/admin/ProductFom.tsx @@ -1,5 +1,6 @@ import React, { FocusEvent } from 'react' import { ProductForm } from '../../../../types' +import { MAX_DISCOUNT_RATE } from '../../../constants' export function ProductFom({ productForm, @@ -110,11 +111,11 @@ export function ProductFom({ 개 이상 구매 시 handleEditProuctForm(e, 'rate', index)} className="w-16 px-2 py-1 border rounded" min="0" - max="100" + max={MAX_DISCOUNT_RATE} placeholder="%" /> % 할인 diff --git a/src/basic/components/ui/cart/Cart.tsx b/src/basic/components/ui/cart/Cart.tsx index d36f7f31..f1f0270c 100644 --- a/src/basic/components/ui/cart/Cart.tsx +++ b/src/basic/components/ui/cart/Cart.tsx @@ -1,4 +1,5 @@ import { CartItem, Coupon } from '../../../../types' +import { MAX_DISCOUNT_RATE } from '../../../constants' import { ProductWithUI } from '../../../types' export const Cart = ({ @@ -78,7 +79,9 @@ export const Cart = ({ const originalPrice = item.product.price * item.quantity const hasDiscount = itemTotal < originalPrice const discountRate = hasDiscount - ? Math.round((1 - itemTotal / originalPrice) * 100) + ? Math.round( + (1 - itemTotal / originalPrice) * MAX_DISCOUNT_RATE, + ) : 0 return ( diff --git a/src/basic/components/ui/cart/ProductList.tsx b/src/basic/components/ui/cart/ProductList.tsx index eeb33b5e..329dd6c2 100644 --- a/src/basic/components/ui/cart/ProductList.tsx +++ b/src/basic/components/ui/cart/ProductList.tsx @@ -1,3 +1,4 @@ +import { LOW_STOCK_THRESHOLD, MAX_DISCOUNT_RATE } from '../../../constants' import { ProductWithUI } from '../../../types' import { formatPrice } from '../../../utils/formatters' @@ -44,7 +45,10 @@ export function ProductList({ )} {product.discounts.length > 0 && ( - ~{Math.max(...product.discounts.map((d) => d.rate)) * 100}% + ~ + {Math.max(...product.discounts.map((d) => d.rate)) * + MAX_DISCOUNT_RATE} + % )}
@@ -70,19 +74,20 @@ export function ProductList({ {product.discounts.length > 0 && (

{product.discounts[0].quantity}개 이상 구매시 할인{' '} - {product.discounts[0].rate * 100}% + {product.discounts[0].rate * MAX_DISCOUNT_RATE}%

)}
{/* 재고 상태 */}
- {remainingStock <= 5 && remainingStock > 0 && ( -

- 품절임박! {remainingStock}개 남음 -

- )} - {remainingStock > 5 && ( + {remainingStock <= LOW_STOCK_THRESHOLD && + remainingStock > 0 && ( +

+ 품절임박! {remainingStock}개 남음 +

+ )} + {remainingStock > LOW_STOCK_THRESHOLD && (

재고 {remainingStock}개

diff --git a/src/basic/constants/index.ts b/src/basic/constants/index.ts index 2733a6f2..a3f6fb3a 100644 --- a/src/basic/constants/index.ts +++ b/src/basic/constants/index.ts @@ -9,13 +9,27 @@ import { Coupon } from '../../types' import { ProductWithUI } from '../types' // TODO: 구현 +// 재고 관련 상수 +export const LOW_STOCK_THRESHOLD = 5 + +// 쿠폰 관련 상수 +export const MIN_COUPON_AMOUNT = 10000 + +// 할인율 관련 상수 +export const MAX_DISCOUNT_RATE = 100 + +// 재고 관련 상수 +export const MAX_STOCK_LIMIT = 9999 + +// 할인 금액 관련 상수 +export const MAX_DISCOUNT_AMOUNT = 100000 // 초기 데이터 export const initialProducts: ProductWithUI[] = [ { id: 'p1', name: '상품1', - price: 10000, + price: MIN_COUPON_AMOUNT, stock: 20, discounts: [ { quantity: 10, rate: 0.1 }, diff --git a/src/basic/hooks/useCart.ts b/src/basic/hooks/useCart.ts index 68fc324d..cf4f2b65 100644 --- a/src/basic/hooks/useCart.ts +++ b/src/basic/hooks/useCart.ts @@ -34,6 +34,7 @@ import { addItemToCart, } from '../models/cart' import { useLocalStorage } from '../utils/hooks/useLocalStorage' +import { MIN_COUPON_AMOUNT } from '../constants' export function useCart( addNotification: ( @@ -126,7 +127,10 @@ export function useCart( selectedCoupon, ).totalAfterDiscount - if (currentTotal < 10000 && coupon.discountType === 'percentage') { + if ( + currentTotal < MIN_COUPON_AMOUNT && + coupon.discountType === 'percentage' + ) { addNotification( 'percentage 쿠폰은 10,000원 이상 구매 시 사용 가능합니다.', 'error', diff --git a/src/basic/hooks/useForm.ts b/src/basic/hooks/useForm.ts index 4f338a52..88d30a06 100644 --- a/src/basic/hooks/useForm.ts +++ b/src/basic/hooks/useForm.ts @@ -2,6 +2,11 @@ import { useState, FocusEvent } from 'react' import { ProductWithUI } from '../types' import { Coupon, Discount, ProductForm } from '../../types' import { isValidStrNumber } from '../utils/validators' +import { + MAX_DISCOUNT_AMOUNT, + MAX_DISCOUNT_RATE, + MAX_STOCK_LIMIT, +} from '../constants' export function useCouponForm( addCoupon: (newCoupon: Coupon) => void, @@ -54,11 +59,11 @@ export function useCouponForm( const value = parseInt(e.target.value) || 0 if (couponForm.discountType === 'percentage') { - if (value > 100) { + if (value > MAX_DISCOUNT_RATE) { addNotification('할인율은 100%를 초과할 수 없습니다', 'error') setCouponForm({ ...couponForm, - discountValue: 100, + discountValue: MAX_DISCOUNT_RATE, }) } else if (value < 0) { setCouponForm({ @@ -67,11 +72,11 @@ export function useCouponForm( }) } } else { - if (value > 100000) { + if (value > MAX_DISCOUNT_AMOUNT) { addNotification('할인 금액은 100,000원을 초과할 수 없습니다', 'error') setCouponForm({ ...couponForm, - discountValue: 100000, + discountValue: MAX_DISCOUNT_AMOUNT, }) } else if (value < 0) { setCouponForm({ @@ -188,7 +193,7 @@ export function useProductForm( const newDiscounts = [...productForm.discounts] const value = parseInt(e.target.value) || 0 newDiscounts[index][key as keyof Discount] = - value / (key === 'rate' ? 100 : 1) + value / (key === 'rate' ? MAX_DISCOUNT_RATE : 1) setProductForm({ ...productForm, discounts: newDiscounts, @@ -228,9 +233,9 @@ export function useProductForm( } else if (parseInt(value) < 0) { addNotification('재고는 0보다 커야 합니다', 'error') setProductForm({ ...productForm, stock: 0 }) - } else if (parseInt(value) > 9999) { + } else if (parseInt(value) > MAX_STOCK_LIMIT) { addNotification('재고는 9999개를 초과할 수 없습니다', 'error') - setProductForm({ ...productForm, stock: 9999 }) + setProductForm({ ...productForm, stock: MAX_STOCK_LIMIT }) } } diff --git a/src/basic/models/coupon.ts b/src/basic/models/coupon.ts index 9b43cb7f..35634989 100644 --- a/src/basic/models/coupon.ts +++ b/src/basic/models/coupon.ts @@ -1,4 +1,5 @@ import { Coupon } from '../../types' +import { MAX_DISCOUNT_RATE } from '../constants' export const calculateDiscountedTotal = (total: number, coupon: Coupon) => { let totalAfterDiscount = total @@ -6,7 +7,7 @@ export const calculateDiscountedTotal = (total: number, coupon: Coupon) => { totalAfterDiscount = Math.max(0, totalAfterDiscount - coupon.discountValue) } else { totalAfterDiscount = Math.round( - totalAfterDiscount * (1 - coupon.discountValue / 100), + totalAfterDiscount * (1 - coupon.discountValue / MAX_DISCOUNT_RATE), ) } From 13d4aaa65645ce8877ff7273e857a13ab311532a Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 19:24:44 +0900 Subject: [PATCH 11/16] refactor: remove dont' need function model product --- src/basic/models/discount.ts | 0 src/basic/models/product.ts | 30 ------------------------------ 2 files changed, 30 deletions(-) delete mode 100644 src/basic/models/discount.ts diff --git a/src/basic/models/discount.ts b/src/basic/models/discount.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/basic/models/product.ts b/src/basic/models/product.ts index c2fe29a2..d2048af8 100644 --- a/src/basic/models/product.ts +++ b/src/basic/models/product.ts @@ -1,35 +1,5 @@ import { ProductWithUI } from '../types' -export const isRecommendedProduct = (product: ProductWithUI) => { - return product.isRecommended -} - -export const isStock = (product: ProductWithUI) => { - return product.stock > 0 -} - -export const isDiscounts = (product: ProductWithUI) => { - return product.discounts.length > 0 -} - -export const getMaxDiscountRate = (product: ProductWithUI) => { - if (product.discounts.length === 0) return 0 - const rates = product.discounts.map((item) => item.rate) - return Math.max(...rates) -} - -export const isRemainingRange = ( - min: number, - max: number, - remainingStock: number, -) => { - return remainingStock <= max && remainingStock > min -} - -export const isOutOfStock = (remainingStock: number) => { - return remainingStock <= 0 -} - export const getFilteredProducts = ( searchTerm: string, products: ProductWithUI[], From d13bdd9e712426c8260c7cf6a1f60b71d7d8c6b6 Mon Sep 17 00:00:00 2001 From: annkimm Date: Wed, 6 Aug 2025 23:28:09 +0900 Subject: [PATCH 12/16] feat: add basic code in advanced --- src/advanced/App.tsx | 1203 ++--------------- src/advanced/components/AdminPage.tsx | 197 +++ src/advanced/components/CartPage.tsx | 131 ++ src/advanced/components/ui/Notifications.tsx | 54 + .../components/ui/admin/AdminHeader.tsx | 27 + .../components/ui/admin/CouponForm.tsx | 99 ++ .../components/ui/admin/CouponList.tsx | 54 + .../components/ui/admin/ProductAccordion.tsx | 88 ++ .../components/ui/admin/ProductFom.tsx | 172 +++ src/advanced/components/ui/cart/Cart.tsx | 239 ++++ .../components/ui/cart/CartHeader.tsx | 69 + .../components/ui/cart/ProductList.tsx | 115 ++ src/advanced/constants/index.ts | 75 + src/advanced/hooks/useCart.ts | 188 +++ src/advanced/hooks/useCoupons.ts | 60 + src/advanced/hooks/useForm.ts | 258 ++++ src/advanced/hooks/useProducts.ts | 81 ++ src/advanced/models/cart.ts | 123 ++ src/advanced/models/coupon.ts | 15 + src/advanced/models/product.ts | 14 + src/advanced/types/index.ts | 12 + src/advanced/utils/formatters.ts | 23 + src/advanced/utils/hooks/useDebounce.ts | 24 + src/advanced/utils/hooks/useLocalStorage.ts | 60 + src/advanced/utils/validators.ts | 13 + 25 files changed, 2279 insertions(+), 1115 deletions(-) create mode 100644 src/advanced/components/AdminPage.tsx create mode 100644 src/advanced/components/CartPage.tsx create mode 100644 src/advanced/components/ui/Notifications.tsx create mode 100644 src/advanced/components/ui/admin/AdminHeader.tsx create mode 100644 src/advanced/components/ui/admin/CouponForm.tsx create mode 100644 src/advanced/components/ui/admin/CouponList.tsx create mode 100644 src/advanced/components/ui/admin/ProductAccordion.tsx create mode 100644 src/advanced/components/ui/admin/ProductFom.tsx create mode 100644 src/advanced/components/ui/cart/Cart.tsx create mode 100644 src/advanced/components/ui/cart/CartHeader.tsx create mode 100644 src/advanced/components/ui/cart/ProductList.tsx create mode 100644 src/advanced/constants/index.ts create mode 100644 src/advanced/hooks/useCart.ts create mode 100644 src/advanced/hooks/useCoupons.ts create mode 100644 src/advanced/hooks/useForm.ts create mode 100644 src/advanced/hooks/useProducts.ts create mode 100644 src/advanced/models/cart.ts create mode 100644 src/advanced/models/coupon.ts create mode 100644 src/advanced/models/product.ts create mode 100644 src/advanced/types/index.ts create mode 100644 src/advanced/utils/formatters.ts create mode 100644 src/advanced/utils/hooks/useDebounce.ts create mode 100644 src/advanced/utils/hooks/useLocalStorage.ts create mode 100644 src/advanced/utils/validators.ts diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx index a4369fe1..6c957579 100644 --- a/src/advanced/App.tsx +++ b/src/advanced/App.tsx @@ -1,1124 +1,97 @@ -import { useState, useCallback, useEffect } from 'react'; -import { CartItem, Coupon, Product } from '../types'; +import { useState, useCallback } from 'react' +import { Notification } from './types' +import { AdminPage } from './components/AdminPage' +import { useProducts } from './hooks/useProducts' +import { CartPage } from './components/CartPage' +import { useCart } from './hooks/useCart' +import { useCoupons } from './hooks/useCoupons' +import { Notifications } from './components/ui/Notifications' -interface ProductWithUI extends Product { - description?: string; - isRecommended?: boolean; -} - -interface Notification { - id: string; - message: string; - type: 'error' | 'success' | 'warning'; -} - -// 초기 데이터 -const initialProducts: ProductWithUI[] = [ - { - id: 'p1', - name: '상품1', - price: 10000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.1 }, - { quantity: 20, rate: 0.2 } - ], - description: '최고급 품질의 프리미엄 상품입니다.' - }, - { - id: 'p2', - name: '상품2', - price: 20000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.15 } - ], - description: '다양한 기능을 갖춘 실용적인 상품입니다.', - isRecommended: true - }, - { - id: 'p3', - name: '상품3', - price: 30000, - stock: 20, - discounts: [ - { quantity: 10, rate: 0.2 }, - { quantity: 30, rate: 0.25 } - ], - description: '대용량과 고성능을 자랑하는 상품입니다.' +const App = () => { + const [notifications, setNotifications] = useState([]) + const addNotification = useCallback( + (message: string, type: 'error' | 'success' | 'warning' = 'success') => { + const id = Date.now().toString() + setNotifications((prev) => [...prev, { id, message, type }]) + + setTimeout(() => { + setNotifications((prev) => prev.filter((n) => n.id !== id)) + }, 3000) + }, + [], + ) + + const { + products, + addProduct, + updateProduct, + deleteProduct, + getFilteredProducts, + } = useProducts(addNotification) + const { + cart, + completeOrder, + removeFromCart, + updateQuantity, + selectedCoupon, + setSelectedCoupon, + addToCart, + calculateCartTotal, + getRemainingStock, + handleSelectCoupon, + calculateTotal, + totalItemCount, + } = useCart(addNotification) + const { coupons, addCoupon, deleteCoupon } = useCoupons( + addNotification, + selectedCoupon, + setSelectedCoupon, + ) + const [isAdmin, setIsAdmin] = useState(false) + + const adminProps = { + isAdmin, + setIsAdmin, + products, + cart, + deleteProduct, + addNotification, + coupons, + deleteCoupon, + updateProduct, + addProduct, + addCoupon, + getRemainingStock, } -]; -const initialCoupons: Coupon[] = [ - { - name: '5000원 할인', - code: 'AMOUNT5000', - discountType: 'amount', - discountValue: 5000 - }, - { - name: '10% 할인', - code: 'PERCENT10', - discountType: 'percentage', - discountValue: 10 + const cartProps = { + isAdmin, + setIsAdmin, + products, + cart, + coupons, + totals: calculateCartTotal(), + selectedCoupon, + addToCart, + removeFromCart, + handleSelectCoupon, + updateQuantity, + completeOrder, + getRemainingStock, + getFilteredProducts, + calculateTotal, + totalItemCount, } -]; - -const App = () => { - - const [products, setProducts] = useState(() => { - const saved = localStorage.getItem('products'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return initialProducts; - } - } - return initialProducts; - }); - - const [cart, setCart] = useState(() => { - const saved = localStorage.getItem('cart'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return []; - } - } - return []; - }); - - const [coupons, setCoupons] = useState(() => { - const saved = localStorage.getItem('coupons'); - if (saved) { - try { - return JSON.parse(saved); - } catch { - return initialCoupons; - } - } - return initialCoupons; - }); - - const [selectedCoupon, setSelectedCoupon] = useState(null); - const [isAdmin, setIsAdmin] = useState(false); - const [notifications, setNotifications] = useState([]); - const [showCouponForm, setShowCouponForm] = useState(false); - const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products'); - const [showProductForm, setShowProductForm] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); - - // Admin - const [editingProduct, setEditingProduct] = useState(null); - const [productForm, setProductForm] = useState({ - name: '', - price: 0, - stock: 0, - description: '', - discounts: [] as Array<{ quantity: number; rate: number }> - }); - - const [couponForm, setCouponForm] = useState({ - name: '', - code: '', - discountType: 'amount' as 'amount' | 'percentage', - discountValue: 0 - }); - - - const formatPrice = (price: number, productId?: string): string => { - if (productId) { - const product = products.find(p => p.id === productId); - if (product && getRemainingStock(product) <= 0) { - return 'SOLD OUT'; - } - } - - if (isAdmin) { - return `${price.toLocaleString()}원`; - } - - return `₩${price.toLocaleString()}`; - }; - - const getMaxApplicableDiscount = (item: CartItem): number => { - const { discounts } = item.product; - const { quantity } = item; - - const baseDiscount = discounts.reduce((maxDiscount, discount) => { - return quantity >= discount.quantity && discount.rate > maxDiscount - ? discount.rate - : maxDiscount; - }, 0); - - const hasBulkPurchase = cart.some(cartItem => cartItem.quantity >= 10); - if (hasBulkPurchase) { - return Math.min(baseDiscount + 0.05, 0.5); // 대량 구매 시 추가 5% 할인 - } - - return baseDiscount; - }; - - const calculateItemTotal = (item: CartItem): number => { - const { price } = item.product; - const { quantity } = item; - const discount = getMaxApplicableDiscount(item); - - return Math.round(price * quantity * (1 - discount)); - }; - - const calculateCartTotal = (): { - totalBeforeDiscount: number; - totalAfterDiscount: number; - } => { - let totalBeforeDiscount = 0; - let totalAfterDiscount = 0; - - cart.forEach(item => { - const itemPrice = item.product.price * item.quantity; - totalBeforeDiscount += itemPrice; - totalAfterDiscount += calculateItemTotal(item); - }); - - if (selectedCoupon) { - if (selectedCoupon.discountType === 'amount') { - totalAfterDiscount = Math.max(0, totalAfterDiscount - selectedCoupon.discountValue); - } else { - totalAfterDiscount = Math.round(totalAfterDiscount * (1 - selectedCoupon.discountValue / 100)); - } - } - - return { - totalBeforeDiscount: Math.round(totalBeforeDiscount), - totalAfterDiscount: Math.round(totalAfterDiscount) - }; - }; - - const getRemainingStock = (product: Product): number => { - const cartItem = cart.find(item => item.product.id === product.id); - const remaining = product.stock - (cartItem?.quantity || 0); - - return remaining; - }; - - const addNotification = useCallback((message: string, type: 'error' | 'success' | 'warning' = 'success') => { - const id = Date.now().toString(); - setNotifications(prev => [...prev, { id, message, type }]); - - setTimeout(() => { - setNotifications(prev => prev.filter(n => n.id !== id)); - }, 3000); - }, []); - - const [totalItemCount, setTotalItemCount] = useState(0); - - - useEffect(() => { - const count = cart.reduce((sum, item) => sum + item.quantity, 0); - setTotalItemCount(count); - }, [cart]); - - useEffect(() => { - localStorage.setItem('products', JSON.stringify(products)); - }, [products]); - - useEffect(() => { - localStorage.setItem('coupons', JSON.stringify(coupons)); - }, [coupons]); - - useEffect(() => { - if (cart.length > 0) { - localStorage.setItem('cart', JSON.stringify(cart)); - } else { - localStorage.removeItem('cart'); - } - }, [cart]); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm); - }, 500); - return () => clearTimeout(timer); - }, [searchTerm]); - - const addToCart = useCallback((product: ProductWithUI) => { - const remainingStock = getRemainingStock(product); - if (remainingStock <= 0) { - addNotification('재고가 부족합니다!', 'error'); - return; - } - - setCart(prevCart => { - const existingItem = prevCart.find(item => item.product.id === product.id); - - if (existingItem) { - const newQuantity = existingItem.quantity + 1; - - if (newQuantity > product.stock) { - addNotification(`재고는 ${product.stock}개까지만 있습니다.`, 'error'); - return prevCart; - } - - return prevCart.map(item => - item.product.id === product.id - ? { ...item, quantity: newQuantity } - : item - ); - } - - return [...prevCart, { product, quantity: 1 }]; - }); - - addNotification('장바구니에 담았습니다', 'success'); - }, [cart, addNotification, getRemainingStock]); - - const removeFromCart = useCallback((productId: string) => { - setCart(prevCart => prevCart.filter(item => item.product.id !== productId)); - }, []); - - const updateQuantity = useCallback((productId: string, newQuantity: number) => { - if (newQuantity <= 0) { - removeFromCart(productId); - return; - } - - const product = products.find(p => p.id === productId); - if (!product) return; - - const maxStock = product.stock; - if (newQuantity > maxStock) { - addNotification(`재고는 ${maxStock}개까지만 있습니다.`, 'error'); - return; - } - - setCart(prevCart => - prevCart.map(item => - item.product.id === productId - ? { ...item, quantity: newQuantity } - : item - ) - ); - }, [products, removeFromCart, addNotification, getRemainingStock]); - - const applyCoupon = useCallback((coupon: Coupon) => { - const currentTotal = calculateCartTotal().totalAfterDiscount; - - if (currentTotal < 10000 && coupon.discountType === 'percentage') { - addNotification('percentage 쿠폰은 10,000원 이상 구매 시 사용 가능합니다.', 'error'); - return; - } - - setSelectedCoupon(coupon); - addNotification('쿠폰이 적용되었습니다.', 'success'); - }, [addNotification, calculateCartTotal]); - - const completeOrder = useCallback(() => { - const orderNumber = `ORD-${Date.now()}`; - addNotification(`주문이 완료되었습니다. 주문번호: ${orderNumber}`, 'success'); - setCart([]); - setSelectedCoupon(null); - }, [addNotification]); - - const addProduct = useCallback((newProduct: Omit) => { - const product: ProductWithUI = { - ...newProduct, - id: `p${Date.now()}` - }; - setProducts(prev => [...prev, product]); - addNotification('상품이 추가되었습니다.', 'success'); - }, [addNotification]); - - const updateProduct = useCallback((productId: string, updates: Partial) => { - setProducts(prev => - prev.map(product => - product.id === productId - ? { ...product, ...updates } - : product - ) - ); - addNotification('상품이 수정되었습니다.', 'success'); - }, [addNotification]); - - const deleteProduct = useCallback((productId: string) => { - setProducts(prev => prev.filter(p => p.id !== productId)); - addNotification('상품이 삭제되었습니다.', 'success'); - }, [addNotification]); - - const addCoupon = useCallback((newCoupon: Coupon) => { - const existingCoupon = coupons.find(c => c.code === newCoupon.code); - if (existingCoupon) { - addNotification('이미 존재하는 쿠폰 코드입니다.', 'error'); - return; - } - setCoupons(prev => [...prev, newCoupon]); - addNotification('쿠폰이 추가되었습니다.', 'success'); - }, [coupons, addNotification]); - - const deleteCoupon = useCallback((couponCode: string) => { - setCoupons(prev => prev.filter(c => c.code !== couponCode)); - if (selectedCoupon?.code === couponCode) { - setSelectedCoupon(null); - } - addNotification('쿠폰이 삭제되었습니다.', 'success'); - }, [selectedCoupon, addNotification]); - - const handleProductSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (editingProduct && editingProduct !== 'new') { - updateProduct(editingProduct, productForm); - setEditingProduct(null); - } else { - addProduct({ - ...productForm, - discounts: productForm.discounts - }); - } - setProductForm({ name: '', price: 0, stock: 0, description: '', discounts: [] }); - setEditingProduct(null); - setShowProductForm(false); - }; - - const handleCouponSubmit = (e: React.FormEvent) => { - e.preventDefault(); - addCoupon(couponForm); - setCouponForm({ - name: '', - code: '', - discountType: 'amount', - discountValue: 0 - }); - setShowCouponForm(false); - }; - - const startEditProduct = (product: ProductWithUI) => { - setEditingProduct(product.id); - setProductForm({ - name: product.name, - price: product.price, - stock: product.stock, - description: product.description || '', - discounts: product.discounts || [] - }); - setShowProductForm(true); - }; - - const totals = calculateCartTotal(); - - const filteredProducts = debouncedSearchTerm - ? products.filter(product => - product.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) || - (product.description && product.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) - ) - : products; return (
- {notifications.length > 0 && ( -
- {notifications.map(notif => ( -
- {notif.message} - -
- ))} -
- )} -
-
-
-
-

SHOP

- {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} - {!isAdmin && ( -
- setSearchTerm(e.target.value)} - placeholder="상품 검색..." - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" - /> -
- )} -
- -
-
-
- -
- {isAdmin ? ( -
-
-

관리자 대시보드

-

상품과 쿠폰을 관리할 수 있습니다

-
-
- -
- - {activeTab === 'products' ? ( -
-
-
-

상품 목록

- -
-
- -
- - - - - - - - - - - - {(activeTab === 'products' ? products : products).map(product => ( - - - - - - - - ))} - -
상품명가격재고설명작업
{product.name}{formatPrice(product.price, product.id)} - 10 ? 'bg-green-100 text-green-800' : - product.stock > 0 ? 'bg-yellow-100 text-yellow-800' : - 'bg-red-100 text-red-800' - }`}> - {product.stock}개 - - {product.description || '-'} - - -
-
- {showProductForm && ( -
-
-

- {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} -

-
-
- - setProductForm({ ...productForm, name: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - required - /> -
-
- - setProductForm({ ...productForm, description: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - /> -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ ...productForm, price: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = e.target.value; - if (value === '') { - setProductForm({ ...productForm, price: 0 }); - } else if (parseInt(value) < 0) { - addNotification('가격은 0보다 커야 합니다', 'error'); - setProductForm({ ...productForm, price: 0 }); - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setProductForm({ ...productForm, stock: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = e.target.value; - if (value === '') { - setProductForm({ ...productForm, stock: 0 }); - } else if (parseInt(value) < 0) { - addNotification('재고는 0보다 커야 합니다', 'error'); - setProductForm({ ...productForm, stock: 0 }); - } else if (parseInt(value) > 9999) { - addNotification('재고는 9999개를 초과할 수 없습니다', 'error'); - setProductForm({ ...productForm, stock: 9999 }); - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" - placeholder="숫자만 입력" - required - /> -
-
-
- -
- {productForm.discounts.map((discount, index) => ( -
- { - const newDiscounts = [...productForm.discounts]; - newDiscounts[index].quantity = parseInt(e.target.value) || 0; - setProductForm({ ...productForm, discounts: newDiscounts }); - }} - className="w-20 px-2 py-1 border rounded" - min="1" - placeholder="수량" - /> - 개 이상 구매 시 - { - const newDiscounts = [...productForm.discounts]; - newDiscounts[index].rate = (parseInt(e.target.value) || 0) / 100; - setProductForm({ ...productForm, discounts: newDiscounts }); - }} - className="w-16 px-2 py-1 border rounded" - min="0" - max="100" - placeholder="%" - /> - % 할인 - -
- ))} - -
-
- -
- - -
-
-
- )} -
- ) : ( -
-
-

쿠폰 관리

-
-
-
- {coupons.map(coupon => ( -
-
-
-

{coupon.name}

-

{coupon.code}

-
- - {coupon.discountType === 'amount' - ? `${coupon.discountValue.toLocaleString()}원 할인` - : `${coupon.discountValue}% 할인`} - -
-
- -
-
- ))} - -
- -
-
- - {showCouponForm && ( -
-
-

새 쿠폰 생성

-
-
- - setCouponForm({ ...couponForm, name: e.target.value })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder="신규 가입 쿠폰" - required - /> -
-
- - setCouponForm({ ...couponForm, code: e.target.value.toUpperCase() })} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" - placeholder="WELCOME2024" - required - /> -
-
- - -
-
- - { - const value = e.target.value; - if (value === '' || /^\d+$/.test(value)) { - setCouponForm({ ...couponForm, discountValue: value === '' ? 0 : parseInt(value) }); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value) || 0; - if (couponForm.discountType === 'percentage') { - if (value > 100) { - addNotification('할인율은 100%를 초과할 수 없습니다', 'error'); - setCouponForm({ ...couponForm, discountValue: 100 }); - } else if (value < 0) { - setCouponForm({ ...couponForm, discountValue: 0 }); - } - } else { - if (value > 100000) { - addNotification('할인 금액은 100,000원을 초과할 수 없습니다', 'error'); - setCouponForm({ ...couponForm, discountValue: 100000 }); - } else if (value < 0) { - setCouponForm({ ...couponForm, discountValue: 0 }); - } - } - }} - className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" - placeholder={couponForm.discountType === 'amount' ? '5000' : '10'} - required - /> -
-
-
- - -
-
-
- )} -
-
- )} -
- ) : ( -
-
- {/* 상품 목록 */} -
-
-

전체 상품

-
- 총 {products.length}개 상품 -
-
- {filteredProducts.length === 0 ? ( -
-

"{debouncedSearchTerm}"에 대한 검색 결과가 없습니다.

-
- ) : ( -
- {filteredProducts.map(product => { - const remainingStock = getRemainingStock(product); - - return ( -
- {/* 상품 이미지 영역 (placeholder) */} -
-
- - - -
- {product.isRecommended && ( - - BEST - - )} - {product.discounts.length > 0 && ( - - ~{Math.max(...product.discounts.map(d => d.rate)) * 100}% - - )} -
- - {/* 상품 정보 */} -
-

{product.name}

- {product.description && ( -

{product.description}

- )} - - {/* 가격 정보 */} -
-

{formatPrice(product.price, product.id)}

- {product.discounts.length > 0 && ( -

- {product.discounts[0].quantity}개 이상 구매시 할인 {product.discounts[0].rate * 100}% -

- )} -
- - {/* 재고 상태 */} -
- {remainingStock <= 5 && remainingStock > 0 && ( -

품절임박! {remainingStock}개 남음

- )} - {remainingStock > 5 && ( -

재고 {remainingStock}개

- )} -
- - {/* 장바구니 버튼 */} - -
-
- ); - })} -
- )} -
-
- -
-
-
-

- - - - 장바구니 -

- {cart.length === 0 ? ( -
- - - -

장바구니가 비어있습니다

-
- ) : ( -
- {cart.map(item => { - const itemTotal = calculateItemTotal(item); - const originalPrice = item.product.price * item.quantity; - const hasDiscount = itemTotal < originalPrice; - const discountRate = hasDiscount ? Math.round((1 - itemTotal / originalPrice) * 100) : 0; - - return ( -
-
-

{item.product.name}

- -
-
-
- - {item.quantity} - -
-
- {hasDiscount && ( - -{discountRate}% - )} -

- {Math.round(itemTotal).toLocaleString()}원 -

-
-
-
- ); - })} -
- )} -
- - {cart.length > 0 && ( - <> -
-
-

쿠폰 할인

- -
- {coupons.length > 0 && ( - - )} -
- -
-

결제 정보

-
-
- 상품 금액 - {totals.totalBeforeDiscount.toLocaleString()}원 -
- {totals.totalBeforeDiscount - totals.totalAfterDiscount > 0 && ( -
- 할인 금액 - -{(totals.totalBeforeDiscount - totals.totalAfterDiscount).toLocaleString()}원 -
- )} -
- 결제 예정 금액 - {totals.totalAfterDiscount.toLocaleString()}원 -
-
- - - -
-

* 실제 결제는 이루어지지 않습니다

-
-
- - )} -
-
-
- )} -
+ + {isAdmin ? : }
- ); -}; + ) +} -export default App; \ No newline at end of file +export default App diff --git a/src/advanced/components/AdminPage.tsx b/src/advanced/components/AdminPage.tsx new file mode 100644 index 00000000..22bb3896 --- /dev/null +++ b/src/advanced/components/AdminPage.tsx @@ -0,0 +1,197 @@ +// 힌트: +// 1. 탭 UI로 상품 관리와 쿠폰 관리 분리 +// 2. 상품 추가/수정/삭제 기능 +// 3. 쿠폰 생성 기능 +// 4. 할인 규칙 설정 +// +// 필요한 hooks: +// - useProducts: 상품 CRUD +// - useCoupons: 쿠폰 CRUD + +import { CartItem, Coupon } from '../../types' +import { ProductWithUI } from '../types' +import { useCouponForm, useProductForm } from '../hooks/useForm' +import { useState } from 'react' +import { AdminHeader } from './ui/admin/AdminHeader' +import { ProductFom } from './ui/admin/ProductFom' +import { CouponForm } from './ui/admin/CouponForm' +import { CouponList } from './ui/admin/CouponList' +import { ProductAccordion } from './ui/admin/ProductAccordion' + +export function AdminPage({ + isAdmin, + cart, + setIsAdmin, + products, + deleteProduct, + addNotification, + coupons, + deleteCoupon, + updateProduct, + addProduct, + addCoupon, + getRemainingStock, +}: { + isAdmin: boolean + cart: CartItem[] + setIsAdmin: React.Dispatch> + products: ProductWithUI[] + deleteProduct: (productId: string) => void + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void + coupons: Coupon[] + deleteCoupon: (couponCode: string) => void + updateProduct: (productId: string, updates: Partial) => void + addProduct: (newProduct: Omit) => void + addCoupon: (newCoupon: Coupon) => void + getRemainingStock: (product: ProductWithUI) => number +}) { + // TODO: 구현 + const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') + const { + couponForm, + handleEditCouponForm, + showCouponForm, + setShowCouponForm, + handleCouponSubmit, + handleDiscountValueValidation, + } = useCouponForm(addCoupon, addNotification) + const { + productForm, + startEditProduct, + handleProductSubmit, + showProductForm, + editingProduct, + handleAddOrCloseProductForm, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handlePriceValidation, + handleStockValidation, + } = useProductForm(addProduct, updateProduct, addNotification) + + const productFormProps = { + productForm, + handleProductSubmit, + editingProduct, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handleAddOrCloseProductForm, + handlePriceValidation, + handleStockValidation, + } + + const productAccordionProps = { + products, + cart, + startEditProduct, + deleteProduct, + getRemainingStock, + } + + const cuponFormProps = { + couponForm, + handleCouponSubmit, + setShowCouponForm, + handleEditCouponForm, + handleDiscountValueValidation, + } + + return ( + <> + +
+
+
+

+ 관리자 대시보드 +

+

+ 상품과 쿠폰을 관리할 수 있습니다 +

+
+
+ +
+ + {activeTab === 'products' ? ( +
+
+
+

상품 목록

+ +
+
+ + + {showProductForm && } +
+ ) : ( +
+
+

쿠폰 관리

+
+
+
+ +
+ +
+
+ + {showCouponForm && } +
+
+ )} +
+
+ + ) +} diff --git a/src/advanced/components/CartPage.tsx b/src/advanced/components/CartPage.tsx new file mode 100644 index 00000000..daaf5461 --- /dev/null +++ b/src/advanced/components/CartPage.tsx @@ -0,0 +1,131 @@ +// TODO: 장바구니 페이지 컴포넌트 +// 힌트: +// 1. 상품 목록 표시 (검색 기능 포함) +// 2. 장바구니 관리 +// 3. 쿠폰 적용 +// 4. 주문 처리 +// +// 필요한 hooks: +// - useProducts: 상품 목록 관리 +// - useCart: 장바구니 상태 관리 +// - useCoupons: 쿠폰 목록 관리 +// - useDebounce: 검색어 디바운싱 + +import { useState } from 'react' +import { CartItem, Coupon } from '../../types' +import { ProductWithUI } from '../types' +import { CartHeader } from './ui/cart/CartHeader' +import { ProductList } from './ui/cart/ProductList' +import { Cart } from './ui/cart/Cart' +import { useDebounce } from '../utils/hooks/useDebounce' + +export function CartPage({ + isAdmin, + setIsAdmin, + products, + cart, + coupons, + totals, + selectedCoupon, + addToCart, + removeFromCart, + updateQuantity, + completeOrder, + handleSelectCoupon, + getFilteredProducts, + getRemainingStock, + calculateTotal, + totalItemCount, +}: { + isAdmin: boolean + setIsAdmin: React.Dispatch> + products: ProductWithUI[] + cart: CartItem[] + coupons: Coupon[] + totals: { + totalBeforeDiscount: number + totalAfterDiscount: number + } + selectedCoupon: Coupon | null + addToCart: (product: ProductWithUI) => void + removeFromCart: (productId: string) => void + updateQuantity: ( + productId: string, + newQuantity: number, + products: ProductWithUI[], + ) => void + completeOrder: () => void + handleSelectCoupon: ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => void + getFilteredProducts: (searchTerm: string) => ProductWithUI[] + getRemainingStock: (product: ProductWithUI) => number + calculateTotal: (item: CartItem) => number + totalItemCount: number +}) { + // TODO: 구현 + const [searchTerm, setSearchTerm] = useState('') + const debouncedSearchTerm = useDebounce(searchTerm, 500) + const filteredProducts = getFilteredProducts(debouncedSearchTerm) + + const headerState = { + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, + } + + const cartProps = { + cart, + coupons, + products, + selectedCoupon, + removeFromCart, + updateQuantity, + totals, + completeOrder, + handleSelectCoupon, + calculateTotal, + } + + return ( + <> + +
+
+
+ {/* 상품 목록 */} +
+
+

+ 전체 상품 +

+
+ 총 {products.length}개 상품 +
+
+ {filteredProducts.length === 0 ? ( +
+

+ "{debouncedSearchTerm}"에 대한 검색 결과가 없습니다. +

+
+ ) : ( + + )} +
+
+ + +
+
+ + ) +} diff --git a/src/advanced/components/ui/Notifications.tsx b/src/advanced/components/ui/Notifications.tsx new file mode 100644 index 00000000..d9c1eb1d --- /dev/null +++ b/src/advanced/components/ui/Notifications.tsx @@ -0,0 +1,54 @@ +import { Notification } from '../../types' + +export const Notifications = ({ + notifications, + setNotifications, +}: { + notifications: Notification[] + setNotifications: React.Dispatch> +}) => { + return ( + <> + {notifications.length > 0 && ( +
+ {notifications.map((notif) => ( +
+ {notif.message} + +
+ ))} +
+ )} + + ) +} diff --git a/src/advanced/components/ui/admin/AdminHeader.tsx b/src/advanced/components/ui/admin/AdminHeader.tsx new file mode 100644 index 00000000..c1535b1b --- /dev/null +++ b/src/advanced/components/ui/admin/AdminHeader.tsx @@ -0,0 +1,27 @@ +export const AdminHeader = ({ + isAdmin, + setIsAdmin, +}: { + isAdmin: boolean + setIsAdmin: React.Dispatch> +}) => { + return ( +
+
+
+
+

SHOP

+
+ +
+
+
+ ) +} diff --git a/src/advanced/components/ui/admin/CouponForm.tsx b/src/advanced/components/ui/admin/CouponForm.tsx new file mode 100644 index 00000000..be350f62 --- /dev/null +++ b/src/advanced/components/ui/admin/CouponForm.tsx @@ -0,0 +1,99 @@ +import { FocusEvent } from 'react' +import { Coupon } from '../../../../types' + +export function CouponForm({ + couponForm, + handleCouponSubmit, + setShowCouponForm, + handleEditCouponForm, + handleDiscountValueValidation, +}: { + couponForm: Coupon + setShowCouponForm: React.Dispatch> + handleCouponSubmit: (e: React.FormEvent) => void + handleEditCouponForm: ( + e: React.ChangeEvent, + key: string, + ) => void + handleDiscountValueValidation: (e: FocusEvent) => void +}) { + return ( +
+
+

새 쿠폰 생성

+
+
+ + handleEditCouponForm(e, 'name')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder="신규 가입 쿠폰" + required + /> +
+
+ + handleEditCouponForm(e, 'code')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm font-mono" + placeholder="WELCOME2024" + required + /> +
+
+ + +
+
+ + handleEditCouponForm(e, 'discountValue')} + onBlur={handleDiscountValueValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border text-sm" + placeholder={couponForm.discountType === 'amount' ? '5000' : '10'} + required + /> +
+
+
+ + +
+
+
+ ) +} diff --git a/src/advanced/components/ui/admin/CouponList.tsx b/src/advanced/components/ui/admin/CouponList.tsx new file mode 100644 index 00000000..cfd64353 --- /dev/null +++ b/src/advanced/components/ui/admin/CouponList.tsx @@ -0,0 +1,54 @@ +import { Coupon } from '../../../../types' + +export const CouponList = ({ + coupons, + deleteCoupon, +}: { + coupons: Coupon[] + deleteCoupon: (couponCode: string) => void +}) => { + return ( + <> + {coupons.map((coupon) => ( +
+
+
+

{coupon.name}

+

+ {coupon.code} +

+
+ + {coupon.discountType === 'amount' + ? `${coupon.discountValue.toLocaleString()}원 할인` + : `${coupon.discountValue}% 할인`} + +
+
+ +
+
+ ))} + + ) +} diff --git a/src/advanced/components/ui/admin/ProductAccordion.tsx b/src/advanced/components/ui/admin/ProductAccordion.tsx new file mode 100644 index 00000000..e55146cb --- /dev/null +++ b/src/advanced/components/ui/admin/ProductAccordion.tsx @@ -0,0 +1,88 @@ +import { CartItem } from '../../../../types' +import { ProductWithUI } from '../../../types' +import { formatPrice } from '../../../utils/formatters' + +export const ProductAccordion = ({ + products, + startEditProduct, + deleteProduct, + getRemainingStock, +}: { + products: ProductWithUI[] + cart: CartItem[] + startEditProduct: (product: ProductWithUI) => void + deleteProduct: (productId: string) => void + getRemainingStock: (product: ProductWithUI) => number +}) => { + return ( +
+ + + + + + + + + + + + {products.map((product) => ( + + + + + + + + ))} + +
+ 상품명 + + 가격 + + 재고 + + 설명 + + 작업 +
+ {product.name} + + {formatPrice( + product.price, + true, + getRemainingStock(product) <= 0, + )} + + 10 + ? 'bg-green-100 text-green-800' + : product.stock > 0 + ? 'bg-yellow-100 text-yellow-800' + : 'bg-red-100 text-red-800' + }`} + > + {product.stock}개 + + + {product.description || '-'} + + + +
+
+ ) +} diff --git a/src/advanced/components/ui/admin/ProductFom.tsx b/src/advanced/components/ui/admin/ProductFom.tsx new file mode 100644 index 00000000..8357215a --- /dev/null +++ b/src/advanced/components/ui/admin/ProductFom.tsx @@ -0,0 +1,172 @@ +import React, { FocusEvent } from 'react' +import { ProductForm } from '../../../../types' +import { MAX_DISCOUNT_RATE } from '../../../constants' + +export function ProductFom({ + productForm, + handleProductSubmit, + editingProduct, + handleAddDiscount, + handleDeleteDiscount, + handleEditProuctForm, + handleAddOrCloseProductForm, + handlePriceValidation, + handleStockValidation, +}: { + productForm: ProductForm + handleProductSubmit: (e: React.FormEvent) => void + editingProduct: string | null + handleAddDiscount: () => void + handleDeleteDiscount: (index: number) => void + handleEditProuctForm: ( + e: React.ChangeEvent, + key: string, + index?: number, + ) => void + handleAddOrCloseProductForm: ( + type: string | null, + isShowProductForm: boolean, + ) => void + handlePriceValidation: (e: FocusEvent) => void + handleStockValidation: (e: FocusEvent) => void +}) { + return ( + <> +
+
+

+ {editingProduct === 'new' ? '새 상품 추가' : '상품 수정'} +

+
+
+ + handleEditProuctForm(e, 'name')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + required + /> +
+
+ + handleEditProuctForm(e, 'description')} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + /> +
+
+ + handleEditProuctForm(e, 'price')} + onBlur={handlePriceValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+ + handleEditProuctForm(e, 'stock')} + onBlur={handleStockValidation} + className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2 border" + placeholder="숫자만 입력" + required + /> +
+
+
+ +
+ {productForm.discounts.map((discount, index) => ( +
+ handleEditProuctForm(e, 'quantity', index)} + className="w-20 px-2 py-1 border rounded" + min="1" + placeholder="수량" + /> + 개 이상 구매 시 + handleEditProuctForm(e, 'rate', index)} + className="w-16 px-2 py-1 border rounded" + min="0" + max={MAX_DISCOUNT_RATE} + placeholder="%" + /> + % 할인 + +
+ ))} + +
+
+ +
+ + +
+
+
+ + ) +} diff --git a/src/advanced/components/ui/cart/Cart.tsx b/src/advanced/components/ui/cart/Cart.tsx new file mode 100644 index 00000000..f1f0270c --- /dev/null +++ b/src/advanced/components/ui/cart/Cart.tsx @@ -0,0 +1,239 @@ +import { CartItem, Coupon } from '../../../../types' +import { MAX_DISCOUNT_RATE } from '../../../constants' +import { ProductWithUI } from '../../../types' + +export const Cart = ({ + cart, + coupons, + products, + selectedCoupon, + removeFromCart, + updateQuantity, + totals, + completeOrder, + handleSelectCoupon, + calculateTotal, +}: { + cart: CartItem[] + coupons: Coupon[] + products: ProductWithUI[] + selectedCoupon: Coupon | null + totals: { + totalBeforeDiscount: number + totalAfterDiscount: number + } + removeFromCart: (productId: string) => void + updateQuantity: ( + productId: string, + newQuantity: number, + products: ProductWithUI[], + ) => void + completeOrder: () => void + handleSelectCoupon: ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => void + calculateTotal: (item: CartItem) => number +}) => { + return ( +
+
+
+

+ + + + 장바구니 +

+ {cart.length === 0 ? ( +
+ + + +

장바구니가 비어있습니다

+
+ ) : ( +
+ {cart.map((item) => { + const itemTotal = calculateTotal(item) + const originalPrice = item.product.price * item.quantity + const hasDiscount = itemTotal < originalPrice + const discountRate = hasDiscount + ? Math.round( + (1 - itemTotal / originalPrice) * MAX_DISCOUNT_RATE, + ) + : 0 + + return ( +
+
+

+ {item.product.name} +

+ +
+
+
+ + + {item.quantity} + + +
+
+ {hasDiscount && ( + + -{discountRate}% + + )} +

+ {Math.round(itemTotal).toLocaleString()}원 +

+
+
+
+ ) + })} +
+ )} +
+ + {cart.length > 0 && ( + <> +
+
+

+ 쿠폰 할인 +

+ +
+ {coupons.length > 0 && ( + + )} +
+ +
+

결제 정보

+
+
+ 상품 금액 + + {totals.totalBeforeDiscount.toLocaleString()}원 + +
+ {totals.totalBeforeDiscount - totals.totalAfterDiscount > 0 && ( +
+ 할인 금액 + + - + {( + totals.totalBeforeDiscount - totals.totalAfterDiscount + ).toLocaleString()} + 원 + +
+ )} +
+ 결제 예정 금액 + + {totals.totalAfterDiscount.toLocaleString()}원 + +
+
+ + + +
+

* 실제 결제는 이루어지지 않습니다

+
+
+ + )} +
+
+ ) +} diff --git a/src/advanced/components/ui/cart/CartHeader.tsx b/src/advanced/components/ui/cart/CartHeader.tsx new file mode 100644 index 00000000..013d5aad --- /dev/null +++ b/src/advanced/components/ui/cart/CartHeader.tsx @@ -0,0 +1,69 @@ +import { CartItem } from '../../../../types' + +export const CartHeader = ({ + isAdmin, + searchTerm, + cart, + totalItemCount, + setSearchTerm, + setIsAdmin, +}: { + isAdmin: boolean + searchTerm: string + cart: CartItem[] + totalItemCount: number + setSearchTerm: React.Dispatch> + setIsAdmin: React.Dispatch> +}) => { + return ( + <> +
+
+
+
+

SHOP

+ {/* 검색창 - 안티패턴: 검색 로직이 컴포넌트에 직접 포함 */} +
+ setSearchTerm(e.target.value)} + placeholder="상품 검색..." + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+ +
+
+
+ + ) +} diff --git a/src/advanced/components/ui/cart/ProductList.tsx b/src/advanced/components/ui/cart/ProductList.tsx new file mode 100644 index 00000000..329dd6c2 --- /dev/null +++ b/src/advanced/components/ui/cart/ProductList.tsx @@ -0,0 +1,115 @@ +import { LOW_STOCK_THRESHOLD, MAX_DISCOUNT_RATE } from '../../../constants' +import { ProductWithUI } from '../../../types' +import { formatPrice } from '../../../utils/formatters' + +export function ProductList({ + filteredProducts, + addToCart, + getRemainingStock, +}: { + filteredProducts: ProductWithUI[] + addToCart: (product: ProductWithUI) => void + getRemainingStock: (product: ProductWithUI) => number +}) { + return ( +
+ {filteredProducts.map((product) => { + const remainingStock = getRemainingStock(product) + + return ( +
+ {/* 상품 이미지 영역 (placeholder) */} +
+
+ + + +
+ {product.isRecommended && ( + + BEST + + )} + {product.discounts.length > 0 && ( + + ~ + {Math.max(...product.discounts.map((d) => d.rate)) * + MAX_DISCOUNT_RATE} + % + + )} +
+ + {/* 상품 정보 */} +
+

{product.name}

+ {product.description && ( +

+ {product.description} +

+ )} + + {/* 가격 정보 */} +
+

+ {formatPrice( + product.price, + false, + getRemainingStock(product) <= 0, + )} +

+ {product.discounts.length > 0 && ( +

+ {product.discounts[0].quantity}개 이상 구매시 할인{' '} + {product.discounts[0].rate * MAX_DISCOUNT_RATE}% +

+ )} +
+ + {/* 재고 상태 */} +
+ {remainingStock <= LOW_STOCK_THRESHOLD && + remainingStock > 0 && ( +

+ 품절임박! {remainingStock}개 남음 +

+ )} + {remainingStock > LOW_STOCK_THRESHOLD && ( +

+ 재고 {remainingStock}개 +

+ )} +
+ + {/* 장바구니 버튼 */} + +
+
+ ) + })} +
+ ) +} diff --git a/src/advanced/constants/index.ts b/src/advanced/constants/index.ts new file mode 100644 index 00000000..a3f6fb3a --- /dev/null +++ b/src/advanced/constants/index.ts @@ -0,0 +1,75 @@ +// TODO: 초기 데이터 상수 +// 정의할 상수들: +// - initialProducts: 초기 상품 목록 (상품1, 상품2, 상품3 + 설명 필드 포함) +// - initialCoupons: 초기 쿠폰 목록 (5000원 할인, 10% 할인) +// +// 참고: origin/App.tsx의 초기 데이터 구조를 참조 + +import { Coupon } from '../../types' +import { ProductWithUI } from '../types' + +// TODO: 구현 +// 재고 관련 상수 +export const LOW_STOCK_THRESHOLD = 5 + +// 쿠폰 관련 상수 +export const MIN_COUPON_AMOUNT = 10000 + +// 할인율 관련 상수 +export const MAX_DISCOUNT_RATE = 100 + +// 재고 관련 상수 +export const MAX_STOCK_LIMIT = 9999 + +// 할인 금액 관련 상수 +export const MAX_DISCOUNT_AMOUNT = 100000 + +// 초기 데이터 +export const initialProducts: ProductWithUI[] = [ + { + id: 'p1', + name: '상품1', + price: MIN_COUPON_AMOUNT, + stock: 20, + discounts: [ + { quantity: 10, rate: 0.1 }, + { quantity: 20, rate: 0.2 }, + ], + description: '최고급 품질의 프리미엄 상품입니다.', + }, + { + id: 'p2', + name: '상품2', + price: 20000, + stock: 20, + discounts: [{ quantity: 10, rate: 0.15 }], + description: '다양한 기능을 갖춘 실용적인 상품입니다.', + isRecommended: true, + }, + { + id: 'p3', + name: '상품3', + price: 30000, + stock: 20, + discounts: [ + { quantity: 10, rate: 0.2 }, + { quantity: 30, rate: 0.25 }, + ], + description: '대용량과 고성능을 자랑하는 상품입니다.', + }, +] + +export const initialCoupons: Coupon[] = [ + { + name: '5000원 할인', + code: 'AMOUNT5000', + discountType: 'amount', + discountValue: 5000, + }, + { + name: '10% 할인', + code: 'PERCENT10', + discountType: 'percentage', + discountValue: 10, + }, +] diff --git a/src/advanced/hooks/useCart.ts b/src/advanced/hooks/useCart.ts new file mode 100644 index 00000000..cf4f2b65 --- /dev/null +++ b/src/advanced/hooks/useCart.ts @@ -0,0 +1,188 @@ +// TODO: 장바구니 관리 Hook +// 힌트: +// 1. 장바구니 상태 관리 (localStorage 연동) +// 2. 상품 추가/삭제/수량 변경 +// 3. 쿠폰 적용 +// 4. 총액 계산 +// 5. 재고 확인 +// +// 사용할 모델 함수: +// - cartModel.addItemToCart +// - cartModel.removeItemFromCart +// - cartModel.updateCartItemQuantity +// - cartModel.calculateCartTotal +// - cartModel.getRemainingStock +// +// 반환할 값: +// - cart: 장바구니 아이템 배열 +// - selectedCoupon: 선택된 쿠폰 +// - addToCart: 상품 추가 함수 +// - removeFromCart: 상품 제거 함수 +// - updateQuantity: 수량 변경 함수 +// - applyCoupon: 쿠폰 적용 함수 +// - calculateTotal: 총액 계산 함수 +// - getRemainingStock: 재고 확인 함수 +// - clearCart: 장바구니 비우기 함수 + +import { useCallback, useEffect, useState } from 'react' +import { CartItem, Coupon } from '../../types' +import { ProductWithUI } from '../types' +import { + getRemainingStock as _getRemainingStock, + calculateItemTotal, + calculateCartTotal as _calculateCartTotal, + addItemToCart, +} from '../models/cart' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' +import { MIN_COUPON_AMOUNT } from '../constants' + +export function useCart( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + // TODO: 구현 + + const [selectedCoupon, setSelectedCoupon] = useState(null) + const [cart, setCart] = useLocalStorage('cart', []) + + const getRemainingStock = useCallback( + (product: ProductWithUI) => { + return _getRemainingStock(product, cart) + }, + [cart], + ) + + const calculateTotal = useCallback( + (item: CartItem) => { + return calculateItemTotal(item, cart) + }, + [cart], + ) + + const addToCart = useCallback( + (product: ProductWithUI) => { + const remainingStock = _getRemainingStock(product, cart) + if (remainingStock <= 0) { + addNotification('재고가 부족합니다!', 'error') + return + } + + const { carts, message, type } = addItemToCart(cart, product) + + setCart(carts) + addNotification(message, type) + }, + [addNotification, cart, setCart], + ) + + const removeFromCart = useCallback( + (productId: string) => { + setCart((prevCart) => + prevCart.filter((item) => item.product.id !== productId), + ) + }, + [setCart], + ) + + const updateQuantity = useCallback( + (productId: string, newQuantity: number, products: ProductWithUI[]) => { + if (newQuantity <= 0) { + removeFromCart(productId) + return + } + + const product = products.find((p) => p.id === productId) + if (!product) return + + const maxStock = product.stock + if (newQuantity > maxStock) { + addNotification(`재고는 ${maxStock}개까지만 있습니다.`, 'error') + return + } + + setCart((prevCart) => + prevCart.map((item) => + item.product.id === productId + ? { ...item, quantity: newQuantity } + : item, + ), + ) + }, + [setCart, removeFromCart, addNotification], + ) + + const calculateCartTotal = useCallback((): { + totalBeforeDiscount: number + totalAfterDiscount: number + } => { + return _calculateCartTotal(cart, selectedCoupon) + }, [cart, selectedCoupon]) + + const applyCoupon = useCallback( + (coupon: Coupon) => { + const currentTotal = _calculateCartTotal( + cart, + selectedCoupon, + ).totalAfterDiscount + + if ( + currentTotal < MIN_COUPON_AMOUNT && + coupon.discountType === 'percentage' + ) { + addNotification( + 'percentage 쿠폰은 10,000원 이상 구매 시 사용 가능합니다.', + 'error', + ) + return + } + + setSelectedCoupon(coupon) + addNotification('쿠폰이 적용되었습니다.', 'success') + }, + [addNotification, cart, selectedCoupon], + ) + + const completeOrder = useCallback(() => { + const orderNumber = `ORD-${Date.now()}` + addNotification( + `주문이 완료되었습니다. 주문번호: ${orderNumber}`, + 'success', + ) + setCart([]) + setSelectedCoupon(null) + }, [addNotification, setCart, setSelectedCoupon]) + + const handleSelectCoupon = ( + e: React.ChangeEvent, + coupons: Coupon[], + ) => { + const coupon = coupons.find((c) => c.code === e.target.value) + if (coupon) applyCoupon(coupon) + else setSelectedCoupon(null) + } + + const [totalItemCount, setTotalItemCount] = useState(0) + + useEffect(() => { + const count = cart.reduce((sum, item) => sum + item.quantity, 0) + setTotalItemCount(count) + }, [cart]) + + return { + cart, + addToCart, + removeFromCart, + updateQuantity, + selectedCoupon, + setSelectedCoupon, + applyCoupon, + calculateCartTotal, + calculateTotal, + completeOrder, + getRemainingStock, + handleSelectCoupon, + totalItemCount, + } +} diff --git a/src/advanced/hooks/useCoupons.ts b/src/advanced/hooks/useCoupons.ts new file mode 100644 index 00000000..be549be4 --- /dev/null +++ b/src/advanced/hooks/useCoupons.ts @@ -0,0 +1,60 @@ +// TODO: 쿠폰 관리 Hook +// 힌트: +// 1. 쿠폰 목록 상태 관리 (localStorage 연동 고려) +// 2. 쿠폰 추가/삭제 +// +// 반환할 값: +// - coupons: 쿠폰 배열 +// - addCoupon: 새 쿠폰 추가 +// - removeCoupon: 쿠폰 삭제 + +import { useCallback } from 'react' +import { initialCoupons } from '../constants' +import { Coupon } from '../../types' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' + +export function useCoupons( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, + selectedCoupon: Coupon | null, + setSelectedCoupon: React.Dispatch>, +) { + // TODO: 구현 + + const [coupons, setCoupons] = useLocalStorage( + 'coupons', + initialCoupons, + ) + + const addCoupon = useCallback( + (newCoupon: Coupon) => { + const existingCoupon = coupons.find((c) => c.code === newCoupon.code) + if (existingCoupon) { + addNotification('이미 존재하는 쿠폰 코드입니다.', 'error') + return + } + setCoupons((prev) => [...prev, newCoupon]) + addNotification('쿠폰이 추가되었습니다.', 'success') + }, + [coupons, setCoupons, addNotification], + ) + + const deleteCoupon = useCallback( + (couponCode: string) => { + setCoupons((prev) => prev.filter((c) => c.code !== couponCode)) + if (selectedCoupon?.code === couponCode) { + setSelectedCoupon(null) + } + addNotification('쿠폰이 삭제되었습니다.', 'success') + }, + [setCoupons, selectedCoupon?.code, addNotification, setSelectedCoupon], + ) + + return { + coupons, + addCoupon, + deleteCoupon, + } +} diff --git a/src/advanced/hooks/useForm.ts b/src/advanced/hooks/useForm.ts new file mode 100644 index 00000000..88d30a06 --- /dev/null +++ b/src/advanced/hooks/useForm.ts @@ -0,0 +1,258 @@ +import { useState, FocusEvent } from 'react' +import { ProductWithUI } from '../types' +import { Coupon, Discount, ProductForm } from '../../types' +import { isValidStrNumber } from '../utils/validators' +import { + MAX_DISCOUNT_AMOUNT, + MAX_DISCOUNT_RATE, + MAX_STOCK_LIMIT, +} from '../constants' + +export function useCouponForm( + addCoupon: (newCoupon: Coupon) => void, + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + const [showCouponForm, setShowCouponForm] = useState(false) + const [couponForm, setCouponForm] = useState({ + name: '', + code: '', + discountType: 'amount' as 'amount' | 'percentage', + discountValue: 0, + }) + + const handleEditCouponForm = ( + e: React.ChangeEvent, + key: string, + ) => { + const { value } = e.target + if (key === 'discountValue') { + if (value === '' || isValidStrNumber(value)) { + setCouponForm((prev) => ({ + ...prev, + [key]: value === '' ? 0 : parseInt(value), + })) + } + } else { + setCouponForm((prev) => ({ + ...prev, + [key]: key === 'code' ? value.toUpperCase() : value, + })) + } + } + + const handleCouponSubmit = (e: React.FormEvent) => { + e.preventDefault() + addCoupon(couponForm) + setCouponForm({ + name: '', + code: '', + discountType: 'amount', + discountValue: 0, + }) + setShowCouponForm(false) + } + + const handleDiscountValueValidation = (e: FocusEvent) => { + const value = parseInt(e.target.value) || 0 + + if (couponForm.discountType === 'percentage') { + if (value > MAX_DISCOUNT_RATE) { + addNotification('할인율은 100%를 초과할 수 없습니다', 'error') + setCouponForm({ + ...couponForm, + discountValue: MAX_DISCOUNT_RATE, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } else { + if (value > MAX_DISCOUNT_AMOUNT) { + addNotification('할인 금액은 100,000원을 초과할 수 없습니다', 'error') + setCouponForm({ + ...couponForm, + discountValue: MAX_DISCOUNT_AMOUNT, + }) + } else if (value < 0) { + setCouponForm({ + ...couponForm, + discountValue: 0, + }) + } + } + } + + return { + couponForm, + setCouponForm, + showCouponForm, + setShowCouponForm, + handleCouponSubmit, + handleEditCouponForm, + handleDiscountValueValidation, + } +} + +export function useProductForm( + addProduct: (newProduct: Omit) => void, + updateProduct: (productId: string, updates: Partial) => void, + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + const [showProductForm, setShowProductForm] = useState(false) + const [editingProduct, setEditingProduct] = useState(null) + const [productForm, setProductForm] = useState({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + + const startEditProduct = (product: ProductWithUI) => { + setEditingProduct(product.id) + setProductForm({ + name: product.name, + price: product.price, + stock: product.stock, + description: product.description || '', + discounts: product.discounts || [], + }) + setShowProductForm(true) + } + + const handleAddOrCloseProductForm = ( + type: string | null, + isShowProductForm: boolean, + ) => { + setEditingProduct(type) + setProductForm({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + setShowProductForm(isShowProductForm) + } + + const handleProductSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (editingProduct && editingProduct !== 'new') { + updateProduct(editingProduct, productForm) + setEditingProduct(null) + } else { + addProduct({ + ...productForm, + discounts: productForm.discounts, + }) + } + setProductForm({ + name: '', + price: 0, + stock: 0, + description: '', + discounts: [], + }) + setEditingProduct(null) + setShowProductForm(false) + } + + const handleEditProuctForm = ( + e: React.ChangeEvent, + key: string, + index?: number, + ) => { + const { value } = e.target + + if (['name', 'description'].includes(key)) { + setProductForm((prev) => ({ + ...prev, + [key]: value, + })) + } + + if ( + ['price', 'stock'].includes(key) && + (value === '' || isValidStrNumber(value)) + ) { + setProductForm({ + ...productForm, + [key]: value === '' ? 0 : parseInt(value), + }) + } + + if (index && ['quantity', 'rate'].includes(key)) { + const newDiscounts = [...productForm.discounts] + const value = parseInt(e.target.value) || 0 + newDiscounts[index][key as keyof Discount] = + value / (key === 'rate' ? MAX_DISCOUNT_RATE : 1) + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + } + } + + const handleDeleteDiscount = (index: number) => { + const newDiscounts = productForm.discounts.filter((_, i) => i !== index) + setProductForm({ + ...productForm, + discounts: newDiscounts, + }) + } + + const handleAddDiscount = () => { + setProductForm({ + ...productForm, + discounts: [...productForm.discounts, { quantity: 10, rate: 0.1 }], + }) + } + + const handlePriceValidation = (e: FocusEvent) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, price: 0 }) + } else if (parseInt(value) < 0) { + addNotification('가격은 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, price: 0 }) + } + } + + const handleStockValidation = (e: FocusEvent) => { + const value = e.target.value + if (value === '') { + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) < 0) { + addNotification('재고는 0보다 커야 합니다', 'error') + setProductForm({ ...productForm, stock: 0 }) + } else if (parseInt(value) > MAX_STOCK_LIMIT) { + addNotification('재고는 9999개를 초과할 수 없습니다', 'error') + setProductForm({ ...productForm, stock: MAX_STOCK_LIMIT }) + } + } + + return { + productForm, + editingProduct, + showProductForm, + setEditingProduct, + setProductForm, + setShowProductForm, + startEditProduct, + handleProductSubmit, + handleAddOrCloseProductForm, + handleDeleteDiscount, + handleAddDiscount, + handleEditProuctForm, + handlePriceValidation, + handleStockValidation, + } +} diff --git a/src/advanced/hooks/useProducts.ts b/src/advanced/hooks/useProducts.ts new file mode 100644 index 00000000..60ea56c0 --- /dev/null +++ b/src/advanced/hooks/useProducts.ts @@ -0,0 +1,81 @@ +// TODO: 상품 관리 Hook +// 힌트: +// 1. 상품 목록 상태 관리 (localStorage 연동 고려) +// 2. 상품 CRUD 작업 +// 3. 재고 업데이트 +// 4. 할인 규칙 추가/삭제 +// +// 반환할 값: +// - products: 상품 배열 +// - updateProduct: 상품 정보 수정 +// - addProduct: 새 상품 추가 +// - updateProductStock: 재고 수정 +// - addProductDiscount: 할인 규칙 추가 +// - removeProductDiscount: 할인 규칙 삭제 + +import { useCallback } from 'react' +import { initialProducts } from '../constants' +import { ProductWithUI } from '../types' +import { useLocalStorage } from '../utils/hooks/useLocalStorage' +import { getFilteredProducts as _getFilteredProducts } from '../models/product' + +export function useProducts( + addNotification: ( + message: string, + type?: 'error' | 'success' | 'warning', + ) => void, +) { + // TODO: 구현 + + const [products, setProducts] = useLocalStorage( + 'products', + initialProducts, + ) + + const addProduct = useCallback( + (newProduct: Omit) => { + const product: ProductWithUI = { + ...newProduct, + id: `p${Date.now()}`, + } + setProducts((prev) => [...prev, product]) + addNotification('상품이 추가되었습니다.', 'success') + }, + [addNotification, setProducts], + ) + + const updateProduct = useCallback( + (productId: string, updates: Partial) => { + setProducts((prev) => + prev.map((product) => + product.id === productId ? { ...product, ...updates } : product, + ), + ) + addNotification('상품이 수정되었습니다.', 'success') + }, + [addNotification, setProducts], + ) + + const deleteProduct = useCallback( + (productId: string) => { + setProducts((prev) => prev.filter((p) => p.id !== productId)) + addNotification('상품이 삭제되었습니다.', 'success') + }, + [addNotification, setProducts], + ) + + const getFilteredProducts = useCallback( + (searchTerm: string) => { + return _getFilteredProducts(searchTerm, products) + }, + [products], + ) + + return { + products, + addProduct, + updateProduct, + deleteProduct, + getFilteredProducts, + } +} diff --git a/src/advanced/models/cart.ts b/src/advanced/models/cart.ts new file mode 100644 index 00000000..cc2a8679 --- /dev/null +++ b/src/advanced/models/cart.ts @@ -0,0 +1,123 @@ +// TODO: 장바구니 비즈니스 로직 (순수 함수) +// 힌트: 모든 함수는 순수 함수로 구현 (부작용 없음, 같은 입력에 항상 같은 출력) +// +// 구현할 함수들: +// 1. calculateItemTotal(item): 개별 아이템의 할인 적용 후 총액 계산 +// 2. getMaxApplicableDiscount(item): 적용 가능한 최대 할인율 계산 +// 3. calculateCartTotal(cart, coupon): 장바구니 총액 계산 (할인 전/후, 할인액) +// 4. updateCartItemQuantity(cart, productId, quantity): 수량 변경 +// 5. addItemToCart(cart, product): 상품 추가 +// 6. getRemainingStock(product, cart): 남은 재고 계산 +// +// 원칙: +// - UI와 관련된 로직 없음 +// - 외부 상태에 의존하지 않음 +// - 모든 필요한 데이터는 파라미터로 전달받음 + +import { CartItem, Coupon, Product } from '../../types' +import { ProductWithUI } from '../types' +import { calculateDiscountedTotal } from './coupon' +// TODO: 구현 + +export const getRemainingStock = ( + product: Product, + cart: CartItem[], +): number => { + const cartItem = cart.find((item) => item.product.id === product.id) + const remaining = product.stock - (cartItem?.quantity || 0) + + return remaining +} + +const getBasicDiscount = (item: CartItem) => { + const { discounts } = item.product + const { quantity } = item + + return discounts.reduce((maxDiscount, discount) => { + return quantity >= discount.quantity && discount.rate > maxDiscount + ? discount.rate + : maxDiscount + }, 0) +} + +export const getMaxApplicableDiscount = ( + item: CartItem, + cart: CartItem[], +): number => { + const baseDiscount = getBasicDiscount(item) + const hasBulkPurchase = cart.some((cartItem) => cartItem.quantity >= 10) + return hasBulkPurchase ? Math.min(baseDiscount + 0.05, 0.5) : baseDiscount +} + +export const calculateItemTotal = ( + item: CartItem, + cart: CartItem[], +): number => { + const { price } = item.product + const { quantity } = item + const discount = getMaxApplicableDiscount(item, cart) + + return Math.round(price * quantity * (1 - discount)) +} + +export const calculateCartTotal = ( + cart: CartItem[], + coupon: Coupon | null, +): { totalBeforeDiscount: number; totalAfterDiscount: number } => { + let totalBeforeDiscount = 0 + let totalAfterDiscount = 0 + + cart.forEach((item) => { + const itemPrice = item.product.price * item.quantity + totalBeforeDiscount += itemPrice + totalAfterDiscount += calculateItemTotal(item, cart) + }) + + if (coupon) { + totalAfterDiscount = calculateDiscountedTotal(totalAfterDiscount, coupon) + } + + return { + totalBeforeDiscount: Math.round(totalBeforeDiscount), + totalAfterDiscount: Math.round(totalAfterDiscount), + } +} + +export const addItemToCart = ( + carts: CartItem[], + product: ProductWithUI, +): { + carts: CartItem[] + message: string + type: 'error' | 'success' // 명확한 타입 +} => { + const existingItem = carts.find((item) => item.product.id === product.id) + + if (existingItem) { + const newQuantity = existingItem.quantity + 1 + + if (newQuantity > product.stock) { + return { + carts: carts, + message: `재고는 ${product.stock}개까지만 있습니다.`, + type: 'error', + } + } + + return { + carts: carts.map((item) => + item.product.id === product.id + ? { ...item, quantity: newQuantity } + : item, + ), + message: '장바구니에 담았습니다', + type: 'success', + } + } + + return { + carts: [...carts, { product, quantity: 1 }], + message: '장바구니에 담았습니다', + type: 'success', + } +} diff --git a/src/advanced/models/coupon.ts b/src/advanced/models/coupon.ts new file mode 100644 index 00000000..35634989 --- /dev/null +++ b/src/advanced/models/coupon.ts @@ -0,0 +1,15 @@ +import { Coupon } from '../../types' +import { MAX_DISCOUNT_RATE } from '../constants' + +export const calculateDiscountedTotal = (total: number, coupon: Coupon) => { + let totalAfterDiscount = total + if (coupon.discountType === 'amount') { + totalAfterDiscount = Math.max(0, totalAfterDiscount - coupon.discountValue) + } else { + totalAfterDiscount = Math.round( + totalAfterDiscount * (1 - coupon.discountValue / MAX_DISCOUNT_RATE), + ) + } + + return totalAfterDiscount +} diff --git a/src/advanced/models/product.ts b/src/advanced/models/product.ts new file mode 100644 index 00000000..d2048af8 --- /dev/null +++ b/src/advanced/models/product.ts @@ -0,0 +1,14 @@ +import { ProductWithUI } from '../types' + +export const getFilteredProducts = ( + searchTerm: string, + products: ProductWithUI[], +) => { + if (!searchTerm) return products + + return products.filter( + (product) => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) || + product.description?.toLowerCase().includes(searchTerm.toLowerCase()), + ) +} diff --git a/src/advanced/types/index.ts b/src/advanced/types/index.ts new file mode 100644 index 00000000..a7fc85de --- /dev/null +++ b/src/advanced/types/index.ts @@ -0,0 +1,12 @@ +import { Product } from '../../types' + +export interface ProductWithUI extends Product { + description?: string + isRecommended?: boolean +} + +export interface Notification { + id: string + message: string + type: 'error' | 'success' | 'warning' +} diff --git a/src/advanced/utils/formatters.ts b/src/advanced/utils/formatters.ts new file mode 100644 index 00000000..f3e19f5a --- /dev/null +++ b/src/advanced/utils/formatters.ts @@ -0,0 +1,23 @@ +// TODO: 포맷팅 유틸리티 함수들 +// 구현할 함수: +// - formatPrice(price: number): string - 가격을 한국 원화 형식으로 포맷 +// - formatDate(date: Date): string - 날짜를 YYYY-MM-DD 형식으로 포맷 +// - formatPercentage(rate: number): string - 소수를 퍼센트로 변환 (0.1 → 10%) + +// TODO: 구현 + +export const formatPrice = ( + price: number, + isAdmin: boolean, + isSoldOut: boolean, +): string => { + if (isSoldOut) { + return 'SOLD OUT' + } + + if (isAdmin) { + return `${price.toLocaleString()}원` + } + + return `₩${price.toLocaleString()}` +} diff --git a/src/advanced/utils/hooks/useDebounce.ts b/src/advanced/utils/hooks/useDebounce.ts new file mode 100644 index 00000000..c3bcdea1 --- /dev/null +++ b/src/advanced/utils/hooks/useDebounce.ts @@ -0,0 +1,24 @@ +// TODO: 디바운스 Hook +// 힌트: +// 1. 값이 변경되어도 지정된 시간 동안 대기 +// 2. 대기 시간 동안 값이 다시 변경되면 타이머 리셋 +// 3. 최종적으로 안정된 값만 반환 +// +// 사용 예시: 검색어 입력 디바운싱 + +import { useEffect, useState } from 'react' + +export function useDebounce(value: T, delay: number): T { + // TODO: 구현 + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(timer) + }, [value, delay]) + + return debouncedValue +} diff --git a/src/advanced/utils/hooks/useLocalStorage.ts b/src/advanced/utils/hooks/useLocalStorage.ts new file mode 100644 index 00000000..1d80db6c --- /dev/null +++ b/src/advanced/utils/hooks/useLocalStorage.ts @@ -0,0 +1,60 @@ +// TODO: LocalStorage Hook +// 힌트: +// 1. localStorage와 React state 동기화 +// 2. 초기값 로드 시 에러 처리 +// 3. 저장 시 JSON 직렬화/역직렬화 +// 4. 빈 배열이나 undefined는 삭제 +// +// 반환값: [저장된 값, 값 설정 함수] + +import { useState } from 'react' + +export function useLocalStorage( + key: string, + initialValue: T, +): [T, (value: T | ((val: T) => T)) => void] { + // TODO: 구현 + const [localValue, setLocalValue] = useState(() => { + const saved = localStorage.getItem(key) + if (saved) { + try { + return JSON.parse(saved) + } catch { + return initialValue + } + } + return initialValue + }) + + const saveLocalStorage = (val: T | ((val: T) => T)) => { + // 타입 가드로 확실하게 구분 + const isUpdater = (val: T | ((prev: T) => T)): val is (prev: T) => T => { + return typeof val === 'function' + } + if (isUpdater(val)) { + // 함수형 업데이트: 함수를 실행한 결과를 localStorage에 저장 + setLocalValue((prev) => { + const result = val(prev) + if (result === null || result === undefined) { + localStorage.removeItem(key) + return initialValue + } else { + localStorage.setItem(key, JSON.stringify(result)) + return result + } + }) + } else if ( + [null, undefined].includes(val as never) || + (Array.isArray(val) && val.length === 0) + ) { + setLocalValue(initialValue) + localStorage.removeItem(key) + } else { + // 직접 값: 값을 그대로 localStorage에 저장 + setLocalValue(val) + localStorage.setItem(key, JSON.stringify(val)) + } + } + + return [localValue, saveLocalStorage] +} diff --git a/src/advanced/utils/validators.ts b/src/advanced/utils/validators.ts new file mode 100644 index 00000000..8b88abea --- /dev/null +++ b/src/advanced/utils/validators.ts @@ -0,0 +1,13 @@ +// TODO: 검증 유틸리티 함수들 +// 구현할 함수: +// - isValidCouponCode(code: string): boolean - 쿠폰 코드 형식 검증 (4-12자 영문 대문자와 숫자) +// - isValidStock(stock: number): boolean - 재고 수량 검증 (0 이상) +// - isValidPrice(price: number): boolean - 가격 검증 (양수) +// - extractNumbers(value: string): string - 문자열에서 숫자만 추출 + +// TODO: 구현 + +// 문자열 숫자 확인 +export function isValidStrNumber(value: string) { + return /^\d+$/.test(value) +} From 08d49d85f0972fec8b722ea8e2842efa176d039c Mon Sep 17 00:00:00 2001 From: annkimm Date: Thu, 7 Aug 2025 19:43:51 +0900 Subject: [PATCH 13/16] feat: add context api --- src/advanced/App.tsx | 86 ++++---- src/advanced/components/AdminPage.tsx | 192 ++++++++---------- src/advanced/components/CartPage.tsx | 100 ++------- .../components/ui/admin/CouponForm.tsx | 32 ++- .../components/ui/admin/CouponList.tsx | 14 +- .../components/ui/admin/ProductAccordion.tsx | 32 +-- .../components/ui/admin/ProductForm.tsx} | 46 ++--- src/advanced/components/ui/cart/Cart.tsx | 59 +++--- .../components/ui/cart/CartHeader.tsx | 10 +- .../components/ui/cart/ProductList.tsx | 10 +- src/advanced/hooks/useCart.ts | 5 +- src/advanced/hooks/useCoupons.ts | 7 +- src/advanced/hooks/useForm.ts | 20 +- src/advanced/hooks/useProducts.ts | 7 +- src/advanced/types/context.ts | 73 +++++++ src/basic/components/AdminPage.tsx | 4 +- .../components/ui/admin/ProductForm.tsx} | 6 +- src/basic/hooks/useCart.ts | 26 --- 18 files changed, 349 insertions(+), 380 deletions(-) rename src/{basic/components/ui/admin/ProductFom.tsx => advanced/components/ui/admin/ProductForm.tsx} (86%) create mode 100644 src/advanced/types/context.ts rename src/{advanced/components/ui/admin/ProductFom.tsx => basic/components/ui/admin/ProductForm.tsx} (98%) diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx index 6c957579..9174e034 100644 --- a/src/advanced/App.tsx +++ b/src/advanced/App.tsx @@ -1,10 +1,10 @@ import { useState, useCallback } from 'react' import { Notification } from './types' import { AdminPage } from './components/AdminPage' -import { useProducts } from './hooks/useProducts' +import { ProductsContext, useProducts } from './hooks/useProducts' import { CartPage } from './components/CartPage' -import { useCart } from './hooks/useCart' -import { useCoupons } from './hooks/useCoupons' +import { CartContext, useCart } from './hooks/useCart' +import { CouponsContext, useCoupons } from './hooks/useCoupons' import { Notifications } from './components/ui/Notifications' const App = () => { @@ -49,47 +49,57 @@ const App = () => { ) const [isAdmin, setIsAdmin] = useState(false) - const adminProps = { - isAdmin, - setIsAdmin, - products, - cart, - deleteProduct, - addNotification, - coupons, - deleteCoupon, - updateProduct, - addProduct, - addCoupon, - getRemainingStock, - } - - const cartProps = { - isAdmin, - setIsAdmin, - products, - cart, - coupons, - totals: calculateCartTotal(), - selectedCoupon, - addToCart, - removeFromCart, - handleSelectCoupon, - updateQuantity, - completeOrder, - getRemainingStock, - getFilteredProducts, - calculateTotal, - totalItemCount, - } - return (
- {isAdmin ? : } + + + + {isAdmin ? ( + + ) : ( + + )} + + +
) } diff --git a/src/advanced/components/AdminPage.tsx b/src/advanced/components/AdminPage.tsx index 22bb3896..7991de66 100644 --- a/src/advanced/components/AdminPage.tsx +++ b/src/advanced/components/AdminPage.tsx @@ -1,60 +1,41 @@ -// 힌트: -// 1. 탭 UI로 상품 관리와 쿠폰 관리 분리 -// 2. 상품 추가/수정/삭제 기능 -// 3. 쿠폰 생성 기능 -// 4. 할인 규칙 설정 -// -// 필요한 hooks: -// - useProducts: 상품 CRUD -// - useCoupons: 쿠폰 CRUD - -import { CartItem, Coupon } from '../../types' -import { ProductWithUI } from '../types' -import { useCouponForm, useProductForm } from '../hooks/useForm' -import { useState } from 'react' +import { + CouponItemFormContext, + ProductItemFormContext, + useCouponForm, + useProductForm, +} from '../hooks/useForm' +import { useContext, useState } from 'react' import { AdminHeader } from './ui/admin/AdminHeader' -import { ProductFom } from './ui/admin/ProductFom' +import { ProductForm } from './ui/admin/ProductForm' import { CouponForm } from './ui/admin/CouponForm' import { CouponList } from './ui/admin/CouponList' import { ProductAccordion } from './ui/admin/ProductAccordion' +import { ProductsContext } from '../hooks/useProducts' +import { CouponContext, ProductContext } from '../types/context' +import { CouponsContext } from '../hooks/useCoupons' export function AdminPage({ isAdmin, - cart, setIsAdmin, - products, - deleteProduct, addNotification, - coupons, - deleteCoupon, - updateProduct, - addProduct, - addCoupon, - getRemainingStock, }: { isAdmin: boolean - cart: CartItem[] setIsAdmin: React.Dispatch> - products: ProductWithUI[] - deleteProduct: (productId: string) => void addNotification: ( message: string, type?: 'error' | 'success' | 'warning', ) => void - coupons: Coupon[] - deleteCoupon: (couponCode: string) => void - updateProduct: (productId: string, updates: Partial) => void - addProduct: (newProduct: Omit) => void - addCoupon: (newCoupon: Coupon) => void - getRemainingStock: (product: ProductWithUI) => number }) { - // TODO: 구현 const [activeTab, setActiveTab] = useState<'products' | 'coupons'>('products') + const { addProduct, updateProduct } = useContext( + ProductsContext, + ) as ProductContext + const { addCoupon } = useContext(CouponsContext) as CouponContext const { couponForm, handleEditCouponForm, showCouponForm, - setShowCouponForm, + toggleShowCouponForm, handleCouponSubmit, handleDiscountValueValidation, } = useCouponForm(addCoupon, addNotification) @@ -72,34 +53,6 @@ export function AdminPage({ handleStockValidation, } = useProductForm(addProduct, updateProduct, addNotification) - const productFormProps = { - productForm, - handleProductSubmit, - editingProduct, - handleAddDiscount, - handleDeleteDiscount, - handleEditProuctForm, - handleAddOrCloseProductForm, - handlePriceValidation, - handleStockValidation, - } - - const productAccordionProps = { - products, - cart, - startEditProduct, - deleteProduct, - getRemainingStock, - } - - const cuponFormProps = { - couponForm, - handleCouponSubmit, - setShowCouponForm, - handleEditCouponForm, - handleDiscountValueValidation, - } - return ( <> @@ -139,56 +92,81 @@ export function AdminPage({
{activeTab === 'products' ? ( -
-
-
-

상품 목록

- -
-
- - - {showProductForm && } -
- ) : ( -
-
-

쿠폰 관리

-
-
-
- -
+ +
+
+
+

상품 목록

- {showCouponForm && } -
-
+ + {showProductForm && } + + + ) : ( + +
+
+

쿠폰 관리

+
+
+
+ +
+ +
+
+ + {showCouponForm && } +
+
+
)}
diff --git a/src/advanced/components/CartPage.tsx b/src/advanced/components/CartPage.tsx index daaf5461..718b3e6d 100644 --- a/src/advanced/components/CartPage.tsx +++ b/src/advanced/components/CartPage.tsx @@ -1,99 +1,35 @@ -// TODO: 장바구니 페이지 컴포넌트 -// 힌트: -// 1. 상품 목록 표시 (검색 기능 포함) -// 2. 장바구니 관리 -// 3. 쿠폰 적용 -// 4. 주문 처리 -// -// 필요한 hooks: -// - useProducts: 상품 목록 관리 -// - useCart: 장바구니 상태 관리 -// - useCoupons: 쿠폰 목록 관리 -// - useDebounce: 검색어 디바운싱 - -import { useState } from 'react' -import { CartItem, Coupon } from '../../types' -import { ProductWithUI } from '../types' +import { useContext, useState } from 'react' import { CartHeader } from './ui/cart/CartHeader' import { ProductList } from './ui/cart/ProductList' import { Cart } from './ui/cart/Cart' import { useDebounce } from '../utils/hooks/useDebounce' +import { ProductContext } from '../types/context' +import { ProductsContext } from '../hooks/useProducts' export function CartPage({ isAdmin, setIsAdmin, - products, - cart, - coupons, - totals, - selectedCoupon, - addToCart, - removeFromCart, - updateQuantity, - completeOrder, - handleSelectCoupon, - getFilteredProducts, - getRemainingStock, - calculateTotal, - totalItemCount, }: { isAdmin: boolean setIsAdmin: React.Dispatch> - products: ProductWithUI[] - cart: CartItem[] - coupons: Coupon[] - totals: { - totalBeforeDiscount: number - totalAfterDiscount: number - } - selectedCoupon: Coupon | null - addToCart: (product: ProductWithUI) => void - removeFromCart: (productId: string) => void - updateQuantity: ( - productId: string, - newQuantity: number, - products: ProductWithUI[], - ) => void - completeOrder: () => void - handleSelectCoupon: ( - e: React.ChangeEvent, - coupons: Coupon[], - ) => void - getFilteredProducts: (searchTerm: string) => ProductWithUI[] - getRemainingStock: (product: ProductWithUI) => number - calculateTotal: (item: CartItem) => number - totalItemCount: number }) { - // TODO: 구현 + const { products, getFilteredProducts } = useContext( + ProductsContext, + ) as ProductContext const [searchTerm, setSearchTerm] = useState('') const debouncedSearchTerm = useDebounce(searchTerm, 500) const filteredProducts = getFilteredProducts(debouncedSearchTerm) - const headerState = { - isAdmin, - searchTerm, - cart, - totalItemCount, - setSearchTerm, - setIsAdmin, - } - - const cartProps = { - cart, - coupons, - products, - selectedCoupon, - removeFromCart, - updateQuantity, - totals, - completeOrder, - handleSelectCoupon, - calculateTotal, - } - return ( <> - +
@@ -114,16 +50,12 @@ export function CartPage({

) : ( - + )}
- +
diff --git a/src/advanced/components/ui/admin/CouponForm.tsx b/src/advanced/components/ui/admin/CouponForm.tsx index be350f62..337c62fb 100644 --- a/src/advanced/components/ui/admin/CouponForm.tsx +++ b/src/advanced/components/ui/admin/CouponForm.tsx @@ -1,22 +1,16 @@ -import { FocusEvent } from 'react' -import { Coupon } from '../../../../types' +import { useContext } from 'react' +import { CouponFormContext } from '../../../types/context' +import { CouponItemFormContext } from '../../../hooks/useForm' + +export function CouponForm() { + const { + couponForm, + handleCouponSubmit, + handleDiscountValueValidation, + handleEditCouponForm, + toggleShowCouponForm, + } = useContext(CouponItemFormContext) as CouponFormContext -export function CouponForm({ - couponForm, - handleCouponSubmit, - setShowCouponForm, - handleEditCouponForm, - handleDiscountValueValidation, -}: { - couponForm: Coupon - setShowCouponForm: React.Dispatch> - handleCouponSubmit: (e: React.FormEvent) => void - handleEditCouponForm: ( - e: React.ChangeEvent, - key: string, - ) => void - handleDiscountValueValidation: (e: FocusEvent) => void -}) { return (
@@ -81,7 +75,7 @@ export function CouponForm({