diff --git a/index.advanced.html b/index.advanced.html index 1d45f718..6962f1fd 100644 --- a/index.advanced.html +++ b/index.advanced.html @@ -8,6 +8,6 @@
- + diff --git a/package.json b/package.json index 02cd2326..31fda7da 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,30 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc && vite build", "preview": "vite preview", "start:basic": "vite serve --open ./index.basic.html", "start:advanced": "vite serve --open ./index.advanced.html", "test": "vitest", "test:basic": "vitest basic.test.js", - "test:advanced": "vitest advanced.test.js", + "test:advanced": "vitest advanced.test.tsx", "test:ui": "vitest --ui" }, "devDependencies": { - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", + "@vitejs/plugin-react": "^4.3.4", "@vitest/ui": "^2.1.1", "jsdom": "^25.0.0", "vite": "^5.1.0", "vitest": "^2.1.1" + }, + "dependencies": { + "@types/react": "^19.0.4", + "@types/react-dom": "^19.0.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.7.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..53347944 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1902 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/react': + specifier: ^19.0.4 + version: 19.0.4 + '@types/react-dom': + specifier: ^19.0.2 + version: 19.0.2(@types/react@19.0.4) + react: + specifier: ^19.0.0 + version: 19.0.0 + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.1.0 + version: 16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.3.4(vite@5.4.11) + '@vitest/ui': + specifier: ^2.1.1 + version: 2.1.8(vitest@2.1.8) + jsdom: + specifier: ^25.0.0 + version: 25.0.1 + vite: + specifier: ^5.1.0 + version: 5.4.11 + vitest: + specifier: ^2.1.1 + version: 2.1.8(@vitest/ui@2.1.8)(jsdom@25.0.1) + +packages: + + '@adobe/css-tools@4.4.1': + resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + + '@rollup/rollup-android-arm-eabi@4.30.0': + resolution: {integrity: sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.30.0': + resolution: {integrity: sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.30.0': + resolution: {integrity: sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.30.0': + resolution: {integrity: sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.30.0': + resolution: {integrity: sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.30.0': + resolution: {integrity: sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.30.0': + resolution: {integrity: sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.30.0': + resolution: {integrity: sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.30.0': + resolution: {integrity: sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.30.0': + resolution: {integrity: sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.30.0': + resolution: {integrity: sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.0': + resolution: {integrity: sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.30.0': + resolution: {integrity: sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.30.0': + resolution: {integrity: sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.30.0': + resolution: {integrity: sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.30.0': + resolution: {integrity: sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.30.0': + resolution: {integrity: sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.30.0': + resolution: {integrity: sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.30.0': + resolution: {integrity: sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==} + cpu: [x64] + os: [win32] + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.1.0': + resolution: {integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==} + 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.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + 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/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/react-dom@19.0.2': + resolution: {integrity: sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.0.4': + resolution: {integrity: sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg==} + + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/expect@2.1.8': + resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} + + '@vitest/mocker@2.1.8': + resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.8': + resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + + '@vitest/runner@2.1.8': + resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} + + '@vitest/snapshot@2.1.8': + resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} + + '@vitest/spy@2.1.8': + resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} + + '@vitest/ui@2.1.8': + resolution: {integrity: sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==} + peerDependencies: + vitest: 2.1.8 + + '@vitest/utils@2.1.8': + resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + 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'} + + 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'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001690: + resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + 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@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + 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.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + electron-to-chromium@1.5.79: + resolution: {integrity: sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + 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'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + 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==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + 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.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + 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@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + 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'} + + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + 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==} + + rollup@4.30.0: + resolution: {integrity: sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + 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.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + 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.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.71: + resolution: {integrity: sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==} + + tldts@6.1.71: + resolution: {integrity: sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==} + hasBin: true + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.2: + resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite-node@2.1.8: + resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.8: + resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.8 + '@vitest/ui': 2.1.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + 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.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@adobe/css-tools@4.4.1': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.3': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@polka/url@1.0.0-next.28': {} + + '@rollup/rollup-android-arm-eabi@4.30.0': + optional: true + + '@rollup/rollup-android-arm64@4.30.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.30.0': + optional: true + + '@rollup/rollup-darwin-x64@4.30.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.30.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.30.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.30.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.30.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.30.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.30.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.30.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.30.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.30.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.30.0': + optional: true + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@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.3': + dependencies: + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react@16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.4 + '@types/react-dom': 19.0.2(@types/react@19.0.4) + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.3 + + '@types/estree@1.0.6': {} + + '@types/react-dom@19.0.2(@types/react@19.0.4)': + dependencies: + '@types/react': 19.0.4 + + '@types/react@19.0.4': + dependencies: + csstype: 3.1.3 + + '@vitejs/plugin-react@4.3.4(vite@5.4.11)': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.11 + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.8': + dependencies: + '@vitest/spy': 2.1.8 + '@vitest/utils': 2.1.8 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.8(vite@5.4.11)': + dependencies: + '@vitest/spy': 2.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.11 + + '@vitest/pretty-format@2.1.8': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.8': + dependencies: + '@vitest/utils': 2.1.8 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.8': + dependencies: + '@vitest/pretty-format': 2.1.8 + magic-string: 0.30.17 + pathe: 1.1.2 + + '@vitest/spy@2.1.8': + dependencies: + tinyspy: 3.0.2 + + '@vitest/ui@2.1.8(vitest@2.1.8)': + dependencies: + '@vitest/utils': 2.1.8 + fflate: 0.8.2 + flatted: 3.3.2 + pathe: 1.1.2 + sirv: 3.0.0 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.8(@vitest/ui@2.1.8)(jsdom@25.0.1) + + '@vitest/utils@2.1.8': + dependencies: + '@vitest/pretty-format': 2.1.8 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + agent-base@7.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001690 + electron-to-chromium: 1.5.79 + node-releases: 2.0.19 + update-browserslist-db: 1.1.2(browserslist@4.24.4) + + cac@6.7.14: {} + + caniuse-lite@1.0.30001690: {} + + chai@5.1.2: + 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@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + css.escape@1.5.1: {} + + cssstyle@4.1.0: + dependencies: + rrweb-cssom: 0.7.1 + + csstype@3.1.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decimal.js@10.4.3: {} + + deep-eql@5.0.2: {} + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + electron-to-chromium@1.5.79: {} + + entities@4.5.0: {} + + es-module-lexer@1.6.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + expect-type@1.1.0: {} + + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.8.2: {} + + flatted@3.3.2: {} + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + globals@11.12.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.4.0 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + indent-string@4.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + js-tokens@4.0.0: {} + + jsdom@25.0.1: + dependencies: + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lodash@4.17.21: {} + + loupe@3.1.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-indent@1.0.1: {} + + mrmime@2.0.0: {} + + ms@2.1.3: {} + + nanoid@3.3.8: {} + + node-releases@2.0.19: {} + + nwsapi@2.2.16: {} + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + pathe@1.1.2: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 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: {} + + react-dom@19.0.0(react@19.0.0): + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + + react-is@17.0.2: {} + + react-refresh@0.14.2: {} + + react@19.0.0: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regenerator-runtime@0.14.1: {} + + rollup@4.30.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.30.0 + '@rollup/rollup-android-arm64': 4.30.0 + '@rollup/rollup-darwin-arm64': 4.30.0 + '@rollup/rollup-darwin-x64': 4.30.0 + '@rollup/rollup-freebsd-arm64': 4.30.0 + '@rollup/rollup-freebsd-x64': 4.30.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.0 + '@rollup/rollup-linux-arm-musleabihf': 4.30.0 + '@rollup/rollup-linux-arm64-gnu': 4.30.0 + '@rollup/rollup-linux-arm64-musl': 4.30.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.0 + '@rollup/rollup-linux-riscv64-gnu': 4.30.0 + '@rollup/rollup-linux-s390x-gnu': 4.30.0 + '@rollup/rollup-linux-x64-gnu': 4.30.0 + '@rollup/rollup-linux-x64-musl': 4.30.0 + '@rollup/rollup-win32-arm64-msvc': 4.30.0 + '@rollup/rollup-win32-ia32-msvc': 4.30.0 + '@rollup/rollup-win32-x64-msvc': 4.30.0 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.25.0: {} + + semver@6.3.1: {} + + siginfo@2.0.0: {} + + sirv@3.0.0: + 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.8.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + 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.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.0.2: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.71: {} + + tldts@6.1.71: + dependencies: + tldts-core: 6.1.71 + + totalist@3.0.1: {} + + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.71 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + + typescript@5.7.3: {} + + update-browserslist-db@1.1.2(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite-node@2.1.8: + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.11 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.11: + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.30.0 + optionalDependencies: + fsevents: 2.3.3 + + vitest@2.1.8(@vitest/ui@2.1.8)(jsdom@25.0.1): + dependencies: + '@vitest/expect': 2.1.8 + '@vitest/mocker': 2.1.8(vite@5.4.11) + '@vitest/pretty-format': 2.1.8 + '@vitest/runner': 2.1.8 + '@vitest/snapshot': 2.1.8 + '@vitest/spy': 2.1.8 + '@vitest/utils': 2.1.8 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.11 + vite-node: 2.1.8 + why-is-node-running: 2.3.0 + optionalDependencies: + '@vitest/ui': 2.1.8(vitest@2.1.8) + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.18.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} diff --git a/src/advanced/App.tsx b/src/advanced/App.tsx new file mode 100644 index 00000000..88e6c475 --- /dev/null +++ b/src/advanced/App.tsx @@ -0,0 +1,16 @@ +import React, { useState } from "react"; +import Cart from "./components/Cart"; +import { INITIAL_PRODUCT_LIST } from "./constants"; +import { Product } from "./types"; + +const App: React.FC = () => { + const [productList, setProductList] = useState(INITIAL_PRODUCT_LIST); + + return ( +
+ +
+ ); +}; + +export default App; diff --git a/src/advanced/components/Cart/index.tsx b/src/advanced/components/Cart/index.tsx new file mode 100644 index 00000000..3da86ca1 --- /dev/null +++ b/src/advanced/components/Cart/index.tsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from "react"; +import { Product, CartItem, CartTotalList } from "../../types"; +import ProductSelect from "../ProductSelect"; +import CartItemList from "../CartItemList"; +import CartSummary from "../CartSummary"; +import { usePromotion } from "../../hooks/usePromotion"; +import { calcTotalList } from "../../services/cartService"; + +interface CartProps { + productList: Product[]; + setProductList: React.Dispatch>; +} + +const Cart: React.FC = ({ productList, setProductList }) => { + const [cartItemList, setCartItemList] = useState([]); + const [lastSelectedProduct, setLastSelectedProduct] = useState( + null + ); + const [totalList, setTotalList] = useState({ + subTotal: 0, + totalAmt: 0, + itemCnt: 0, + discount: 0, + }); + + usePromotion(productList, setProductList, lastSelectedProduct); + + useEffect(() => { + const newTotalList = calcTotalList(cartItemList, productList); + setTotalList(newTotalList); + }, [cartItemList, productList]); + + const handleAddToCart = (productId: string) => { + const product = productList.find((p) => p.id === productId); + if (!product || product.stock <= 0) return; + + setProductList((prev) => + prev.map((p) => (p.id === productId ? { ...p, stock: p.stock - 1 } : p)) + ); + + setCartItemList((prev) => { + const existingItem = prev.find((item) => item.id === productId); + if (existingItem) { + return prev.map((item) => + item.id === productId ? { ...item, qty: item.qty + 1 } : item + ); + } + return [...prev, { id: productId, qty: 1 }]; + }); + + setLastSelectedProduct(productId); + }; + + const handleUpdateQty = (productId: string, change: number) => { + const product = productList.find((p) => p.id === productId); + if (!product) return; + + if (change > 0 && product.stock <= 0) { + alert("재고가 부족합니다."); + return; + } + + setProductList((prev) => + prev.map((p) => + p.id === productId ? { ...p, stock: p.stock - change } : p + ) + ); + + setCartItemList((prev) => { + const updatedItemList = prev + .map((item) => + item.id === productId ? { ...item, qty: item.qty + change } : item + ) + .filter((item) => item.qty > 0); + + return updatedItemList; + }); + }; + + return ( +
+

장바구니

+ + + +
+ ); +}; + +export default Cart; diff --git a/src/advanced/components/CartItemList/index.tsx b/src/advanced/components/CartItemList/index.tsx new file mode 100644 index 00000000..21ec8061 --- /dev/null +++ b/src/advanced/components/CartItemList/index.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Product, CartItem } from "../../types"; + +interface CartItemListProps { + itemList: CartItem[]; + productList: Product[]; + handleUpdateQty: (productId: string, change: number) => void; +} + +const CartItemList: React.FC = ({ + itemList, + productList, + handleUpdateQty, +}) => { + return ( +
+ {itemList.map((item) => { + const product = productList.find((p) => p.id === item.id); + if (!product) return null; + + return ( +
+ + {product.name} - {product.price}원 x {item.qty} + +
+ + + +
+
+ ); + })} +
+ ); +}; + +export default CartItemList; diff --git a/src/advanced/components/CartSummary/index.tsx b/src/advanced/components/CartSummary/index.tsx new file mode 100644 index 00000000..b934b4f7 --- /dev/null +++ b/src/advanced/components/CartSummary/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { CartTotalList } from "../../types"; + +interface CartSummaryProps { + totalList: CartTotalList; +} + +const CartSummary: React.FC = ({ totalList }) => { + const pointList = Math.floor(totalList.subTotal / 100); + + return ( +
+
+ 총액: {Math.round(totalList.totalAmt)}원 + {totalList.discount > 0 && + ` (${(totalList.discount * 100).toFixed(1)}% 할인 적용)`} +
+
(포인트: {pointList})
+
+ ); +}; + +export default CartSummary; diff --git a/src/advanced/components/ProductSelect/index.tsx b/src/advanced/components/ProductSelect/index.tsx new file mode 100644 index 00000000..11e2977d --- /dev/null +++ b/src/advanced/components/ProductSelect/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Product } from "../../types"; + +interface ProductSelectProps { + productList: Product[]; + handleAddToCart: (productId: string) => void; +} + +const ProductSelect: React.FC = ({ + productList, + handleAddToCart, +}) => { + return ( +
+ + +
+ ); +}; + +export default ProductSelect; diff --git a/src/advanced/constants/index.ts b/src/advanced/constants/index.ts new file mode 100644 index 00000000..d9790d0b --- /dev/null +++ b/src/advanced/constants/index.ts @@ -0,0 +1,32 @@ +export const PRODUCT_DISCOUNT_LIST: { [key: string]: number } = { + p1: 0.1, + p2: 0.15, + p3: 0.2, + p4: 0.05, + p5: 0.25, +}; + +export const BULK_PURCHASE_THRESHOLD = 30; +export const BULK_PURCHASE_DISCOUNT_RATE = 0.25; + +export const PROMOTION_CONFIG = { + FLASH_SALE: { + INTERVAL: 30000, + INITIAL_DELAY: 10000, + PROBABILITY: 0.3, + DISCOUNT_RATE: 0.8, + }, + RECOMMENDATION: { + INTERVAL: 60000, + INITIAL_DELAY: 20000, + DISCOUNT_RATE: 0.95, + }, +}; + +export const INITIAL_PRODUCT_LIST = [ + { id: "p1", name: "상품1", price: 10000, stock: 50 }, + { id: "p2", name: "상품2", price: 20000, stock: 30 }, + { id: "p3", name: "상품3", price: 30000, stock: 20 }, + { id: "p4", name: "상품4", price: 15000, stock: 0 }, + { id: "p5", name: "상품5", price: 25000, stock: 10 }, +]; diff --git a/src/advanced/hooks/usePromotion.ts b/src/advanced/hooks/usePromotion.ts new file mode 100644 index 00000000..71f6a03b --- /dev/null +++ b/src/advanced/hooks/usePromotion.ts @@ -0,0 +1,79 @@ +import { useEffect } from "react"; +import { Product } from "../types"; +import { PROMOTION_CONFIG } from "../constants"; + +export const usePromotion = ( + productList: Product[], + setProductList: React.Dispatch>, + lastSelectedProduct: string | null +) => { + useEffect(() => { + // 번개세일 + const flashSaleConfig = PROMOTION_CONFIG.FLASH_SALE; + const flashSaleTimer = setTimeout(() => { + const interval = setInterval(() => { + if (Math.random() < flashSaleConfig.PROBABILITY) { + setProductList((prev) => { + const availableProductList = prev.filter((p) => p.stock > 0); + if (availableProductList.length === 0) return prev; + + const randomProduct = + availableProductList[ + Math.floor(Math.random() * availableProductList.length) + ]; + + alert(`번개세일! ${randomProduct.name}이(가) 20% 할인 중입니다!`); + + return prev.map((p) => + p.id === randomProduct.id + ? { + ...p, + price: Math.round(p.price * flashSaleConfig.DISCOUNT_RATE), + } + : p + ); + }); + } + }, flashSaleConfig.INTERVAL); + + return () => clearInterval(interval); + }, flashSaleConfig.INITIAL_DELAY); + + // 추천 프로모션 + const recConfig = PROMOTION_CONFIG.RECOMMENDATION; + const recTimer = setTimeout(() => { + const interval = setInterval(() => { + if (lastSelectedProduct) { + setProductList((prev) => { + const availableProductList = prev.filter( + (p) => p.id !== lastSelectedProduct && p.stock > 0 + ); + if (availableProductList.length === 0) return prev; + + const randomProduct = + availableProductList[ + Math.floor(Math.random() * availableProductList.length) + ]; + + alert( + `${randomProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!` + ); + + return prev.map((p) => + p.id === randomProduct.id + ? { ...p, price: Math.round(p.price * recConfig.DISCOUNT_RATE) } + : p + ); + }); + } + }, recConfig.INTERVAL); + + return () => clearInterval(interval); + }, recConfig.INITIAL_DELAY); + + return () => { + clearTimeout(flashSaleTimer); + clearTimeout(recTimer); + }; + }, [lastSelectedProduct]); +}; diff --git a/src/advanced/main.advanced.js b/src/advanced/main.advanced.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/advanced/main.advanced.tsx b/src/advanced/main.advanced.tsx new file mode 100644 index 00000000..e4b1b158 --- /dev/null +++ b/src/advanced/main.advanced.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("app")!).render( + + + +); diff --git a/src/advanced/services/cartService.ts b/src/advanced/services/cartService.ts new file mode 100644 index 00000000..d6a2185b --- /dev/null +++ b/src/advanced/services/cartService.ts @@ -0,0 +1,30 @@ +import { Product, CartItem, CartTotalList } from "../types"; +import { PRODUCT_DISCOUNT_LIST } from "../constants"; + +export const calcTotalList = ( + cartItemList: CartItem[], + productList: Product[] +): CartTotalList => { + const initialTotalList: CartTotalList = { + subTotal: 0, + totalAmt: 0, + itemCnt: 0, + discount: 0, + }; + + return cartItemList.reduce((totalList, item) => { + const product = productList.find((p) => p.id === item.id); + if (!product) return totalList; + + const itemSubTotal = product.price * item.qty; + const discount = PRODUCT_DISCOUNT_LIST[item.id] || 0; + const itemDiscount = itemSubTotal * discount; + + return { + subTotal: totalList.subTotal + itemSubTotal, + discount: totalList.discount + itemDiscount, + itemCnt: totalList.itemCnt + item.qty, + totalAmt: totalList.totalAmt + (itemSubTotal - itemDiscount), + }; + }, initialTotalList); +}; diff --git a/src/advanced/types/index.ts b/src/advanced/types/index.ts new file mode 100644 index 00000000..c478b099 --- /dev/null +++ b/src/advanced/types/index.ts @@ -0,0 +1,38 @@ +export interface Product { + id: string; + name: string; + price: number; + stock: number; +} + +export interface CartItem { + id: string; + qty: number; +} + +export interface CartState { + lastSelectedProduct: string | null; + bonusPointList: number; + itemCnt: number; +} + +export interface CartTotalList { + subTotal: number; + totalAmt: number; + itemCnt: number; + discount: number; +} + +export interface PromotionConfig { + FLASH_SALE: { + INTERVAL: number; + INITIAL_DELAY: number; + PROBABILITY: number; + DISCOUNT_RATE: number; + }; + RECOMMENDATION: { + INTERVAL: number; + INITIAL_DELAY: number; + DISCOUNT_RATE: number; + }; +} diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js index 0efffa28..1dfd7a41 100644 --- a/src/basic/main.basic.js +++ b/src/basic/main.basic.js @@ -1,204 +1,439 @@ -var prodList, sel, addBtn, cartDisp, sum, stockInfo; -var lastSel, bonusPts=0, totalAmt=0, itemCnt=0; -function main() { - prodList=[ - {id: 'p1', name: '상품1', val: 10000, q: 50 }, - {id: 'p2', name: '상품2', val: 20000, q: 30 }, - {id: 'p3', name: '상품3', val: 30000, q: 20 }, - {id: 'p4', name: '상품4', val: 15000, q: 0 }, - {id: 'p5', name: '상품5', val: 25000, q: 10 } - ]; - var root=document.getElementById('app'); - let cont=document.createElement('div'); - var wrap=document.createElement('div'); - let hTxt=document.createElement('h1'); - cartDisp=document.createElement('div'); - sum=document.createElement('div'); - sel=document.createElement('select'); - addBtn=document.createElement('button'); - stockInfo=document.createElement('div'); - cartDisp.id='cart-items'; - sum.id='cart-total'; - sel.id='product-select'; - addBtn.id='add-to-cart'; - stockInfo.id='stock-status'; - cont.className='bg-gray-100 p-8'; - wrap.className='max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl p-8'; - hTxt.className='text-2xl font-bold mb-4'; - sum.className='text-xl font-bold my-4'; - sel.className='border rounded p-2 mr-2'; - addBtn.className='bg-blue-500 text-white px-4 py-2 rounded'; - stockInfo.className='text-sm text-gray-500 mt-2'; - hTxt.textContent='장바구니'; - addBtn.textContent='추가'; - updateSelOpts(); - wrap.appendChild(hTxt); - wrap.appendChild(cartDisp); - wrap.appendChild(sum); - wrap.appendChild(sel); - wrap.appendChild(addBtn); - wrap.appendChild(stockInfo); - cont.appendChild(wrap); - root.appendChild(cont); - calcCart(); - setTimeout(function () { - setInterval(function () { - var luckyItem=prodList[Math.floor(Math.random() * prodList.length)]; - if(Math.random() < 0.3 && luckyItem.q > 0) { - luckyItem.val=Math.round(luckyItem.val * 0.8); - alert('번개세일! ' + luckyItem.name + '이(가) 20% 할인 중입니다!'); - updateSelOpts(); - } - }, 30000); - }, Math.random() * 10000); - setTimeout(function () { - setInterval(function () { - if(lastSel) { - var suggest=prodList.find(function (item) { return item.id !== lastSel && item.q > 0; }); - if(suggest) { - alert(suggest.name + '은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!'); - suggest.val=Math.round(suggest.val * 0.95); - updateSelOpts(); - } - } - }, 60000); - }, Math.random() * 20000); +/** + * 상수 정의 + */ +const PRODUCT_DISCOUNTS = { + p1: 0.1, + p2: 0.15, + p3: 0.2, + p4: 0.05, + p5: 0.25, }; -function updateSelOpts() { - sel.innerHTML=''; - prodList.forEach(function (item) { - var opt=document.createElement('option'); - opt.value=item.id; - opt.textContent=item.name + ' - ' + item.val + '원'; - if(item.q === 0) opt.disabled=true; - sel.appendChild(opt); - }); -} -function calcCart() { - totalAmt=0; - itemCnt=0; - var cartItems=cartDisp.children; - var subTot=0; - for (var i=0; i < cartItems.length; i++) { - (function () { - var curItem; - for (var j=0; j < prodList.length; j++) { - if(prodList[j].id === cartItems[i].id) { - curItem=prodList[j]; - break; + +const BULK_PURCHASE_THRESHOLD = 30; +const BULK_PURCHASE_DISCOUNT_RATE = 0.25; +const PROMOTION_CONFIG = { + FLASH_SALE: { + INTERVAL: 30000, + INITIAL_DELAY: 10000, + PROBABILITY: 0.3, + DISCOUNT_RATE: 0.8, + }, + RECOMMENDATION: { + INTERVAL: 60000, + INITIAL_DELAY: 20000, + DISCOUNT_RATE: 0.95, + }, +}; + +/** + * 비즈니스 로직: 상품 관리 + */ +const ProductManager = { + availableProducts: [], + + initialize() { + this.availableProducts = [ + { id: "p1", name: "상품1", price: 10000, stock: 50 }, + { id: "p2", name: "상품2", price: 20000, stock: 30 }, + { id: "p3", name: "상품3", price: 30000, stock: 20 }, + { id: "p4", name: "상품4", price: 15000, stock: 0 }, + { id: "p5", name: "상품5", price: 25000, stock: 10 }, + ]; + }, + + findProduct(id) { + return this.availableProducts.find((p) => p.id === id); + }, + + updateStock(id, change) { + const product = this.findProduct(id); + if (!product) return false; + + const newStock = product.stock + change; + if (newStock < 0) return false; + + product.stock = newStock; + return true; + }, +}; + +/** + * 비즈니스 로직: 할인 관리 + */ +const DiscountManager = { + calculateBaseDiscount(subTotal, totalAmount) { + return subTotal !== 0 ? (subTotal - totalAmount) / subTotal : 0; + }, + + applyBulkPurchaseDiscount(currentRate, itemCount) { + return itemCount >= BULK_PURCHASE_THRESHOLD + ? Math.max(BULK_PURCHASE_DISCOUNT_RATE, currentRate) + : currentRate; + }, + + getCurrentDay() { + return new Date().getDay(); + }, + + applyWeekdayDiscount(currentRate) { + return this.getCurrentDay() === 2 + ? Math.max(0.1, currentRate) + : currentRate; + }, + + calculateTotalDiscount(subTotal, totalAmount, itemCount) { + let rate = this.calculateBaseDiscount(subTotal, totalAmount); + rate = this.applyBulkPurchaseDiscount(rate, itemCount); + rate = this.applyWeekdayDiscount(rate); + return rate; + }, +}; + +/** + * 비즈니스 로직: 장바구니 관리 + */ +const CartManager = { + state: { + lastSelectedProduct: null, + bonusPoints: 0, + itemCount: 0, + }, + + calculateTotals(items) { + const result = items.reduce( + (acc, { id, quantity }) => { + const product = ProductManager.findProduct(id); + if (!product) return acc; + + const itemTotal = product.price * quantity; + let discount = 0; + + // 수량 할인 (10개 이상) + if (quantity >= 10) { + discount = Math.max(discount, PRODUCT_DISCOUNTS[id] || 0); } + + // 화요일 할인 (10%) + if (DiscountManager.getCurrentDay() === 2) { + discount = Math.max(discount, 0.1); + } + + const discountedTotal = itemTotal * (1 - discount); + + return { + subTotal: acc.subTotal + itemTotal, + totalAmount: acc.totalAmount + discountedTotal, + itemCount: acc.itemCount + quantity, + discount: discount > 0 ? discount : acc.discount, + }; + }, + { subTotal: 0, totalAmount: 0, itemCount: 0, discount: 0 } + ); + + this.state.itemCount = result.itemCount; + this.state.bonusPoints = Math.floor(result.subTotal / 1000); + + return result; + }, +}; + +/** + * UI 로직: DOM 관리 + */ +const DOMManager = { + elements: { + root: null, + productSelect: null, + addToCartBtn: null, + cartItems: null, + cartTotal: null, + loyaltyPoints: null, + stockStatus: null, + }, + + // 재고 상태 캐시 추가 + _stockCache: new Map(), + + initialize() { + // 기본 HTML 구조 생성 + document.getElementById("app").innerHTML = ` +

장바구니

+ + +
+
총액: 0원(포인트: 0)
+
(포인트: 0)
+
+ `; + + // DOM 요소 참조 저장 후 초기화 + this.initializeElements(); + this.updateProductSelect(); + this.updateStockDisplay(); + }, + + initializeElements() { + this.elements = { + root: document.getElementById("app"), + productSelect: document.getElementById("product-select"), + addToCartBtn: document.getElementById("add-to-cart"), + cartItems: document.getElementById("cart-items"), + cartTotal: document.getElementById("cart-total"), + loyaltyPoints: document.getElementById("loyalty-points"), + stockStatus: document.getElementById("stock-status"), + }; + }, + + // 상품 선택 목록 업데이트 최적화 + updateProductSelect() { + const select = this.elements.productSelect; + if (!select) return; + + const fragment = document.createDocumentFragment(); + + ProductManager.availableProducts.forEach((product) => { + const option = document.createElement("option"); + option.value = product.id; + option.textContent = `${product.name} - ${product.price}원`; + option.disabled = product.stock <= 0; + fragment.appendChild(option); + }); + + // 기존 옵션을 한 번에 교체 + select.innerHTML = ""; + select.appendChild(fragment); + }, + + // 전체 재고 상태 표시 최적화 + updateStockDisplay() { + const fragment = document.createDocumentFragment(); + const stockContainer = this.elements.stockStatus; + + ProductManager.availableProducts.forEach((product) => { + const stockText = product.stock > 0 ? `${product.stock}개` : "품절"; + const div = document.createElement("div"); + div.setAttribute("data-product-id", product.id); + div.textContent = `${product.name}: ${stockText}`; + fragment.appendChild(div); + + // 캐시 업데이트 + this._stockCache.set(product.id, product.stock); + }); + + // 기존 내용을 한 번에 교체 + stockContainer.innerHTML = ""; + stockContainer.appendChild(fragment); + }, + + // 개별 상품 재고 업데이트 + updateProductStock(productId) { + const product = ProductManager.findProduct(productId); + if (!product) return; + + // 캐시된 재고 상태와 비교해서 없으면 스킵하도록 + const cachedStock = this._stockCache.get(productId); + if (cachedStock === product.stock) return; + + const stockElement = this.elements.stockStatus.querySelector( + `[data-product-id="${productId}"]` + ); + + if (stockElement) { + const stockText = product.stock > 0 ? `${product.stock}개` : "품절"; + if (stockElement.lastStockText !== stockText) { + stockElement.textContent = `${product.name}: ${stockText}`; + stockElement.lastStockText = stockText; + this._stockCache.set(productId, product.stock); } - var q=parseInt(cartItems[i].querySelector('span').textContent.split('x ')[1]); - var itemTot=curItem.val * q; - var disc=0; - itemCnt += q; - subTot += itemTot; - if(q >= 10) { - if(curItem.id === 'p1') disc=0.1; - else if(curItem.id === 'p2') disc=0.15; - else if(curItem.id === 'p3') disc=0.2; - else if(curItem.id === 'p4') disc=0.05; - else if(curItem.id === 'p5') disc=0.25; - } - totalAmt += itemTot * (1 - disc); - })(); - } - let discRate=0; - if(itemCnt >= 30) { - var bulkDisc=totalAmt * 0.25; - var itemDisc=subTot - totalAmt; - if(bulkDisc > itemDisc) { - totalAmt=subTot * (1 - 0.25); - discRate=0.25; - } else { - discRate=(subTot - totalAmt) / subTot; } - } else { - discRate=(subTot - totalAmt) / subTot; - } - if(new Date().getDay() === 2) { - totalAmt *= (1 - 0.1); - discRate=Math.max(discRate, 0.1); - } - sum.textContent='총액: ' + Math.round(totalAmt) + '원'; - if(discRate > 0) { - var span=document.createElement('span'); - span.className='text-green-500 ml-2'; - span.textContent='(' + (discRate * 100).toFixed(1) + '% 할인 적용)'; - sum.appendChild(span); - } - updateStockInfo(); - renderBonusPts(); -} -const renderBonusPts=() => { - bonusPts = Math.floor(totalAmt / 1000); - var ptsTag=document.getElementById('loyalty-points'); - if(!ptsTag) { - ptsTag=document.createElement('span'); - ptsTag.id='loyalty-points'; - ptsTag.className='text-blue-500 ml-2'; - sum.appendChild(ptsTag); - } - ptsTag.textContent='(포인트: ' + bonusPts + ')'; + }, + + createCartItem(product, quantity = 1) { + return ` +
+ ${product.name} - ${product.price}원 x ${quantity} +
+ + + +
+
+ `; + }, }; -function updateStockInfo() { - var infoMsg=''; - prodList.forEach(function (item) { - if(item.q < 5) {infoMsg += item.name + ': ' + (item.q > 0 ? '재고 부족 ('+item.q+'개 남음)' : '품절') + '\n'; + +/** + * UI 로직: 이벤트 처리 + */ +const EventHandler = { + initialize() { + DOMManager.elements.addToCartBtn.addEventListener("click", () => + this.handleAddToCart() + ); + DOMManager.elements.cartItems.addEventListener("click", (e) => + this.handleCartAction(e) + ); + }, + + handleAddToCart() { + const productId = DOMManager.elements.productSelect.value; + const product = ProductManager.findProduct(productId); + + if (!product || product.stock <= 0) return; + + const existingItem = document.getElementById(productId); + if (existingItem) { + this.handleQuantityChange(existingItem, 1); + } else { + if (ProductManager.updateStock(productId, -1)) { + DOMManager.elements.cartItems.insertAdjacentHTML( + "beforeend", + DOMManager.createCartItem(product) + ); + this.updateDisplays(); + CartManager.state.lastSelectedProduct = productId; + } } - }); - stockInfo.textContent=infoMsg; -} -main(); -addBtn.addEventListener('click', function () { - var selItem=sel.value; - var itemToAdd=prodList.find(function (p) { return p.id === selItem; }); - if(itemToAdd && itemToAdd.q > 0) { - var item=document.getElementById(itemToAdd.id); - if(item) { - var newQty=parseInt(item.querySelector('span').textContent.split('x ')[1]) + 1; - if(newQty <= itemToAdd.q) { - item.querySelector('span').textContent=itemToAdd.name + ' - ' + itemToAdd.val + '원 x ' + newQty; - itemToAdd.q--; - } else {alert('재고가 부족합니다.');} + }, + + handleCartAction(event) { + const target = event.target; + if (target.classList.contains("quantity-change")) { + const item = target.closest(".cart-item"); + const change = parseInt(target.dataset.change); + this.handleQuantityChange(item, change); + } else if (target.classList.contains("remove-item")) { + const item = target.closest(".cart-item"); + this.handleRemoveItem(item); + } + }, + + handleQuantityChange(item, change) { + const productId = item.id; + const product = ProductManager.findProduct(productId); + const currentQuantity = parseInt( + item.querySelector("span").textContent.split("x ")[1] + ); + const newQuantity = currentQuantity + change; + + if (newQuantity <= 0) { + this.handleRemoveItem(item); + return; + } + + if (ProductManager.updateStock(productId, -change)) { + item.querySelector( + "span" + ).textContent = `${product.name} - ${product.price}원 x ${newQuantity}`; + this.updateDisplays(); } else { - var newItem=document.createElement('div'); - newItem.id=itemToAdd.id; - newItem.className='flex justify-between items-center mb-2'; - newItem.innerHTML='' + itemToAdd.name + ' - ' + itemToAdd.val + '원 x 1
' + - '' + - '' + - '
'; - cartDisp.appendChild(newItem); - itemToAdd.q--; + alert("재고가 부족합니다."); } - calcCart(); - lastSel=selItem; - } -}); -cartDisp.addEventListener('click', function (event) { - var tgt=event.target; - if(tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) { - var prodId=tgt.dataset.productId; - var itemElem=document.getElementById(prodId); - var prod=prodList.find(function (p) { return p.id === prodId; }); - if(tgt.classList.contains('quantity-change')) { - var qtyChange=parseInt(tgt.dataset.change); - var newQty=parseInt(itemElem.querySelector('span').textContent.split('x ')[1]) + qtyChange; - if(newQty > 0 && newQty <= prod.q + parseInt(itemElem.querySelector('span').textContent.split('x ')[1])) { - itemElem.querySelector('span').textContent=itemElem.querySelector('span').textContent.split('x ')[0] + 'x ' + newQty; - prod.q -= qtyChange; - } else if(newQty <= 0) { - itemElem.remove(); - prod.q -= qtyChange; - } else { - alert('재고가 부족합니다.'); - } - } else if(tgt.classList.contains('remove-item')) { - var remQty=parseInt(itemElem.querySelector('span').textContent.split('x ')[1]); - prod.q += remQty; - itemElem.remove(); + }, + + handleRemoveItem(item) { + const productId = item.id; + const quantity = parseInt( + item.querySelector("span").textContent.split("x ")[1] + ); + ProductManager.updateStock(productId, quantity); + item.remove(); + this.updateDisplays(); + }, + + updateDisplays() { + const cartItems = Array.from(DOMManager.elements.cartItems.children).map( + (item) => ({ + id: item.id, + quantity: parseInt( + item.querySelector("span").textContent.split("x ")[1] + ), + }) + ); + + const totals = CartManager.calculateTotals(cartItems); + const points = Math.floor(totals.totalAmount / 1000); + + let displayText = `총액: ${Math.round(totals.totalAmount)}원`; + if (totals.discount > 0) { + displayText += `(${(totals.discount * 100).toFixed(1)}% 할인 적용)`; } - calcCart(); + displayText += `(포인트: ${points})`; + + DOMManager.elements.cartTotal.textContent = displayText; + DOMManager.elements.loyaltyPoints.textContent = `(포인트: ${points})`; + DOMManager.updateProductStock(CartManager.state.lastSelectedProduct); + }, +}; + +/** + * 프로모션 관리 + */ +const PromotionManager = { + initialize() { + this.setupFlashSale(); + this.setupRecommendation(); + }, + + // 번개세일 프로모션 + setupFlashSale() { + const config = PROMOTION_CONFIG.FLASH_SALE; + setTimeout(() => { + setInterval(() => { + if (Math.random() < config.PROBABILITY) { + const products = ProductManager.availableProducts.filter( + (p) => p.stock > 0 + ); + const product = products[Math.floor(Math.random() * products.length)]; + if (product) { + product.price = Math.round(product.price * config.DISCOUNT_RATE); + alert(`번개세일! ${product.name}이(가) 20% 할인 중입니다!`); + DOMManager.updateProductSelect(); + } + } + }, config.INTERVAL); + }, config.INITIAL_DELAY); + }, + + // 추천 프로모션 + setupRecommendation() { + const config = PROMOTION_CONFIG.RECOMMENDATION; + setTimeout(() => { + setInterval(() => { + if (CartManager.state.lastSelectedProduct) { + const products = ProductManager.availableProducts.filter( + (p) => p.id !== CartManager.state.lastSelectedProduct && p.stock > 0 + ); + const product = products[Math.floor(Math.random() * products.length)]; + if (product) { + product.price = Math.round(product.price * config.DISCOUNT_RATE); + alert( + `${product.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!` + ); + DOMManager.updateProductSelect(); + } + } + }, config.INTERVAL); + }, config.INITIAL_DELAY); + }, +}; + +// 애플리케이션 초기화 +function main() { + ProductManager.initialize(); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + DOMManager.initialize(); + EventHandler.initialize(); + PromotionManager.initialize(); + }); + } else { + DOMManager.initialize(); + EventHandler.initialize(); + PromotionManager.initialize(); } -}); \ No newline at end of file +} + +// 앱 실행 +main(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..fc0c4929 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "strict": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..9d31e2ae --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 19977832..00000000 --- a/vite.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'jsdom', - setupFiles: './src/setupTests.js' - }, -}) diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..9276770d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + rollupOptions: { + input: { + advanced: './index.advanced.html', + basic: './index.basic.html', + }, + }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/setupTests.js', + }, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + server: { + open: './index.advanced.html', + }, +})