diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6b23b65 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: Release Extension + +concurrency: + group: ext-release + cancel-in-progress: true + +on: + push: + tags: + - 'v*-ext' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate tag is on prod branch + run: | + git fetch origin prod + if git merge-base --is-ancestor ${{ github.sha }} origin/prod; then + echo "이 태그는 prod branch에 포함되어 있습니다. 릴리즈를 진행합니다." + else + echo "Error: 이 tag는 prod branch에 없습니다." + exit 1 + fi + + - name: Validate version match + run: | + TAG_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//' | sed 's/-ext$//') + MANIFEST_VERSION=$(node -p "require('./public/manifest.json').version") + + echo "Tag version: $TAG_VERSION" + echo "Manifest version: $MANIFEST_VERSION" + + if [ "$TAG_VERSION" != "$MANIFEST_VERSION" ]; then + echo "Error: 태그 버전($TAG_VERSION)과 manifest.json 버전($MANIFEST_VERSION)이 일치하지 않습니다." + exit 1 + fi + + echo "버전이 일치합니다." + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test:run + + - name: Build extension + run: npm run build + env: + VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }} + + - name: Create ZIP file + run: | + cd dist + zip -r ../recycle-study-extension-${{ github.ref_name }}.zip . + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: recycle-study-extension-${{ github.ref_name }}.zip + generate_release_notes: true + draft: true diff --git a/.github/workflows/test-validation.yml b/.github/workflows/test-validation.yml new file mode 100644 index 0000000..7f1f5da --- /dev/null +++ b/.github/workflows/test-validation.yml @@ -0,0 +1,32 @@ +name: Test & Build Validation + +on: + pull_request: + types: + [ opened, synchronize, reopened ] + branches: + - dev + - prod + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test:run + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 527cb86..16aadca 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,9 @@ out/ ### .env ### .env +.env.* + +### Node.js ### +node_modules/ +dist/ +*.log diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..79ac838 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1911 @@ +{ + "name": "recycle-study-extension", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "recycle-study-extension", + "version": "1.0.0", + "devDependencies": { + "vite": "^5.4.0", + "vitest": "^4.0.16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "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/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "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/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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/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/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "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.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "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/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.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f8315d --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "recycle-study-extension", + "version": "1.0.0", + "description": "복습 URL 저장 크롬 익스텐션", + "type": "module", + "scripts": { + "dev": "vite build --watch", + "build": "vite build", + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" + }, + "devDependencies": { + "vite": "^5.4.0", + "vitest": "^4.0.16" + } +} diff --git a/public/icons/icon128.png b/public/icons/icon128.png new file mode 100644 index 0000000..421194b Binary files /dev/null and b/public/icons/icon128.png differ diff --git a/public/icons/icon16.png b/public/icons/icon16.png new file mode 100644 index 0000000..f0e4d36 Binary files /dev/null and b/public/icons/icon16.png differ diff --git a/public/icons/icon48.png b/public/icons/icon48.png new file mode 100644 index 0000000..db5747e Binary files /dev/null and b/public/icons/icon48.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..87be2e1 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,32 @@ +{ + "manifest_version": 3, + "name": "Recycle Study", + "version": "1.0.0", + "description": "복습 URL을 저장하고 스케줄에 따라 알림을 받는 익스텐션", + "permissions": [ + "storage", + "activeTab", + "tabs" + ], + "host_permissions": [ + "https://api.recycle-study.site/*", + "http://localhost:8080/*" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/public/popup.css b/public/popup.css new file mode 100644 index 0000000..986e745 --- /dev/null +++ b/public/popup.css @@ -0,0 +1,333 @@ +/* 기본 스타일 초기화 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.5; + color: #333; + background-color: #fff; +} + +.container { + width: 320px; + min-height: 200px; + padding: 16px; +} + +/* 헤더 */ +.header { + text-align: center; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #e0e0e0; +} + +.header h1 { + font-size: 18px; + font-weight: 600; + color: #2c3e50; +} + +/* 뷰 전환 */ +.view { + display: block; +} + +.hidden { + display: none !important; +} + +/* 폼 요소 */ +.form-group { + margin-bottom: 12px; +} + +.form-group label { + display: block; + margin-bottom: 4px; + font-size: 12px; + font-weight: 500; + color: #666; +} + +.form-group input { + width: 100%; + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: #3498db; +} + +/* 버튼 */ +.btn { + display: block; + width: 100%; + padding: 10px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, opacity 0.2s; +} + +.btn:hover { + opacity: 0.9; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: #3498db; + color: #fff; +} + +.btn-primary:hover { + background-color: #2980b9; +} + +.btn-secondary { + background-color: #ecf0f1; + color: #333; +} + +.btn-secondary:hover { + background-color: #d5dbdb; +} + +.btn-text { + background: none; + color: #7f8c8d; + font-size: 12px; + margin-top: 8px; +} + +.btn-text:hover { + color: #e74c3c; +} + +.btn-large { + padding: 14px 16px; + font-size: 16px; +} + +.btn-danger { + background-color: #e74c3c; + color: #fff; + padding: 6px 12px; + font-size: 12px; + width: auto; +} + +/* 힌트 텍스트 */ +.hint { + margin-top: 12px; + font-size: 12px; + color: #95a5a6; + text-align: center; +} + +/* 인증 대기 화면 */ +.pending-message { + text-align: center; + padding: 20px 0; +} + +.pending-message p { + margin-bottom: 8px; +} + +.email-display { + font-weight: 600; + color: #3498db; +} + +/* 메인 화면 */ +.user-info { + text-align: center; + margin-bottom: 16px; + padding: 8px; + background-color: #f8f9fa; + border-radius: 6px; + font-size: 12px; + color: #666; +} + +/* 저장 결과 */ +.result { + margin-top: 16px; + padding: 12px; + background-color: #d5f5e3; + border-radius: 6px; +} + +.result-title { + font-weight: 600; + color: #27ae60; + margin-bottom: 8px; +} + +.schedule-list { + font-size: 12px; + color: #333; +} + +.schedule-list p { + margin-bottom: 4px; + font-weight: 500; +} + +.schedule-list ul { + list-style: none; + padding-left: 0; +} + +.schedule-list li { + padding: 4px 0; + color: #555; +} + +/* 구분선 */ +.divider { + border: none; + border-top: 1px solid #e0e0e0; + margin: 16px 0; +} + +/* 디바이스 목록 */ +.devices-list { + list-style: none; + margin-top: 12px; +} + +.devices-list li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + margin-bottom: 8px; + background-color: #f8f9fa; + border-radius: 6px; + font-size: 12px; +} + +.device-info { + flex: 1; +} + +.device-id { + font-weight: 500; + word-break: break-all; +} + +.device-date { + color: #95a5a6; + font-size: 11px; +} + +.current-device { + color: #27ae60; + font-size: 10px; + font-weight: 600; +} + +/* 메시지 영역 */ +.message-area { + margin-top: 12px; + padding: 10px; + border-radius: 6px; + font-size: 13px; + text-align: center; +} + +.message-area.error { + background-color: #fadbd8; + color: #c0392b; +} + +.message-area.success { + background-color: #d5f5e3; + color: #27ae60; +} + +.message-area.info { + background-color: #d6eaf8; + color: #2980b9; +} + +/* 로딩 */ +.loading { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 30px; + height: 30px; + border: 3px solid #e0e0e0; + border-top-color: #3498db; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 확인 다이얼로그 */ +.confirm-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.confirm-content { + background: #fff; + padding: 20px; + border-radius: 8px; + text-align: center; + max-width: 280px; +} + +.confirm-content p { + margin-bottom: 16px; +} + +.confirm-buttons { + display: flex; + gap: 8px; +} + +.confirm-buttons .btn { + flex: 1; +} diff --git a/public/popup.html b/public/popup.html new file mode 100644 index 0000000..60f7628 --- /dev/null +++ b/public/popup.html @@ -0,0 +1,76 @@ + + + + + + Recycle Study + + + +
+
+

