From 3bfe242c1d049ad75b75d79e56fc506afed07bd5 Mon Sep 17 00:00:00 2001 From: jhan0121 <56645802+jhan0121@users.noreply.github.com> Date: Sat, 27 Dec 2025 23:43:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=B3=B5=EC=8A=B5=20URL=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=81=AC=EB=A1=AC=20=EC=9D=B5=EC=8A=A4=ED=85=90?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 크롬 익스텐션 기능 추가 * refactor: vite 기반 프로젝트 구조 리팩터링 * chore: EOF 개행 추가 * refactor: XSS 취약점 방지를 위해 innerHTML 제거 * refactor: .env 기반 환경 관리 적용 * fix: 설정 변수명 오기입 수정 * refactor: json 파싱 에러 처리 명시 * refactor: 불필요한 리턴 제거 * refactor: 불필요한 import 제거 * chore: 파일별 주석 내용 정리 --- .gitignore | 6 + package-lock.json | 942 +++++++++++++++++++++++++++++++++++++++ package.json | 14 + public/icons/icon128.png | Bin 0 -> 19463 bytes public/icons/icon16.png | Bin 0 -> 727 bytes public/icons/icon48.png | Bin 0 -> 3965 bytes public/manifest.json | 31 ++ public/popup.css | 333 ++++++++++++++ public/popup.html | 76 ++++ src/api.js | 102 +++++ src/background.js | 25 ++ src/config.js | 16 + src/constants.js | 25 ++ src/errors.js | 63 +++ src/handlers.js | 232 ++++++++++ src/popup.js | 90 ++++ src/storage.js | 83 ++++ src/ui.js | 153 +++++++ src/utils.js | 31 ++ vite.config.js | 26 ++ 20 files changed, 2248 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/icons/icon128.png create mode 100644 public/icons/icon16.png create mode 100644 public/icons/icon48.png create mode 100644 public/manifest.json create mode 100644 public/popup.css create mode 100644 public/popup.html create mode 100644 src/api.js create mode 100644 src/background.js create mode 100644 src/config.js create mode 100644 src/constants.js create mode 100644 src/errors.js create mode 100644 src/handlers.js create mode 100644 src/popup.js create mode 100644 src/storage.js create mode 100644 src/ui.js create mode 100644 src/utils.js create mode 100644 vite.config.js 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..e3d9d0a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,942 @@ +{ + "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" + } + }, + "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-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-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/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/@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/@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/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/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/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/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/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/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/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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f73fa7 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "recycle-study-extension", + "version": "1.0.0", + "description": "복습 URL 저장 크롬 익스텐션", + "type": "module", + "scripts": { + "dev": "vite build --watch", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.4.0" + } +} diff --git a/public/icons/icon128.png b/public/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..421194b623695eae0abb853fde0f4e3bf860e8c5 GIT binary patch literal 19463 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 JPDHLkV1oNFO40xT literal 0 HcmV?d00001 diff --git a/public/icons/icon48.png b/public/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..db5747ec3ecc50b7f1a417205f209909c51baa0a GIT binary patch literal 3965 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 X?w9bD0e%BU00000NkvXXu0mjfsoAl} literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..d7882d9 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,31 @@ +{ + "manifest_version": 3, + "name": "Recycle Study", + "version": "1.0.0", + "description": "복습 URL을 저장하고 스케줄에 따라 알림을 받는 익스텐션", + "permissions": [ + "storage", + "activeTab", + "tabs" + ], + "host_permissions": [ + "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/api.js b/src/api.js new file mode 100644 index 0000000..6b65d26 --- /dev/null +++ b/src/api.js @@ -0,0 +1,102 @@ +/** + * API 호출 관련 함수 + * + * 서버와의 통신을 담당하며, 디바이스 등록/조회/삭제, 복습 URL 저장 등의 API를 제공한다. + */ + +import { CONFIG } from './config.js'; +import { ERROR_CODES } from './constants.js'; +import { ApiError, getErrorCodeFromStatus } from './errors.js'; + +/** + * API 요청 래퍼 (공통 에러 처리) + * @param {string} url - 요청 URL + * @param {Object} options - fetch 옵션 + * @returns {Promise} 응답 데이터 + * @throws {ApiError} + */ +async function apiRequest(url, options = {}) { + let response; + + try { + response = await fetch(url, options); + } catch (error) { + throw new ApiError(ERROR_CODES.NETWORK_ERROR, error.message); + } + + // 204 No Content인 경우 (DELETE 성공 등) + if (response.status === 204) { + return 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) { + const errorCode = getErrorCodeFromStatus(response.status); + throw new ApiError(errorCode, data.message); + } + + return data; +} + +/** + * 디바이스 등록 (회원가입) + * @param {string} email - 사용자 이메일 + * @returns {Promise} { email, identifier } + */ +export async function registerDevice(email) { + return await apiRequest(`${CONFIG.BASE_URL}/api/v1/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); +} + +/** + * 디바이스 목록 조회 + * @param {string} email - 사용자 이메일 + * @param {string} identifier - 디바이스 식별자 + * @returns {Promise} { email, devices } + */ +export async function getDevices(email, identifier) { + const params = new URLSearchParams({ email, identifier }); + return await apiRequest(`${CONFIG.BASE_URL}/api/v1/members?${params}`); +} + +/** + * 디바이스 삭제 + * @param {string} email - 사용자 이메일 + * @param {string} deviceIdentifier - 현재 디바이스 식별자 + * @param {string} targetDeviceIdentifier - 삭제할 디바이스 식별자 + */ +export async function deleteDevice(email, deviceIdentifier, targetDeviceIdentifier) { + return await apiRequest(`${CONFIG.BASE_URL}/api/v1/device`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + deviceIdentifier, + targetDeviceIdentifier + }) + }); +} + +/** + * 복습 URL 저장 + * @param {string} identifier - 디바이스 식별자 + * @param {string} targetUrl - 저장할 URL + * @returns {Promise} { url, scheduledAts } + */ +export async function saveReviewUrl(identifier, targetUrl) { + return await apiRequest(`${CONFIG.BASE_URL}/api/v1/reviews`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier, targetUrl }) + }); +} diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..c0ee00d --- /dev/null +++ b/src/background.js @@ -0,0 +1,25 @@ +/** + * 백그라운드 서비스 워커 + * + * 익스텐션 설치/업데이트 이벤트 처리 및 popup과의 메시지 통신을 담당한다. + */ + +// ============================================ +// 익스텐션 설치/업데이트 이벤트 +// ============================================ +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + console.log('Recycle Study 익스텐션이 설치되었습니다.'); + } else if (details.reason === 'update') { + console.log('Recycle Study 익스텐션이 업데이트되었습니다.'); + } +}); + +// ============================================ +// 메시지 리스너 (popup.js와 통신용) +// ============================================ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'CHECK_AUTH') { + sendResponse({ success: true }); + } +}); diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..b689717 --- /dev/null +++ b/src/config.js @@ -0,0 +1,16 @@ +/** + * 환경 설정 + * + * 개발: vite build --mode dev (.env.dev 사용) + * 프로덕션: vite build --mode prod (.env.prod 사용) + */ +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..7752f84 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + publicDir: 'public', + 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') + } + } +}); From 1b2496ef5dc150c040caee096090353bdc1495e4 Mon Sep 17 00:00:00 2001 From: jhan0121 <56645802+jhan0121@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:17:13 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20CORS=20=EC=9A=B0=ED=9A=8C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.js | 71 +++++++++++++++++++---------------------------- src/background.js | 59 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/api.js b/src/api.js index 6b65d26..40bc5ee 100644 --- a/src/api.js +++ b/src/api.js @@ -2,47 +2,33 @@ * API 호출 관련 함수 * * 서버와의 통신을 담당하며, 디바이스 등록/조회/삭제, 복습 URL 저장 등의 API를 제공한다. + * 모든 API 요청은 background.js를 통해 처리되어 CORS 문제를 우회한다. */ -import { CONFIG } from './config.js'; import { ERROR_CODES } from './constants.js'; import { ApiError, getErrorCodeFromStatus } from './errors.js'; /** - * API 요청 래퍼 (공통 에러 처리) - * @param {string} url - 요청 URL - * @param {Object} options - fetch 옵션 + * Background Script로 API 요청 전송 + * @param {Object} request - API 요청 정보 * @returns {Promise} 응답 데이터 * @throws {ApiError} */ -async function apiRequest(url, options = {}) { - let response; - - try { - response = await fetch(url, options); - } catch (error) { - throw new ApiError(ERROR_CODES.NETWORK_ERROR, error.message); - } - - // 204 No Content인 경우 (DELETE 성공 등) - if (response.status === 204) { - return 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.' }; - } +async function sendApiRequest(request) { + const response = await chrome.runtime.sendMessage({ + type: 'API_REQUEST', + request + }); - if (!response.ok) { + if (!response.success) { + if (response.isNetworkError) { + throw new ApiError(ERROR_CODES.NETWORK_ERROR, response.message); + } const errorCode = getErrorCodeFromStatus(response.status); - throw new ApiError(errorCode, data.message); + throw new ApiError(errorCode, response.message); } - return data; + return response.data; } /** @@ -51,10 +37,10 @@ async function apiRequest(url, options = {}) { * @returns {Promise} { email, identifier } */ export async function registerDevice(email) { - return await apiRequest(`${CONFIG.BASE_URL}/api/v1/members`, { + return await sendApiRequest({ + endpoint: '/api/v1/members', method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }) + body: { email } }); } @@ -65,8 +51,11 @@ export async function registerDevice(email) { * @returns {Promise} { email, devices } */ export async function getDevices(email, identifier) { - const params = new URLSearchParams({ email, identifier }); - return await apiRequest(`${CONFIG.BASE_URL}/api/v1/members?${params}`); + return await sendApiRequest({ + endpoint: '/api/v1/members', + method: 'GET', + params: { email, identifier } + }); } /** @@ -76,14 +65,10 @@ export async function getDevices(email, identifier) { * @param {string} targetDeviceIdentifier - 삭제할 디바이스 식별자 */ export async function deleteDevice(email, deviceIdentifier, targetDeviceIdentifier) { - return await apiRequest(`${CONFIG.BASE_URL}/api/v1/device`, { + return await sendApiRequest({ + endpoint: '/api/v1/device', method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email, - deviceIdentifier, - targetDeviceIdentifier - }) + body: { email, deviceIdentifier, targetDeviceIdentifier } }); } @@ -94,9 +79,9 @@ export async function deleteDevice(email, deviceIdentifier, targetDeviceIdentifi * @returns {Promise} { url, scheduledAts } */ export async function saveReviewUrl(identifier, targetUrl) { - return await apiRequest(`${CONFIG.BASE_URL}/api/v1/reviews`, { + return await sendApiRequest({ + endpoint: '/api/v1/reviews', method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ identifier, targetUrl }) + body: { identifier, targetUrl } }); } diff --git a/src/background.js b/src/background.js index c0ee00d..bcf430a 100644 --- a/src/background.js +++ b/src/background.js @@ -2,8 +2,11 @@ * 백그라운드 서비스 워커 * * 익스텐션 설치/업데이트 이벤트 처리 및 popup과의 메시지 통신을 담당한다. + * API 요청은 CORS 우회를 위해 이 서비스 워커에서 처리한다. */ +import { CONFIG } from './config.js'; + // ============================================ // 익스텐션 설치/업데이트 이벤트 // ============================================ @@ -16,9 +19,63 @@ chrome.runtime.onInstalled.addListener((details) => { }); // ============================================ -// 메시지 리스너 (popup.js와 통신용) +// 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 }); } From 4b3f6f3afc678e6ebe9f4933087c9c8d1e35543e Mon Sep 17 00:00:00 2001 From: flinter <56645802+jhan0121@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:25:43 +0900 Subject: [PATCH 3/4] =?UTF-8?q?CICD=20=EC=9E=90=EB=8F=99=ED=99=94=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#2?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 운영/개발 서버 환경 관리 추가 * feat: 배포 스크립트 추가 * test: 테스트 의존성 추가 및 작성 * feat: 테스트 검증 자동화 스크립트 추가 * feat: 배포 스크립트 추가 --- .github/workflows/release.yml | 76 ++ .github/workflows/test-validation.yml | 34 + package-lock.json | 971 +++++++++++++++++++++++++- package.json | 7 +- public/manifest.json | 1 + src/__tests__/errors.test.js | 54 ++ src/__tests__/utils.test.js | 29 + src/config.js | 4 +- vite.config.js | 5 + 9 files changed, 1176 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test-validation.yml create mode 100644 src/__tests__/errors.test.js create mode 100644 src/__tests__/utils.test.js 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..e2f21d4 --- /dev/null +++ b/.github/workflows/test-validation.yml @@ -0,0 +1,34 @@ +name: Test & Build Validation + +on: + pull_request: + branches: + - dev + - prod + push: + 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/package-lock.json b/package-lock.json index e3d9d0a..79ac838 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "recycle-study-extension", "version": "1.0.0", "devDependencies": { - "vite": "^5.4.0" + "vite": "^5.4.0", + "vitest": "^4.0.16" } }, "node_modules/@esbuild/aix-ppc64": { @@ -300,6 +301,23 @@ "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", @@ -317,6 +335,23 @@ "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", @@ -334,6 +369,23 @@ "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", @@ -402,6 +454,13 @@ "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", @@ -710,6 +769,31 @@ "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", @@ -717,6 +801,117 @@ "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", @@ -756,6 +951,44 @@ "@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", @@ -771,6 +1004,16 @@ "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", @@ -790,6 +1033,24 @@ "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", @@ -797,6 +1058,19 @@ "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", @@ -868,6 +1142,13 @@ "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", @@ -878,6 +1159,64 @@ "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", @@ -937,6 +1276,636 @@ "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 index 2f73fa7..6f8315d 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,12 @@ "scripts": { "dev": "vite build --watch", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" }, "devDependencies": { - "vite": "^5.4.0" + "vite": "^5.4.0", + "vitest": "^4.0.16" } } diff --git a/public/manifest.json b/public/manifest.json index d7882d9..87be2e1 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -9,6 +9,7 @@ "tabs" ], "host_permissions": [ + "https://api.recycle-study.site/*", "http://localhost:8080/*" ], "action": { 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/config.js b/src/config.js index b689717..943b4c2 100644 --- a/src/config.js +++ b/src/config.js @@ -1,8 +1,8 @@ /** * 환경 설정 * - * 개발: vite build --mode dev (.env.dev 사용) - * 프로덕션: vite build --mode prod (.env.prod 사용) + * 개발: vite build (.env.development 사용, 기본값) + * 프로덕션: vite build --mode production (.env.production 사용) */ export const CONFIG = { BASE_URL: import.meta.env.VITE_BASE_URL || 'http://localhost:8080', diff --git a/vite.config.js b/vite.config.js index 7752f84..739727c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,8 +1,13 @@ +/// import { defineConfig } from 'vite'; import { resolve } from 'path'; export default defineConfig({ publicDir: 'public', + test: { + globals: true, + environment: 'node' + }, build: { outDir: 'dist', emptyOutDir: true, From 8be0f06b6a8e5fbf216c402b207b079512031c54 Mon Sep 17 00:00:00 2001 From: flinter <56645802+jhan0121@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:35:40 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-validation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-validation.yml b/.github/workflows/test-validation.yml index e2f21d4..7f1f5da 100644 --- a/.github/workflows/test-validation.yml +++ b/.github/workflows/test-validation.yml @@ -2,10 +2,8 @@ name: Test & Build Validation on: pull_request: - branches: - - dev - - prod - push: + types: + [ opened, synchronize, reopened ] branches: - dev - prod