diff --git a/.github/workflows/ci-e2e-test.yml b/.github/workflows/ci-e2e-test.yml index b2930be..7e43e50 100644 --- a/.github/workflows/ci-e2e-test.yml +++ b/.github/workflows/ci-e2e-test.yml @@ -1,4 +1,4 @@ -name: CI E2E test +name: E2E Test on: pull_request: diff --git a/.github/workflows/ci-unit-test.yml b/.github/workflows/ci-unit-test.yml new file mode 100644 index 0000000..ecbe955 --- /dev/null +++ b/.github/workflows/ci-unit-test.yml @@ -0,0 +1,41 @@ +name: Unit Test + +on: + pull_request: + branches: [develop, main] + +permissions: + contents: read + +concurrency: + group: ci-unit-test-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + verify: + name: unit-verify + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + # 빠르게 실패하는 순서로 배치한다. 타입 오류는 lint / test 를 돌리기 전에 끊는다. + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Unit test + run: npm test diff --git a/global.d.ts b/global.d.ts index 33c5e05..f917d04 100644 --- a/global.d.ts +++ b/global.d.ts @@ -19,6 +19,7 @@ export declare global { targetIdOrEventName: string | Date, params?: Record, ) => void + __IS_NOT_FOUND_PAGE__?: boolean } interface Navigator { diff --git a/package-lock.json b/package-lock.json index 974f8be..810f7b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,8 @@ "prettier-plugin-tailwindcss": "^0.6.11", "supabase": "^2.31.8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" } }, "node_modules/@alloc/quick-lru": { @@ -3899,6 +3900,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4042,6 +4053,286 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", @@ -5032,6 +5323,13 @@ "webpack": ">=5.0.0" } }, + "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/@supabase/auth-js": { "version": "2.89.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", @@ -5413,6 +5711,17 @@ "tslib": "^2.4.0" } }, + "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/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5422,6 +5731,13 @@ "@types/node": "*" } }, + "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/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6090,6 +6406,129 @@ "win32" ] }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "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 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6621,6 +7060,16 @@ "safer-buffer": "^2.1.0" } }, + "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/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -7028,6 +7477,16 @@ ], "license": "CC-BY-4.0" }, + "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/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7579,8 +8038,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -7891,8 +8350,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8409,6 +8867,16 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9930,38 +10398,311 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "CC0-1.0" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -10223,9 +10964,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -10612,6 +11353,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10773,6 +11528,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -10879,7 +11641,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10901,9 +11662,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -10920,7 +11681,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11596,6 +12357,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -12021,6 +12816,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12081,6 +12883,13 @@ "dev": true, "license": "MIT" }, + "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/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", @@ -12093,6 +12902,13 @@ "node": ">=6" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -12699,15 +13515,32 @@ "node": ">=0.8" } }, + "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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "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==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -12735,9 +13568,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -12747,6 +13580,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13083,6 +13926,200 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.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.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "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/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -13366,6 +14403,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 5533c7b..f267a28 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "next lint --dir src --dir e2e", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "test:e2e": "playwright test", "generate-sitemap": "next-sitemap", "update-types": "supabase gen types typescript --project-id yyisakaqnaoomehqlyjz --schema public > src/types/supabase.ts" @@ -48,6 +51,7 @@ "prettier-plugin-tailwindcss": "^0.6.11", "supabase": "^2.31.8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" } } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 22c0603..c4c387c 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -8,9 +8,9 @@ const Page = () => { const router = useRouter() useEffect(() => { - ;(window as any).__IS_NOT_FOUND_PAGE__ = true + window.__IS_NOT_FOUND_PAGE__ = true return () => { - ;(window as any).__IS_NOT_FOUND_PAGE__ = false + window.__IS_NOT_FOUND_PAGE__ = false } }, []) diff --git a/src/components/community/Community.tsx b/src/components/community/Community.tsx index 26381fe..87881b3 100644 --- a/src/components/community/Community.tsx +++ b/src/components/community/Community.tsx @@ -1,4 +1,5 @@ import { Post } from '@/types/post' +import { PostRow } from '@/types/supabase' import PostCard from './PostCard' import { useState } from 'react' import CreatePostButton from './CreatePostButton' @@ -49,16 +50,16 @@ const Community = () => { const [hasFetched, setHasFetched] = useState(false) const params = useParams() const nbreadId = params.nbreadId as string - const mapToPost = (raw: any): Post => ({ + const mapToPost = (raw: PostRow): Post => ({ id: raw.id, - content: raw.content, - userName: raw.user_name, - userProfileImage: raw.profile_image, + content: raw.content ?? '', + userName: raw.user_name ?? '', + userProfileImage: raw.profile_image ?? '', createdAt: new Date(raw.created_at) .toISOString() .slice(0, 10) .replace(/-/g, '.'), - userId: raw.user_id, + userId: raw.user_id ?? '', nbreadId: raw.nbread_id, }) const fetchPosts = async () => { diff --git a/src/components/home/NbreadCard.tsx b/src/components/home/NbreadCard.tsx index b167745..51d171f 100644 --- a/src/components/home/NbreadCard.tsx +++ b/src/components/home/NbreadCard.tsx @@ -1,4 +1,5 @@ import Avatar from '@/components/common/avatar/avatar' +import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' import { Nbread } from '@/types/nbread' import { useRouter } from 'next/navigation' @@ -23,7 +24,10 @@ const NbreadCard = ({ nbread, showParticipants = true }: NbreadCardProps) => {

{nbread.title}

- {Math.floor(nbread.amount / nbread.participantCount).toLocaleString()} + {calculateIndividualShare( + nbread.amount, + nbread.participantCount, + ).toLocaleString()} 원 /{nbread.paymentPeriod === 'year' ? ' 매년' : ' 매월'}

diff --git a/src/components/nbread/NbreadDetail.tsx b/src/components/nbread/NbreadDetail.tsx index 4b46693..3a9d8b5 100644 --- a/src/components/nbread/NbreadDetail.tsx +++ b/src/components/nbread/NbreadDetail.tsx @@ -26,12 +26,12 @@ import QuitNbreadModal from '@/components/common/modal/QuitNbreadModal' import Spinner from '@/components/common/spinner/Spinner' import InviteBottomSheet from '@/components/invite/InviteBottomSheet' import { getFriendList } from '@/lib/friend/getSearchFriend' -interface nbreadDetailProps { +interface NbreadDetailProps { nbreadData: Nbread setNbreadData: Dispatch> } -const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { +const NbreadDetail = ({ nbreadData, setNbreadData }: NbreadDetailProps) => { const userData = useUserStore((state) => state.user) const [nbreadRecords, setNbreadRecords] = useState( null, @@ -114,7 +114,8 @@ const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { // 엔빵 탈퇴 처리 함수 const onSubmitQuitNbread = async () => { try { - await deleteParticipants(userData?.id!, nbreadData!.id) + if (!userData?.id) throw new Error('사용자 정보를 찾을 수 없어요.') + await deleteParticipants(userData.id, nbreadData!.id) setIsQuitNbreadModalOpen(false) useToast.success('엔빵 나가기에 성공했어요.') router.replace('/home') @@ -234,4 +235,4 @@ const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { ) } -export default nbreadDetail +export default NbreadDetail diff --git a/src/components/nbread/nbreadCard.tsx b/src/components/nbread/nbreadCard.tsx index 37b6e1c..65173d1 100644 --- a/src/components/nbread/nbreadCard.tsx +++ b/src/components/nbread/nbreadCard.tsx @@ -1,3 +1,4 @@ +import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' import { Nbread } from '@/types/nbread' import { User } from '@/types/user' import Tab from '../common/tab/tab' @@ -13,8 +14,10 @@ const NbreadCard = ({ userData, handleEditingNbread, }: NbreadCardProps) => { - const paymentAmount = - Math.floor(nbreadData!.amount / nbreadData!.participantCount) || 0 + const paymentAmount = calculateIndividualShare( + nbreadData!.amount, + nbreadData!.participantCount, + ) return ( <> diff --git a/src/lib/friend/getSearchFriend.ts b/src/lib/friend/getSearchFriend.ts index 77020cb..6644990 100644 --- a/src/lib/friend/getSearchFriend.ts +++ b/src/lib/friend/getSearchFriend.ts @@ -1,4 +1,10 @@ import { supabase } from '../supabaseClient' +import { NbreadInviteRow } from '@/types/supabase' + +type NbreadInviteSummary = Pick< + NbreadInviteRow, + 'status' | 'target_user_id' | 'created_at' +> export interface FriendListItem { name: string @@ -95,7 +101,7 @@ export const getFriendList = async ( })) const friendIds = processedFriends.map((f) => f.id) - let inviteData: any[] = [] + let inviteData: NbreadInviteSummary[] = [] if (nbreadId) { const { data, error } = await supabase .from('nbread_invite') diff --git a/src/lib/nbread/calculateIndividualShare.test.ts b/src/lib/nbread/calculateIndividualShare.test.ts new file mode 100644 index 0000000..247c77e --- /dev/null +++ b/src/lib/nbread/calculateIndividualShare.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, test } from 'vitest' +import { calculateIndividualShare } from './calculateIndividualShare' + +describe('calculateIndividualShare', () => { + describe('정상 계산', () => { + it('총 금액을 참여 인원으로 나눈다', () => { + expect(calculateIndividualShare(30000, 3)).toBe(10000) + }) + + it('나누어떨어지지 않으면 원 단위 미만을 버린다', () => { + expect(calculateIndividualShare(10000, 3)).toBe(3333) + }) + + it('참여 인원이 1명이면 총 금액을 그대로 반환한다', () => { + expect(calculateIndividualShare(17900, 1)).toBe(17900) + }) + }) + + describe('경계값', () => { + it('총 금액이 0이면 0을 반환한다', () => { + expect(calculateIndividualShare(0, 4)).toBe(0) + }) + + it('1인당 금액이 1원 미만이면 0으로 내려간다', () => { + expect(calculateIndividualShare(3, 4)).toBe(0) + }) + + it('참여 인원에 소수가 들어오면 정수로 버린 뒤 나눈다', () => { + expect(calculateIndividualShare(10000, 2.7)).toBe(5000) + }) + + // 금액 컬럼에 음수를 막는 제약이 없어 표시 로직까지 그대로 내려온다. + it('총 금액이 음수면 음수 몫을 그대로 반환한다', () => { + expect(calculateIndividualShare(-3000, 2)).toBe(-1500) + }) + }) + + describe('잘못된 참여 인원', () => { + // 인원이 0이면 0 나눗셈으로 Infinity 가 되어 화면에 그대로 노출된 적이 있다. + // 1명으로 취급해 최소한 숫자가 나오도록 막는다. + // 1명 미만 소수는 정수로 버린 뒤 0이 되므로 같은 경로를 탄다. + test.for([ + { label: '0명', participantCount: 0 }, + { label: '음수', participantCount: -3 }, + { label: '1명 미만 소수', participantCount: 0.5 }, + { label: '숫자가 아닌 값', participantCount: Number.NaN }, + ])('참여 인원이 $label 이면 1명으로 취급한다', ({ participantCount }) => { + expect(calculateIndividualShare(30000, participantCount)).toBe(30000) + }) + }) + + describe('잘못된 총 금액', () => { + it('총 금액이 숫자가 아니면 0을 반환한다', () => { + expect(calculateIndividualShare(Number.NaN, 3)).toBe(0) + }) + + it('총 금액이 무한이면 0을 반환한다', () => { + expect(calculateIndividualShare(Number.POSITIVE_INFINITY, 3)).toBe(0) + }) + }) +}) diff --git a/src/lib/nbread/calculateIndividualShare.ts b/src/lib/nbread/calculateIndividualShare.ts new file mode 100644 index 0000000..84168a5 --- /dev/null +++ b/src/lib/nbread/calculateIndividualShare.ts @@ -0,0 +1,15 @@ +/** + * 총 금액을 참여 인원으로 나눈 1인당 정산 금액을 계산한다. + * + * 원 단위 미만은 버린다. 참여 인원이 0 이하이거나 숫자가 아니면 1명으로 취급해 + * 0 나눗셈으로 Infinity 가 화면에 노출되는 것을 막는다. + */ +export const calculateIndividualShare = ( + amount: number, + participantCount: number, +): number => { + const safeParticipantCount = Math.max(Math.trunc(participantCount) || 1, 1) + const share = Math.floor(amount / safeParticipantCount) + + return Number.isFinite(share) ? share : 0 +} diff --git a/src/lib/nbread/getUserTotalNbreadAmount.ts b/src/lib/nbread/getUserTotalNbreadAmount.ts index 313060b..3b342eb 100644 --- a/src/lib/nbread/getUserTotalNbreadAmount.ts +++ b/src/lib/nbread/getUserTotalNbreadAmount.ts @@ -1,4 +1,5 @@ import { supabase } from "@/lib/supabaseClient"; +import { calculateIndividualShare } from "@/lib/nbread/calculateIndividualShare"; export const getUserTotalNbreadAmount = async (userId: string) => { if (!userId) return 0; @@ -31,7 +32,7 @@ export const getUserTotalNbreadAmount = async (userId: string) => { } const totalAmount = nbreads.reduce((sum, nbread) => { - const individualShare = Math.floor(nbread.amount / Math.max(nbread.participant_count, 1)); + const individualShare = calculateIndividualShare(nbread.amount, nbread.participant_count); return sum + individualShare; }, 0); diff --git a/src/lib/nbread/index.ts b/src/lib/nbread/index.ts index f7b14d8..31eda67 100644 --- a/src/lib/nbread/index.ts +++ b/src/lib/nbread/index.ts @@ -3,3 +3,4 @@ export { insertNbread } from './insertNbread' export { updateNbread } from './updateNbread' export { deleteNbread } from './deleteNbread' export { getUserNbreads } from './getUserNbread' +export { calculateIndividualShare } from './calculateIndividualShare' diff --git a/src/lib/nbreadRecord/getNbreadRecords.ts b/src/lib/nbreadRecord/getNbreadRecords.ts index 59346f2..6fbbcc0 100644 --- a/src/lib/nbreadRecord/getNbreadRecords.ts +++ b/src/lib/nbreadRecord/getNbreadRecords.ts @@ -1,13 +1,12 @@ import { supabase } from '@/lib/supabaseClient' import { Nbread, NbreadRecord } from '@/types/nbread' +import { toPaymentDateKey } from './toPaymentDateKey' export const getNbreadRecords = async ( nbreadId: string, startDate: string, ) => { - const translatedStartDate = new Date(startDate) - .toISOString() - .split('T')[0] + const translatedStartDate = toPaymentDateKey(startDate) try { const { data, error } = await supabase diff --git a/src/lib/nbreadRecord/index.ts b/src/lib/nbreadRecord/index.ts index caae0ad..0b79da9 100644 --- a/src/lib/nbreadRecord/index.ts +++ b/src/lib/nbreadRecord/index.ts @@ -1,2 +1,3 @@ export { getNbreadRecords } from './getNbreadRecords' export { updateNbreadRecord } from './updateNbreadRecord' +export { toPaymentDateKey } from './toPaymentDateKey' diff --git a/src/lib/nbreadRecord/toPaymentDateKey.test.ts b/src/lib/nbreadRecord/toPaymentDateKey.test.ts new file mode 100644 index 0000000..d3e9b5e --- /dev/null +++ b/src/lib/nbreadRecord/toPaymentDateKey.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { toPaymentDateKey } from './toPaymentDateKey' + +// 이 함수의 결과는 nbread_records.payment_date 와 문자열이 정확히 일치해야 한다. +// 하루라도 어긋나면 조회 0건, 갱신 0행이 되고 예외는 나지 않는다. +// 아래 테스트는 현재 변환 규칙을 고정하는 회귀 테스트다. +describe('toPaymentDateKey', () => { + describe('정상 변환', () => { + it('날짜만 있는 문자열은 그대로 유지한다', () => { + expect(toPaymentDateKey('2026-09-01')).toBe('2026-09-01') + }) + + it('UTC 시각이 붙어 있으면 시각을 떼고 날짜만 남긴다', () => { + expect(toPaymentDateKey('2026-09-01T23:00:00Z')).toBe('2026-09-01') + }) + }) + + describe('타임존 경계', () => { + // UTC 로 환산한 뒤 날짜를 자르기 때문에 KST 자정 입력은 전날로 밀린다. + // 현재 동작이며, 결제일이 하루 밀리는 경로이므로 명시적으로 고정한다. + it('KST 자정은 UTC 기준 전날로 변환된다', () => { + expect(toPaymentDateKey('2026-09-01T00:00:00+09:00')).toBe('2026-08-31') + }) + + it('KST 오전 9시 이후는 같은 날로 변환된다', () => { + expect(toPaymentDateKey('2026-09-01T09:00:00+09:00')).toBe('2026-09-01') + }) + + it('UTC 자정은 같은 날로 유지된다', () => { + expect(toPaymentDateKey('2026-09-01T00:00:00Z')).toBe('2026-09-01') + }) + }) + + describe('잘못된 입력', () => { + // 조용히 잘못된 키를 만드는 것보다 예외로 끊는 편이 안전하다. + it('빈 문자열이면 예외를 던진다', () => { + expect(() => toPaymentDateKey('')).toThrow(RangeError) + }) + + it('날짜로 해석할 수 없는 문자열이면 예외를 던진다', () => { + expect(() => toPaymentDateKey('납부일 미정')).toThrow(RangeError) + }) + }) +}) diff --git a/src/lib/nbreadRecord/toPaymentDateKey.ts b/src/lib/nbreadRecord/toPaymentDateKey.ts new file mode 100644 index 0000000..5d31728 --- /dev/null +++ b/src/lib/nbreadRecord/toPaymentDateKey.ts @@ -0,0 +1,8 @@ +/** + * nbread_records.payment_date 조회 및 갱신에 쓰는 날짜 키(YYYY-MM-DD)를 만든다. + * + * 이 값이 한 칸이라도 어긋나면 조회는 0건, 갱신은 0행이 되고 예외는 발생하지 않는다. + * 즉 납부 체크가 조용히 실패한다. 따라서 변환 규칙을 한 곳에 고정한다. + */ +export const toPaymentDateKey = (startDate: string): string => + new Date(startDate).toISOString().split('T')[0] diff --git a/src/lib/nbreadRecord/updateNbreadRecord.ts b/src/lib/nbreadRecord/updateNbreadRecord.ts index 09bb39a..dd229a2 100644 --- a/src/lib/nbreadRecord/updateNbreadRecord.ts +++ b/src/lib/nbreadRecord/updateNbreadRecord.ts @@ -1,6 +1,7 @@ import { supabase } from '@/lib/supabaseClient' import { Nbread } from '@/types/nbread' import { captureAppError } from '@/lib/sentry/sentry' +import { toPaymentDateKey } from './toPaymentDateKey' export const updateNbreadRecord = async ( nbreadId: string, @@ -8,7 +9,7 @@ export const updateNbreadRecord = async ( isPaid: boolean, startDate: string, ) => { - const translatedStartDate = new Date(startDate).toISOString().split('T')[0] + const translatedStartDate = toPaymentDateKey(startDate) try { const { data, error } = await supabase diff --git a/src/lib/notification/getNotificationDestination.test.ts b/src/lib/notification/getNotificationDestination.test.ts new file mode 100644 index 0000000..d395715 --- /dev/null +++ b/src/lib/notification/getNotificationDestination.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it, test } from 'vitest' +import { + getNotificationDestination, + getNotificationDestinationError, +} from './getNotificationDestination' +import type { Notification, NotificationType } from '@/types/notification' + +const notification = ( + type: NotificationType, + data: Notification['data'] = null, +): Pick => ({ type, data }) + +// 엔빵 아이디가 있어야 이동 경로가 만들어지는 알림 타입 +const nbreadIdRequiredTypes: { type: NotificationType }[] = [ + { type: 'chat' }, + { type: 'payment' }, + { type: 'invite_accept' }, +] + +// data 에서 값을 읽어야 이동 경로가 만들어지는 알림 타입 +const dataRequiredTypes: { type: NotificationType }[] = [ + ...nbreadIdRequiredTypes, + { type: 'invite' }, +] + +describe('getNotificationDestination', () => { + describe('초대 알림', () => { + it('camelCase 키에서 초대 토큰을 읽는다', () => { + expect( + getNotificationDestination( + notification('invite', { inviteToken: 'abc123' }), + ), + ).toBe('/invite/abc123') + }) + + it('snake_case 키에서도 초대 토큰을 읽는다', () => { + expect( + getNotificationDestination( + notification('invite', { invite_token: 'abc123' }), + ), + ).toBe('/invite/abc123') + }) + + it('토큰에 URL 예약 문자가 있으면 인코딩한다', () => { + expect( + getNotificationDestination( + notification('invite', { inviteToken: 'a/b c' }), + ), + ).toBe('/invite/a%2Fb%20c') + }) + + it('토큰이 없으면 null 을 반환한다', () => { + expect( + getNotificationDestination(notification('invite', { nbreadId: 'n1' })), + ).toBeNull() + }) + }) + + describe('엔빵 알림', () => { + it('채팅 알림은 chat 탭으로 보낸다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: 'n1' })), + ).toBe('/nbread/n1?tab=chat') + }) + + it('납부 알림은 엔빵 상세로 보낸다', () => { + expect( + getNotificationDestination(notification('payment', { nbread_id: 'n1' })), + ).toBe('/nbread/n1') + }) + + it('초대 수락 알림은 엔빵 상세로 보낸다', () => { + expect( + getNotificationDestination( + notification('invite_accept', { nbreadId: 'n1' }), + ), + ).toBe('/nbread/n1') + }) + + test.for(nbreadIdRequiredTypes)( + '$type 알림은 엔빵 아이디가 없으면 null 을 반환한다', + ({ type }) => { + expect(getNotificationDestination(notification(type, {}))).toBeNull() + }, + ) + }) + + describe('친구 알림', () => { + it('친구 응답 알림은 data 없이도 친구 목록으로 보낸다', () => { + expect(getNotificationDestination(notification('friend_response'))).toBe( + '/friendList', + ) + }) + + it('친구 요청 알림은 이동 경로가 없다', () => { + expect( + getNotificationDestination(notification('friend_request')), + ).toBeNull() + }) + }) + + describe('잘못된 data', () => { + test.for(dataRequiredTypes)( + '$type 알림은 data 가 null 이면 null 을 반환한다', + ({ type }) => { + expect(getNotificationDestination(notification(type, null))).toBeNull() + }, + ) + + it('data 가 배열이면 null 을 반환한다', () => { + expect( + getNotificationDestination(notification('chat', ['n1'])), + ).toBeNull() + }) + + it('값이 빈 문자열이면 없는 것으로 취급한다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: '' })), + ).toBeNull() + }) + + it('값이 문자열이 아니면 없는 것으로 취급한다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: 123 })), + ).toBeNull() + }) + + it('camelCase 키가 비어 있으면 snake_case 키로 넘어간다', () => { + expect( + getNotificationDestination( + notification('chat', { nbreadId: '', nbread_id: 'n1' }), + ), + ).toBe('/nbread/n1?tab=chat') + }) + }) +}) + +describe('getNotificationDestinationError', () => { + it('초대 알림은 초대 정보 안내 문구를 반환한다', () => { + expect(getNotificationDestinationError('invite')).toBe( + '초대 정보를 찾을 수 없어요.', + ) + }) + + it('친구 요청 알림은 이동할 곳이 없으므로 문구가 없다', () => { + expect(getNotificationDestinationError('friend_request')).toBe('') + }) + + it('나머지 알림은 공통 안내 문구를 반환한다', () => { + expect(getNotificationDestinationError('chat')).toBe( + '이동할 페이지 정보를 찾을 수 없어요.', + ) + }) +}) diff --git a/src/lib/post/deletePost.ts b/src/lib/post/deletePost.ts index d597070..b7bf444 100644 --- a/src/lib/post/deletePost.ts +++ b/src/lib/post/deletePost.ts @@ -1,5 +1,5 @@ import { supabase } from '../supabaseClient' -export const deletePost = async (post: any) => { +export const deletePost = async (post: number) => { try { const { data, error } = await supabase .from('post') diff --git a/src/lib/post/updatePost.ts b/src/lib/post/updatePost.ts index 67c5fde..4ec92d0 100644 --- a/src/lib/post/updatePost.ts +++ b/src/lib/post/updatePost.ts @@ -1,6 +1,7 @@ import { supabase } from "../supabaseClient" +import { Post } from '@/types/post' -export const UpdatePost = async (post : any) => { +export const UpdatePost = async (post : Pick) => { try { const { data, error } = await supabase .from('post') diff --git a/src/types/supabase.ts b/src/types/supabase.ts index 2c09f90..a5a26c4 100644 --- a/src/types/supabase.ts +++ b/src/types/supabase.ts @@ -530,3 +530,6 @@ export type NotificationRow = Database['public']['Tables']['notification']['Row'] export type FriendRequestRow = Database['public']['Tables']['friend_request']['Row'] +export type PostRow = Database['public']['Tables']['post']['Row'] +export type NbreadInviteRow = + Database['public']['Tables']['nbread_invite']['Row'] diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts new file mode 100644 index 0000000..6f14ee6 --- /dev/null +++ b/src/types/svg.d.ts @@ -0,0 +1,12 @@ +// next-env.d.ts 는 .gitignore 대상이라 새 체크아웃에는 존재하지 않는다. +// 그 파일이 참조하는 next/image-types/global 이 *.svg 선언을 제공하므로, +// tsc --noEmit 만 실행하는 CI 에서는 SVG 임포트가 전부 미해결이 된다. +// 이 프로젝트는 @svgr/webpack 으로 SVG 를 React 컴포넌트로 가져오므로 +// 그 형태를 저장소에 직접 선언해 타입 체크가 next-env.d.ts 에 의존하지 않게 한다. +declare module '*.svg' { + import type { FunctionComponent, SVGProps } from 'react' + + const ReactComponent: FunctionComponent> + + export default ReactComponent +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..2660848 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + // tsconfig 의 "@/*" 경로 별칭을 Vite 기본 기능으로 해석한다. + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + // include 만으로도 e2e 의 *.spec.ts 는 걸리지 않는다. + // 테스트 위치가 늘어나도 Playwright 스펙을 집어가지 않도록 방어로 남긴다. + exclude: ['e2e/**', 'node_modules/**'], + }, +})