Recycle Study

+
+ + +
+
+ + +
+ +

이메일 인증 후 서비스를 이용할 수 있습니다.

+
+ + + + + + + + + + + + +
+ + + + diff --git a/src/__tests__/errors.test.js b/src/__tests__/errors.test.js new file mode 100644 index 0000000..416340c --- /dev/null +++ b/src/__tests__/errors.test.js @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { ERROR_CODES } from '../constants.js'; +import { + ApiError, + getErrorCodeFromStatus, + getErrorMessage, + isLogoutRequiredError +} from '../errors.js'; + +describe('ApiError', () => { + it('code와 message를 저장한다', () => { + const error = new ApiError(ERROR_CODES.UNAUTHORIZED, '인증 실패'); + expect(error.code).toBe(ERROR_CODES.UNAUTHORIZED); + expect(error.message).toBe('인증 실패'); + expect(error.name).toBe('ApiError'); + }); +}); + +describe('getErrorCodeFromStatus', () => { + it('401은 UNAUTHORIZED를 반환한다', () => { + expect(getErrorCodeFromStatus(401)).toBe(ERROR_CODES.UNAUTHORIZED); + }); + + it('404는 NOT_FOUND를 반환한다', () => { + expect(getErrorCodeFromStatus(404)).toBe(ERROR_CODES.NOT_FOUND); + }); + + it('400은 BAD_REQUEST를 반환한다', () => { + expect(getErrorCodeFromStatus(400)).toBe(ERROR_CODES.BAD_REQUEST); + }); + + it('5xx는 SERVER_ERROR를 반환한다', () => { + expect(getErrorCodeFromStatus(500)).toBe(ERROR_CODES.SERVER_ERROR); + expect(getErrorCodeFromStatus(503)).toBe(ERROR_CODES.SERVER_ERROR); + }); +}); + +describe('isLogoutRequiredError', () => { + it('UNAUTHORIZED는 로그아웃 필요', () => { + expect(isLogoutRequiredError(ERROR_CODES.UNAUTHORIZED)).toBe(true); + }); + + it('NOT_FOUND는 로그아웃 필요', () => { + expect(isLogoutRequiredError(ERROR_CODES.NOT_FOUND)).toBe(true); + }); + + it('NETWORK_ERROR는 로그아웃 불필요', () => { + expect(isLogoutRequiredError(ERROR_CODES.NETWORK_ERROR)).toBe(false); + }); + + it('SERVER_ERROR는 로그아웃 불필요', () => { + expect(isLogoutRequiredError(ERROR_CODES.SERVER_ERROR)).toBe(false); + }); +}); diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js new file mode 100644 index 0000000..82d7519 --- /dev/null +++ b/src/__tests__/utils.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { isValidEmail, formatDate } from '../utils.js'; + +describe('isValidEmail', () => { + it('유효한 이메일 형식을 통과시킨다', () => { + expect(isValidEmail('test@example.com')).toBe(true); + expect(isValidEmail('user.name@domain.co.kr')).toBe(true); + }); + + it('잘못된 이메일 형식을 거부한다', () => { + expect(isValidEmail('invalid')).toBe(false); + expect(isValidEmail('no-at-sign.com')).toBe(false); + expect(isValidEmail('@no-local.com')).toBe(false); + expect(isValidEmail('no-domain@')).toBe(false); + }); + + it('빈 문자열을 거부한다', () => { + expect(isValidEmail('')).toBe(false); + }); +}); + +describe('formatDate', () => { + it('ISO 날짜 문자열을 한국어 형식으로 변환한다', () => { + const result = formatDate('2024-01-15T10:30:00'); + expect(result).toContain('2024'); + expect(result).toContain('1월'); + expect(result).toContain('15'); + }); +}); diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..40bc5ee --- /dev/null +++ b/src/api.js @@ -0,0 +1,87 @@ +/** + * API 호출 관련 함수 + * + * 서버와의 통신을 담당하며, 디바이스 등록/조회/삭제, 복습 URL 저장 등의 API를 제공한다. + * 모든 API 요청은 background.js를 통해 처리되어 CORS 문제를 우회한다. + */ + +import { ERROR_CODES } from './constants.js'; +import { ApiError, getErrorCodeFromStatus } from './errors.js'; + +/** + * Background Script로 API 요청 전송 + * @param {Object} request - API 요청 정보 + * @returns {Promise} 응답 데이터 + * @throws {ApiError} + */ +async function sendApiRequest(request) { + const response = await chrome.runtime.sendMessage({ + type: 'API_REQUEST', + request + }); + + if (!response.success) { + if (response.isNetworkError) { + throw new ApiError(ERROR_CODES.NETWORK_ERROR, response.message); + } + const errorCode = getErrorCodeFromStatus(response.status); + throw new ApiError(errorCode, response.message); + } + + return response.data; +} + +/** + * 디바이스 등록 (회원가입) + * @param {string} email - 사용자 이메일 + * @returns {Promise} { email, identifier } + */ +export async function registerDevice(email) { + return await sendApiRequest({ + endpoint: '/api/v1/members', + method: 'POST', + body: { email } + }); +} + +/** + * 디바이스 목록 조회 + * @param {string} email - 사용자 이메일 + * @param {string} identifier - 디바이스 식별자 + * @returns {Promise} { email, devices } + */ +export async function getDevices(email, identifier) { + return await sendApiRequest({ + endpoint: '/api/v1/members', + method: 'GET', + params: { email, identifier } + }); +} + +/** + * 디바이스 삭제 + * @param {string} email - 사용자 이메일 + * @param {string} deviceIdentifier - 현재 디바이스 식별자 + * @param {string} targetDeviceIdentifier - 삭제할 디바이스 식별자 + */ +export async function deleteDevice(email, deviceIdentifier, targetDeviceIdentifier) { + return await sendApiRequest({ + endpoint: '/api/v1/device', + method: 'DELETE', + body: { email, deviceIdentifier, targetDeviceIdentifier } + }); +} + +/** + * 복습 URL 저장 + * @param {string} identifier - 디바이스 식별자 + * @param {string} targetUrl - 저장할 URL + * @returns {Promise} { url, scheduledAts } + */ +export async function saveReviewUrl(identifier, targetUrl) { + return await sendApiRequest({ + endpoint: '/api/v1/reviews', + method: 'POST', + body: { identifier, targetUrl } + }); +} diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..bcf430a --- /dev/null +++ b/src/background.js @@ -0,0 +1,82 @@ +/** + * 백그라운드 서비스 워커 + * + * 익스텐션 설치/업데이트 이벤트 처리 및 popup과의 메시지 통신을 담당한다. + * API 요청은 CORS 우회를 위해 이 서비스 워커에서 처리한다. + */ + +import { CONFIG } from './config.js'; + +// ============================================ +// 익스텐션 설치/업데이트 이벤트 +// ============================================ +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + console.log('Recycle Study 익스텐션이 설치되었습니다.'); + } else if (details.reason === 'update') { + console.log('Recycle Study 익스텐션이 업데이트되었습니다.'); + } +}); + +// ============================================ +// API 프록시 핸들러 +// ============================================ +async function handleApiRequest(request) { + const { endpoint, method = 'GET', body, params } = request; + + let url = `${CONFIG.BASE_URL}${endpoint}`; + if (params) { + url += `?${new URLSearchParams(params)}`; + } + + const options = { + method, + headers: { 'Content-Type': 'application/json' } + }; + + if (body) { + options.body = JSON.stringify(body); + } + + try { + const response = await fetch(url, options); + + // 204 No Content + if (response.status === 204) { + return { success: true, data: null }; + } + + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error('Failed to parse JSON response:', parseError); + data = { message: 'Invalid JSON response from server.' }; + } + + if (!response.ok) { + console.error('API request failed:', { url, status: response.status, data }); + return { success: false, status: response.status, message: data.message }; + } + + return { success: true, data }; + } catch (error) { + console.error('Network request failed:', { url, error }); + return { success: false, status: 0, message: error.message, isNetworkError: true }; + } +} + +// ============================================ +// 메시지 리스너 (popup과 통신) +// ============================================ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'API_REQUEST') { + handleApiRequest(message.request) + .then(sendResponse); + return true; // 비동기 응답을 위해 true 반환 + } + + if (message.type === 'CHECK_AUTH') { + sendResponse({ success: true }); + } +}); diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..943b4c2 --- /dev/null +++ b/src/config.js @@ -0,0 +1,16 @@ +/** + * 환경 설정 + * + * 개발: vite build (.env.development 사용, 기본값) + * 프로덕션: vite build --mode production (.env.production 사용) + */ +export const CONFIG = { + BASE_URL: import.meta.env.VITE_BASE_URL || 'http://localhost:8080', + ENV: import.meta.env.MODE || 'development' +}; + +export const STORAGE_KEYS = { + EMAIL: 'email', + IDENTIFIER: 'identifier', + IS_AUTHENTICATED: 'isAuthenticated' +}; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..13a535f --- /dev/null +++ b/src/constants.js @@ -0,0 +1,25 @@ +/** + * 상수 정의 + * + * 에러 코드 및 자동 로그아웃이 필요한 에러 목록을 정의한다. + */ +export const ERROR_CODES = { + // 로그아웃이 필요한 에러 + UNAUTHORIZED: 'UNAUTHORIZED', // 401: 인증되지 않은 디바이스 + NOT_FOUND: 'NOT_FOUND', // 404: 존재하지 않는 리소스 + INVALID_STORAGE: 'INVALID_STORAGE', // 스토리지 데이터 손상 + + // 로그아웃 불필요한 에러 + BAD_REQUEST: 'BAD_REQUEST', // 400: 잘못된 요청 + SERVER_ERROR: 'SERVER_ERROR', // 5xx: 서버 오류 + NETWORK_ERROR: 'NETWORK_ERROR' // 네트워크 연결 실패 +}; + +/** + * 자동 로그아웃이 필요한 에러 코드 + */ +export const LOGOUT_REQUIRED_ERRORS = [ + ERROR_CODES.UNAUTHORIZED, + ERROR_CODES.NOT_FOUND, + ERROR_CODES.INVALID_STORAGE +]; diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..bc1a7af --- /dev/null +++ b/src/errors.js @@ -0,0 +1,63 @@ +/** + * 에러 처리 관련 함수 + * + * ApiError 클래스, HTTP 상태 코드 변환, 사용자 메시지 생성, 로그아웃 필요 여부 판단 등을 제공한다. + */ + +import { ERROR_CODES, LOGOUT_REQUIRED_ERRORS } from './constants.js'; + +/** + * API 에러 클래스 + */ +export class ApiError extends Error { + constructor(code, message) { + super(message); + this.code = code; + this.name = 'ApiError'; + } +} + +/** + * HTTP 상태 코드를 에러 코드로 변환 + * @param {number} status - HTTP 상태 코드 + * @returns {string} 에러 코드 + */ +export function getErrorCodeFromStatus(status) { + if (status === 401) return ERROR_CODES.UNAUTHORIZED; + if (status === 404) return ERROR_CODES.NOT_FOUND; + if (status === 400) return ERROR_CODES.BAD_REQUEST; + if (status >= 500) return ERROR_CODES.SERVER_ERROR; + return ERROR_CODES.BAD_REQUEST; +} + +/** + * 에러 코드에 따른 사용자 메시지 생성 + * @param {string} code - 에러 코드 + * @param {string} serverMessage - 서버에서 받은 메시지 + * @returns {string} 사용자에게 표시할 메시지 + */ +export function getErrorMessage(code, serverMessage) { + switch (code) { + case ERROR_CODES.UNAUTHORIZED: + return '인증 정보가 유효하지 않습니다. 다시 로그인해주세요.'; + case ERROR_CODES.NOT_FOUND: + return '계정 정보를 찾을 수 없습니다. 다시 등록해주세요.'; + case ERROR_CODES.INVALID_STORAGE: + return '저장된 정보가 손상되었습니다. 다시 로그인해주세요.'; + case ERROR_CODES.SERVER_ERROR: + return '서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.'; + case ERROR_CODES.NETWORK_ERROR: + return '서버에 연결할 수 없습니다. 네트워크를 확인해주세요.'; + default: + return serverMessage || '오류가 발생했습니다.'; + } +} + +/** + * 로그아웃이 필요한 에러인지 확인 + * @param {string} code - 에러 코드 + * @returns {boolean} + */ +export function isLogoutRequiredError(code) { + return LOGOUT_REQUIRED_ERRORS.includes(code); +} diff --git a/src/handlers.js b/src/handlers.js new file mode 100644 index 0000000..9f9878e --- /dev/null +++ b/src/handlers.js @@ -0,0 +1,232 @@ +/** + * 이벤트 핸들러 + * + * 디바이스 등록, 인증 확인, URL 저장, 디바이스 관리, 로그아웃 등 + * 사용자 액션에 대한 핸들러 함수를 정의한다. + */ + +import { STORAGE_KEYS } from './config.js'; +import { ERROR_CODES } from './constants.js'; +import { registerDevice, getDevices, deleteDevice, saveReviewUrl } from './api.js'; +import { setStorageData, clearStorage, validateStorageForAuth } from './storage.js'; +import { + elements, + showLoading, + hideLoading, + showMessage, + showView, + handleApiError +} from './ui.js'; +import { formatDate, isValidEmail } from './utils.js'; + +/** + * 디바이스 등록 버튼 클릭 핸들러 + */ +export async function handleRegister() { + const email = elements.emailInput.value.trim(); + + if (!email) { + showMessage('이메일을 입력해주세요.', 'error'); + return; + } + + if (!isValidEmail(email)) { + showMessage('유효한 이메일 형식이 아닙니다.', 'error'); + return; + } + + try { + showLoading(); + const result = await registerDevice(email); + + await setStorageData({ + [STORAGE_KEYS.EMAIL]: result.email, + [STORAGE_KEYS.IDENTIFIER]: result.identifier, + [STORAGE_KEYS.IS_AUTHENTICATED]: false + }); + + elements.emailDisplay.textContent = result.email; + showView('pending'); + showMessage('이메일로 인증 링크가 전송되었습니다.', 'success'); + } catch (error) { + showMessage(error.message, 'error'); + } finally { + hideLoading(); + } +} + +/** + * 인증 확인 버튼 클릭 핸들러 + */ +export async function handleCheckAuth() { + try { + showLoading(); + const storageData = await validateStorageForAuth(); + const result = await getDevices(storageData.email, storageData.identifier); + + await setStorageData({ + [STORAGE_KEYS.IS_AUTHENTICATED]: true + }); + + elements.userEmail.textContent = result.email; + showView('main'); + showMessage('인증이 완료되었습니다!', 'success'); + } catch (error) { + if (error.code === ERROR_CODES.UNAUTHORIZED) { + showMessage('아직 인증이 완료되지 않았습니다.', 'info'); + } else { + await handleApiError(error); + } + } finally { + hideLoading(); + } +} + +/** + * 다른 이메일로 등록 버튼 클릭 핸들러 + */ +export async function handleReset() { + await clearStorage(); + elements.emailInput.value = ''; + showView('login'); +} + +/** + * URL 저장 버튼 클릭 핸들러 + */ +export async function handleSaveUrl() { + try { + showLoading(); + + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (!tab?.url) { + showMessage('현재 페이지의 URL을 가져올 수 없습니다.', 'error'); + return; + } + + const storageData = await validateStorageForAuth(); + const result = await saveReviewUrl(storageData.identifier, tab.url); + + elements.scheduleDates.innerHTML = ''; + result.scheduledAts.forEach(date => { + const li = document.createElement('li'); + li.textContent = formatDate(date); + elements.scheduleDates.appendChild(li); + }); + + elements.saveResult.classList.remove('hidden'); + showMessage('저장되었습니다!', 'success'); + } catch (error) { + await handleApiError(error); + } finally { + hideLoading(); + } +} + +/** + * 디바이스 관리 버튼 클릭 핸들러 + */ +export async function handleShowDevices() { + const isVisible = !elements.devicesSection.classList.contains('hidden'); + + if (isVisible) { + elements.devicesSection.classList.add('hidden'); + return; + } + + try { + showLoading(); + const storageData = await validateStorageForAuth(); + const result = await getDevices(storageData.email, storageData.identifier); + + elements.devicesList.innerHTML = ''; + + result.devices.forEach(device => { + const li = document.createElement('li'); + const isCurrentDevice = device.identifier === storageData.identifier; + + const deviceInfo = document.createElement('div'); + deviceInfo.className = 'device-info'; + + const deviceIdDiv = document.createElement('div'); + deviceIdDiv.className = 'device-id'; + deviceIdDiv.textContent = device.identifier.substring(0, 20) + '...'; + deviceInfo.appendChild(deviceIdDiv); + + const deviceDateDiv = document.createElement('div'); + deviceDateDiv.className = 'device-date'; + deviceDateDiv.textContent = formatDate(device.createdAt); + deviceInfo.appendChild(deviceDateDiv); + + if (isCurrentDevice) { + const currentDeviceDiv = document.createElement('div'); + currentDeviceDiv.className = 'current-device'; + currentDeviceDiv.textContent = '현재 디바이스'; + deviceInfo.appendChild(currentDeviceDiv); + } + + li.appendChild(deviceInfo); + + if (!isCurrentDevice) { + const deleteButton = document.createElement('button'); + deleteButton.className = 'btn btn-danger'; + deleteButton.setAttribute('data-id', device.identifier); + deleteButton.textContent = '삭제'; + li.appendChild(deleteButton); + } + + elements.devicesList.appendChild(li); + }); + + elements.devicesSection.classList.remove('hidden'); + } catch (error) { + await handleApiError(error); + } finally { + hideLoading(); + } +} + +/** + * 디바이스 삭제 클릭 핸들러 + * @param {string} targetIdentifier - 삭제할 디바이스 식별자 + */ +export async function handleDeleteDevice(targetIdentifier) { + if (!confirm('이 디바이스를 삭제하시겠습니까?')) { + return; + } + + try { + showLoading(); + const storageData = await validateStorageForAuth(); + + await deleteDevice( + storageData.email, + storageData.identifier, + targetIdentifier + ); + + showMessage('디바이스가 삭제되었습니다.', 'success'); + elements.devicesSection.classList.add('hidden'); + await handleShowDevices(); + } catch (error) { + await handleApiError(error); + } finally { + hideLoading(); + } +} + +/** + * 로그아웃 버튼 클릭 핸들러 + */ +export async function handleLogout() { + if (!confirm('로그아웃 하시겠습니까?')) { + return; + } + + await clearStorage(); + elements.saveResult.classList.add('hidden'); + elements.devicesSection.classList.add('hidden'); + showView('login'); + showMessage('로그아웃 되었습니다.', 'info'); +} diff --git a/src/popup.js b/src/popup.js new file mode 100644 index 0000000..b72c286 --- /dev/null +++ b/src/popup.js @@ -0,0 +1,90 @@ +/** + * 팝업 진입점 + * + * DOM 로드 후 UI 초기화, 이벤트 리스너 등록, 인증 상태에 따른 뷰 전환을 수행한다. + */ + +import { getStorageData, clearStorage, isStorageDataValid } from './storage.js'; +import { + elements, + initializeElements, + showView, + showMessage +} from './ui.js'; +import { + handleRegister, + handleCheckAuth, + handleReset, + handleSaveUrl, + handleShowDevices, + handleDeleteDevice, + handleLogout +} from './handlers.js'; + +/** + * 이벤트 리스너 등록 + */ +function setupEventListeners() { + elements.registerBtn.addEventListener('click', handleRegister); + elements.checkAuthBtn.addEventListener('click', handleCheckAuth); + elements.resetBtn.addEventListener('click', handleReset); + elements.saveUrlBtn.addEventListener('click', handleSaveUrl); + elements.showDevicesBtn.addEventListener('click', handleShowDevices); + elements.logoutBtn.addEventListener('click', handleLogout); + + // 디바이스 삭제 버튼 (이벤트 위임) + elements.devicesList.addEventListener('click', (e) => { + if (e.target.classList.contains('btn-danger')) { + const targetId = e.target.dataset.id; + handleDeleteDevice(targetId); + } + }); + + // 엔터키로 등록 + elements.emailInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handleRegister(); + } + }); +} + +/** + * 앱 초기화 + */ +async function initialize() { + try { + const storageData = await getStorageData(); + + // 스토리지 데이터 유효성 검증 + if (!isStorageDataValid(storageData)) { + console.warn('손상된 스토리지 데이터 감지, 초기화 진행'); + await clearStorage(); + showView('login'); + showMessage('저장된 정보에 문제가 있어 초기화되었습니다.', 'info'); + return; + } + + if (storageData.isAuthenticated) { + elements.userEmail.textContent = storageData.email; + showView('main'); + } else if (storageData.email && storageData.identifier) { + elements.emailDisplay.textContent = storageData.email; + showView('pending'); + } else { + showView('login'); + } + } catch (error) { + console.error('초기화 오류:', error); + await clearStorage(); + showView('login'); + } +} + +/** + * DOM 로드 후 실행 + */ +document.addEventListener('DOMContentLoaded', () => { + initializeElements(); + setupEventListeners(); + initialize(); +}); diff --git a/src/storage.js b/src/storage.js new file mode 100644 index 0000000..c73ac16 --- /dev/null +++ b/src/storage.js @@ -0,0 +1,83 @@ +/** + * Chrome Storage 관련 함수 + * + * 로컬 스토리지를 통해 이메일, 디바이스 식별자, 인증 상태를 저장하고 관리한다. + */ + +import { STORAGE_KEYS } from './config.js'; +import { ERROR_CODES } from './constants.js'; +import { ApiError } from './errors.js'; + +/** + * 스토리지에서 데이터 가져오기 + * @returns {Promise} 저장된 데이터 + */ +export async function getStorageData() { + return await chrome.storage.local.get([ + STORAGE_KEYS.EMAIL, + STORAGE_KEYS.IDENTIFIER, + STORAGE_KEYS.IS_AUTHENTICATED + ]); +} + +/** + * 스토리지에 데이터 저장 + * @param {Object} data - 저장할 데이터 + */ +export async function setStorageData(data) { + await chrome.storage.local.set(data); +} + +/** + * 스토리지 초기화 + */ +export async function clearStorage() { + await chrome.storage.local.remove([ + STORAGE_KEYS.EMAIL, + STORAGE_KEYS.IDENTIFIER, + STORAGE_KEYS.IS_AUTHENTICATED + ]); +} + +/** + * 스토리지 데이터 유효성 검증 + * @param {Object} data - 검증할 데이터 + * @returns {boolean} 유효하면 true + */ +export function isStorageDataValid(data) { + // 인증 완료 상태라면 email과 identifier가 모두 있어야 함 + if (data.isAuthenticated) { + return !!(data.email && data.identifier); + } + + // 인증 대기 상태 (email, identifier 있고 isAuthenticated는 false) + if (data.email && data.identifier) { + return true; + } + + // 미등록 상태 (모두 없으면 정상) + if (!data.email && !data.identifier && !data.isAuthenticated) { + return true; + } + + // 일부만 있는 경우는 손상된 상태 + return false; +} + +/** + * 인증이 필요한 작업 전 스토리지 검증 + * @throws {ApiError} 스토리지가 손상된 경우 + * @returns {Promise} 검증된 스토리지 데이터 + */ +export async function validateStorageForAuth() { + const data = await getStorageData(); + + if (!data.email || !data.identifier) { + throw new ApiError( + ERROR_CODES.INVALID_STORAGE, + '저장된 인증 정보가 없습니다.' + ); + } + + return data; +} diff --git a/src/ui.js b/src/ui.js new file mode 100644 index 0000000..481b19e --- /dev/null +++ b/src/ui.js @@ -0,0 +1,153 @@ +/** + * UI 관련 함수 + * + * DOM 요소 캐싱, 로딩/메시지 표시, 뷰 전환, 에러 처리 등 + * 화면 표시와 관련된 기능을 담당한다. + */ + +import { clearStorage } from './storage.js'; +import { getErrorMessage, isLogoutRequiredError } from './errors.js'; +import { ERROR_CODES } from './constants.js'; + +/** + * DOM 요소 캐시 + */ +export const elements = { + // 뷰 + loginView: null, + pendingView: null, + mainView: null, + + // 로그인 화면 + emailInput: null, + registerBtn: null, + + // 인증 대기 화면 + emailDisplay: null, + checkAuthBtn: null, + resetBtn: null, + + // 메인 화면 + userEmail: null, + saveUrlBtn: null, + saveResult: null, + scheduleDates: null, + showDevicesBtn: null, + devicesSection: null, + devicesList: null, + logoutBtn: null, + + // 공통 + messageArea: null, + loading: null +}; + +/** + * DOM 요소 초기화 + */ +export function initializeElements() { + elements.loginView = document.getElementById('login-view'); + elements.pendingView = document.getElementById('pending-view'); + elements.mainView = document.getElementById('main-view'); + + elements.emailInput = document.getElementById('email-input'); + elements.registerBtn = document.getElementById('register-btn'); + + elements.emailDisplay = document.querySelector('.email-display'); + elements.checkAuthBtn = document.getElementById('check-auth-btn'); + elements.resetBtn = document.getElementById('reset-btn'); + + elements.userEmail = document.getElementById('user-email'); + elements.saveUrlBtn = document.getElementById('save-url-btn'); + elements.saveResult = document.getElementById('save-result'); + elements.scheduleDates = document.getElementById('schedule-dates'); + elements.showDevicesBtn = document.getElementById('show-devices-btn'); + elements.devicesSection = document.getElementById('devices-section'); + elements.devicesList = document.getElementById('devices-list'); + elements.logoutBtn = document.getElementById('logout-btn'); + + elements.messageArea = document.getElementById('message-area'); + elements.loading = document.getElementById('loading'); +} + +/** + * 로딩 표시 + */ +export function showLoading() { + elements.loading.classList.remove('hidden'); +} + +/** + * 로딩 숨김 + */ +export function hideLoading() { + elements.loading.classList.add('hidden'); +} + +/** + * 메시지 표시 + * @param {string} message - 표시할 메시지 + * @param {string} type - 메시지 타입 ('info' | 'success' | 'error') + */ +export function showMessage(message, type = 'info') { + elements.messageArea.textContent = message; + elements.messageArea.className = `message-area ${type}`; + elements.messageArea.classList.remove('hidden'); + + setTimeout(() => { + elements.messageArea.classList.add('hidden'); + }, 3000); +} + +/** + * 뷰 전환 + * @param {string} viewName - 표시할 뷰 ('login' | 'pending' | 'main') + */ +export function showView(viewName) { + elements.loginView.classList.add('hidden'); + elements.pendingView.classList.add('hidden'); + elements.mainView.classList.add('hidden'); + + switch (viewName) { + case 'login': + elements.loginView.classList.remove('hidden'); + break; + case 'pending': + elements.pendingView.classList.remove('hidden'); + break; + case 'main': + elements.mainView.classList.remove('hidden'); + break; + } +} + +/** + * 강제 로그아웃 처리 + * @param {string} message - 표시할 메시지 + */ +export async function forceLogout(message) { + await clearStorage(); + elements.saveResult.classList.add('hidden'); + elements.devicesSection.classList.add('hidden'); + elements.emailInput.value = ''; + showView('login'); + showMessage(message, 'error'); +} + +/** + * 공통 API 에러 핸들러 + * @param {Error} error - 에러 객체 + * @returns {Promise} 로그아웃되었으면 true + */ +export async function handleApiError(error) { + const code = error.code || ERROR_CODES.BAD_REQUEST; + const message = getErrorMessage(code, error.message); + + if (isLogoutRequiredError(code)) { + await forceLogout(message); + return true; + } + + showMessage(message, 'error'); + return false; +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d68d51c --- /dev/null +++ b/src/utils.js @@ -0,0 +1,31 @@ +/** + * 유틸리티 함수 + * + * 날짜 포맷팅, 이메일 검증 등 범용 헬퍼 함수를 정의한다. + */ + +/** + * 날짜 포맷팅 + * @param {string} dateString - ISO 형식 날짜 문자열 + * @returns {string} 포맷된 날짜 문자열 + */ +export function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +/** + * 이메일 형식 검증 + * @param {string} email - 검증할 이메일 + * @returns {boolean} 유효하면 true + */ +export function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..739727c --- /dev/null +++ b/vite.config.js @@ -0,0 +1,31 @@ +/// +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + publicDir: 'public', + test: { + globals: true, + environment: 'node' + }, + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: { + popup: resolve(__dirname, 'src/popup.js'), + background: resolve(__dirname, 'src/background.js') + }, + output: { + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name].js', + assetFileNames: '[name].[ext]' + } + } + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + } +});