diff --git a/.coderabbit.yaml b/.coderabbit.yaml
index 04a79c3..f6fc1dc 100644
--- a/.coderabbit.yaml
+++ b/.coderabbit.yaml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
# CodeRabbit Configuration
# https://docs.coderabbit.ai/guides/configure-coderabbit
@@ -27,6 +28,7 @@ reviews:
base_branches:
- "main"
- "develop"
+ - "dev"
poem: false
collapse_walkthrough: true
diff --git a/.gitignore b/.gitignore
index 4b8ec10..79395b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ dist
dist-ssr
*.local
+# 테스트·커버리지 산출물 (CI/로컬에서 생성)
+coverage
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/package-lock.json b/package-lock.json
index 56cfff2..492e65b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,17 +22,21 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@storybook/addon-docs": "^10.3.1",
+ "@storybook/addon-vitest": "^10.3.4",
"@storybook/react-vite": "^10.3.1",
"@types/node": "^24.10.1",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^5.1.1",
+ "@vitest/browser-playwright": "^4.1.2",
+ "@vitest/coverage-v8": "^4.1.2",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "playwright": "^1.59.1",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"storybook": "^10.3.1",
@@ -40,7 +44,8 @@
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",
- "vite-plugin-svgr": "^4.5.0"
+ "vite-plugin-svgr": "^4.5.0",
+ "vitest": "^4.1.2"
}
},
"node_modules/@adobe/css-tools": {
@@ -356,6 +361,23 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@blazediff/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -1772,6 +1794,13 @@
"node": ">= 8"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2547,6 +2576,13 @@
"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/@storybook/addon-docs": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.1.tgz",
@@ -2570,6 +2606,42 @@
"storybook": "^10.3.1"
}
},
+ "node_modules/@storybook/addon-vitest": {
+ "version": "10.3.4",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.3.4.tgz",
+ "integrity": "sha512-lSn8opaHVzDxLtMy28FnSkyx6uP1oQVnGzodNunTjrbJ8Ue8JVK+fjWtC/JfErIio0avlq79mgC5tfHSWlPr9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@storybook/global": "^5.0.0",
+ "@storybook/icons": "^2.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "^3.0.0 || ^4.0.0",
+ "@vitest/browser-playwright": "^4.0.0",
+ "@vitest/runner": "^3.0.0 || ^4.0.0",
+ "storybook": "^10.3.4",
+ "vitest": "^3.0.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/runner": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@storybook/builder-vite": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.1.tgz",
@@ -3460,6 +3532,170 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
+ "node_modules/@vitest/browser": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.2.tgz",
+ "integrity": "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@blazediff/core": "1.9.1",
+ "@vitest/mocker": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "magic-string": "^0.30.21",
+ "pngjs": "^7.0.0",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.1.0",
+ "ws": "^8.19.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "4.1.2"
+ }
+ },
+ "node_modules/@vitest/browser-playwright": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.2.tgz",
+ "integrity": "sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/browser": "4.1.2",
+ "@vitest/mocker": "4.1.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "vitest": "4.1.2"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@vitest/browser-playwright/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz",
+ "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.2",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.2",
+ "vitest": "4.1.2"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -3477,6 +3713,53 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
+ "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.2",
+ "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/@vitest/spy": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
+ "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "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": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
@@ -3490,6 +3773,112 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@vitest/runner": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz",
+ "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.2",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz",
+ "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@vitest/spy": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
@@ -3518,6 +3907,13 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@webcontainer/env": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz",
+ "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3675,19 +4071,48 @@
"node": ">=4"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/autoprefixer": {
- "version": "10.4.23",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
- "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
+ "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
"dev": true,
- "funding": [
- {
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/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/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
@@ -4378,6 +4803,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -4690,6 +5122,16 @@
"node": ">=0.10.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/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5205,6 +5647,13 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-parser-js": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
@@ -5400,6 +5849,45 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -5622,6 +6110,47 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5732,6 +6261,16 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5825,6 +6364,17 @@
"node": ">= 6"
}
},
+ "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/open": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
@@ -5990,6 +6540,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/pathval": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
@@ -6040,6 +6597,63 @@
"node": ">= 6"
}
},
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "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,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -6783,6 +7397,28 @@
"node": ">=8"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@@ -6814,10 +7450,24 @@
"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": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/storybook": {
- "version": "10.3.1",
- "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.1.tgz",
- "integrity": "sha512-i/CA1dUyVcF6cNL3tgPTQ/G6Evh6r3QdATuiiKObrA3QkEKmt3jrY+WeuQA7FCcmHk/vKabeliNrblaff8aY6Q==",
+ "version": "10.3.4",
+ "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.4.tgz",
+ "integrity": "sha512-866YXZy9k59tLPl9SN3KZZOFeBC/swxkuBVtW8iQjJIzfCrvk7zXQd8RSQ4ignmCdArVvY4lGMCAT4yNaZSt1g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6827,6 +7477,7 @@
"@testing-library/user-event": "^14.6.1",
"@vitest/expect": "3.2.4",
"@vitest/spy": "3.2.4",
+ "@webcontainer/env": "^1.1.1",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0",
"open": "^10.2.0",
"recast": "^0.23.5",
@@ -7049,6 +7700,23 @@
"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.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+ "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+ "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",
@@ -7099,6 +7767,16 @@
"node": ">=8.0"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -7427,6 +8105,164 @@
"vite": ">=2.6.0"
}
},
+ "node_modules/vitest": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz",
+ "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.2",
+ "@vitest/mocker": "4.1.2",
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/runner": "4.1.2",
+ "@vitest/snapshot": "4.1.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "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.2",
+ "@vitest/browser-preview": "4.1.2",
+ "@vitest/browser-webdriverio": "4.1.2",
+ "@vitest/ui": "4.1.2",
+ "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/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/expect": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
+ "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/spy": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
+ "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest/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/vitest/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
@@ -7479,6 +8315,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
diff --git a/package.json b/package.json
index c4329b7..83a45cd 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,9 @@
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"preview": "vite preview",
"storybook": "storybook dev -p 6006 --config-dir storybook",
- "build-storybook": "storybook build --config-dir storybook"
+ "build-storybook": "storybook build --config-dir storybook",
+ "test:unit": "vitest --project=unit --run",
+ "test:storybook": "vitest --project=storybook"
},
"dependencies": {
"@tanstack/react-query": "^5.90.21",
@@ -29,6 +31,7 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@storybook/addon-docs": "^10.3.1",
+ "@storybook/addon-vitest": "^10.3.4",
"@storybook/react-vite": "^10.3.1",
"@types/node": "^24.10.1",
"@types/react": "^18.3.27",
@@ -47,6 +50,10 @@
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",
- "vite-plugin-svgr": "^4.5.0"
+ "vite-plugin-svgr": "^4.5.0",
+ "vitest": "^4.1.2",
+ "playwright": "^1.59.1",
+ "@vitest/browser-playwright": "^4.1.2",
+ "@vitest/coverage-v8": "^4.1.2"
}
}
diff --git a/src/features/home/user/constants/calendar.ts b/src/features/home/user/constants/calendar.ts
index 5da5cb8..6c4a0ae 100644
--- a/src/features/home/user/constants/calendar.ts
+++ b/src/features/home/user/constants/calendar.ts
@@ -8,6 +8,12 @@ export const WEEKDAY_LABELS = [
'토',
] as const
+/** 월요일 시작 주( date-fns weekStartsOn: 1 )와 그리드 열 순서를 맞춤 */
+export const WEEKDAY_LABELS_MONDAY_FIRST = [
+ ...WEEKDAY_LABELS.slice(1),
+ WEEKDAY_LABELS[0],
+] as const
+
export const DATE_KEY_FORMAT = 'yyyy-MM-dd'
export const MONTH_LABEL_FORMAT = 'yyyy년 M월'
diff --git a/src/features/home/user/hooks/useMonthlyCalendarViewModel.ts b/src/features/home/user/hooks/useMonthlyCalendarViewModel.ts
index e2aeaf3..6619cac 100644
--- a/src/features/home/user/hooks/useMonthlyCalendarViewModel.ts
+++ b/src/features/home/user/hooks/useMonthlyCalendarViewModel.ts
@@ -13,7 +13,7 @@ import { useMemo } from 'react'
import {
DATE_KEY_FORMAT,
MONTH_LABEL_FORMAT,
- WEEKDAY_LABELS,
+ WEEKDAY_LABELS_MONDAY_FIRST,
} from '@/features/home/user/constants/calendar'
import { useMonthlyDateCellsState } from '@/features/home/user/hooks/useMonthlyDateCellsState'
import type {
@@ -27,8 +27,8 @@ import type { CalendarViewData } from '@/features/home/user/types/schedule'
function getMonthlyCells(baseDate: Date): MonthlyCellInput[] {
const monthStart = startOfMonth(baseDate)
const monthEnd = endOfMonth(baseDate)
- const intervalStart = startOfWeek(monthStart, { weekStartsOn: 0 })
- const intervalEnd = endOfWeek(monthEnd, { weekStartsOn: 0 })
+ const intervalStart = startOfWeek(monthStart, { weekStartsOn: 1 })
+ const intervalEnd = endOfWeek(monthEnd, { weekStartsOn: 1 })
return eachDayOfInterval({ start: intervalStart, end: intervalEnd }).map(
date => ({
@@ -133,7 +133,7 @@ export function useMonthlyCalendarViewModel({
totalWorkHoursText: String(
Math.round(data?.summary.totalWorkHours ?? 0)
).padStart(2, '0'),
- weekdayLabels: WEEKDAY_LABELS,
+ weekdayLabels: WEEKDAY_LABELS_MONDAY_FIRST,
monthlyDateCellsState,
}
}
diff --git a/src/features/home/user/lib/date.test.ts b/src/features/home/user/lib/date.test.ts
new file mode 100644
index 0000000..f05b8b2
--- /dev/null
+++ b/src/features/home/user/lib/date.test.ts
@@ -0,0 +1,177 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import type { ScheduleItemDto } from '@/features/home/user/types/schedule'
+import { StatusEnum } from '@/shared/types/enums'
+
+import {
+ getDailyHourTicks,
+ getDayHours,
+ getDurationHours,
+ getMonthlyDateCells,
+ getRangeParamsByMode,
+ getWeekRangeLabel,
+ getWeeklyDateCells,
+ moveDateByMode,
+ toDateKey,
+ toTimeLabel,
+} from './date'
+
+describe('toDateKey', () => {
+ it('ISO 문자열에서 날짜 부분만 반환한다', () => {
+ expect(toDateKey('2026-04-05T09:30:00')).toBe('2026-04-05')
+ })
+})
+
+describe('toTimeLabel', () => {
+ it('ISO 문자열에서 HH:mm 구간을 반환한다', () => {
+ expect(toTimeLabel('2026-04-05T14:30:00')).toBe('14:30')
+ })
+})
+
+describe('getDurationHours', () => {
+ it('시작·종료 시각 차이를 시간 단위로 반환한다', () => {
+ expect(getDurationHours('2026-04-05T10:00:00', '2026-04-05T12:30:00')).toBe(
+ 2.5
+ )
+ })
+
+ it('종료가 시작보다 이르면 0을 반환한다', () => {
+ expect(getDurationHours('2026-04-05T12:00:00', '2026-04-05T10:00:00')).toBe(
+ 0
+ )
+ })
+})
+
+describe('getMonthlyDateCells', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date(2026, 3, 15, 12, 0, 0))
+ })
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('그리드에 dateKey·day·isCurrentMonth·isToday가 포함된다', () => {
+ const cells = getMonthlyDateCells(new Date(2026, 3, 1))
+ expect(cells.length).toBeGreaterThan(27)
+ const todayCell = cells.find(c => c.dateKey === '2026-04-15')
+ expect(todayCell?.isToday).toBe(true)
+ expect(
+ cells
+ .filter(c => c.isCurrentMonth)
+ .every(c => c.dateKey.startsWith('2026-04'))
+ ).toBe(true)
+ expect(cells.some(c => !c.isCurrentMonth)).toBe(true)
+ })
+})
+
+describe('getWeeklyDateCells', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date(2026, 3, 15, 12, 0, 0))
+ })
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('한 주 7일과 요일 라벨을 반환한다', () => {
+ const cells = getWeeklyDateCells(new Date(2026, 3, 15))
+ expect(cells).toHaveLength(7)
+ expect(cells.every(c => c.dayLabel.length > 0)).toBe(true)
+ const today = cells.find(c => c.dateKey === '2026-04-15')
+ expect(today?.isToday).toBe(true)
+ })
+})
+
+describe('getDayHours', () => {
+ const events: ScheduleItemDto[] = [
+ {
+ shiftId: 1,
+ workspace: { workspaceId: 1, workspaceName: 'A' },
+ startDateTime: '2026-04-05T09:00:00',
+ endDateTime: '2026-04-05T11:00:00',
+ position: 'x',
+ status: StatusEnum.CONFIRMED,
+ },
+ {
+ shiftId: 2,
+ workspace: { workspaceId: 1, workspaceName: 'A' },
+ startDateTime: '2026-04-06T10:00:00',
+ endDateTime: '2026-04-06T12:00:00',
+ position: 'x',
+ status: StatusEnum.CONFIRMED,
+ },
+ ]
+
+ it('해당 dateKey 이벤트만 합산한다', () => {
+ expect(getDayHours(events, '2026-04-05')).toBe(2)
+ expect(getDayHours(events, '2026-04-06')).toBe(2)
+ expect(getDayHours(events, '2026-04-07')).toBe(0)
+ })
+
+ it('빈 배열이면 0이다', () => {
+ expect(getDayHours([], '2026-04-05')).toBe(0)
+ })
+})
+
+describe('getWeekRangeLabel', () => {
+ it('주의 시작·끝을 M/d 형식으로 반환한다', () => {
+ expect(getWeekRangeLabel(new Date(2026, 3, 15))).toMatch(
+ /^\d{1,2}\/\d{1,2} - \d{1,2}\/\d{1,2}$/
+ )
+ })
+})
+
+describe('getDailyHourTicks', () => {
+ it('00:00부터 23:00까지 24개를 반환한다', () => {
+ const ticks = getDailyHourTicks()
+ expect(ticks).toHaveLength(24)
+ expect(ticks[0]).toBe('00:00')
+ expect(ticks[23]).toBe('23:00')
+ })
+})
+
+describe('getRangeParamsByMode', () => {
+ const base = new Date(2026, 3, 15)
+
+ it('monthly이면 해당 월의 첫날·마지막날이다', () => {
+ expect(getRangeParamsByMode(base, 'monthly')).toEqual({
+ startDate: '2026-04-01',
+ endDate: '2026-04-30',
+ })
+ })
+
+ it('weekly이면 해당 주 월요일~일요일이다', () => {
+ const { startDate, endDate } = getRangeParamsByMode(base, 'weekly')
+ expect(startDate <= endDate).toBe(true)
+ expect(startDate).toMatch(/^\d{4}-\d{2}-\d{2}$/)
+ })
+
+ it('daily이면 하루 범위로 동일한 날짜다', () => {
+ expect(getRangeParamsByMode(base, 'daily')).toEqual({
+ startDate: '2026-04-15',
+ endDate: '2026-04-15',
+ })
+ })
+})
+
+describe('moveDateByMode', () => {
+ const base = new Date(2026, 3, 15)
+
+ it('monthly prev/next는 한 달씩 이동한다', () => {
+ expect(moveDateByMode(base, 'prev', 'monthly').getMonth()).toBe(2)
+ expect(moveDateByMode(base, 'next', 'monthly').getMonth()).toBe(4)
+ })
+
+ it('weekly prev/next는 7일씩 이동한다', () => {
+ const prev = moveDateByMode(base, 'prev', 'weekly')
+ const next = moveDateByMode(base, 'next', 'weekly')
+ expect(prev.getTime()).toBe(base.getTime() - 7 * 24 * 60 * 60 * 1000)
+ expect(next.getTime()).toBe(base.getTime() + 7 * 24 * 60 * 60 * 1000)
+ })
+
+ it('daily prev/next는 하루씩 이동한다', () => {
+ expect(moveDateByMode(base, 'prev', 'daily').getDate()).toBe(14)
+ expect(moveDateByMode(base, 'next', 'daily').getDate()).toBe(16)
+ })
+})
diff --git a/src/features/home/user/types/monthlyCalendar.ts b/src/features/home/user/types/monthlyCalendar.ts
index f32fdf1..bed9761 100644
--- a/src/features/home/user/types/monthlyCalendar.ts
+++ b/src/features/home/user/types/monthlyCalendar.ts
@@ -1,4 +1,4 @@
-import type { WEEKDAY_LABELS } from '@/features/home/user/constants/calendar'
+import type { WEEKDAY_LABELS_MONDAY_FIRST } from '@/features/home/user/constants/calendar'
import type { BaseCalendarProps } from '@/features/home/user/types/calendar'
export interface MonthlyCellInput {
@@ -31,7 +31,7 @@ export interface MonthlyCalendarViewModel {
title: string
monthLabel: string
totalWorkHoursText: string
- weekdayLabels: typeof WEEKDAY_LABELS
+ weekdayLabels: typeof WEEKDAY_LABELS_MONDAY_FIRST
monthlyDateCellsState: MonthlyDateCellState[]
}
diff --git a/src/features/home/user/ui/MonthlyCalendar.tsx b/src/features/home/user/ui/MonthlyCalendar.tsx
index 027ddc2..225a0ec 100644
--- a/src/features/home/user/ui/MonthlyCalendar.tsx
+++ b/src/features/home/user/ui/MonthlyCalendar.tsx
@@ -57,7 +57,7 @@ export function MonthlyCalendar({
{weekdayLabels.map((label, index) => (
{label}
diff --git a/storybook/main.ts b/storybook/main.ts
index 4aed413..c25a90f 100644
--- a/storybook/main.ts
+++ b/storybook/main.ts
@@ -8,7 +8,7 @@ const config: StorybookConfig = {
'./stories/**/*.mdx',
'./stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
- addons: ['@storybook/addon-docs'],
+ addons: ['@storybook/addon-docs', '@storybook/addon-vitest'],
framework: '@storybook/react-vite',
}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index e6e8706..a0bf1ab 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -30,5 +30,5 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"]
+ "include": ["src", "vitest.shims.d.ts"]
}
diff --git a/vite.config.ts b/vite.config.ts
index bfa1264..f6b0e44 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,11 @@
+///
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr'
import path from 'path'
import { fileURLToPath } from 'node:url'
+import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
+import { playwright } from '@vitest/browser-playwright'
const dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -28,4 +31,56 @@ export default defineConfig({
},
},
},
+ test: {
+ // 브라우저 커버리지: 리맵 후 exclude를 다시 적용해 CSS·정적 자산·Storybook preview 등을 리포트에서 제외
+ coverage: {
+ provider: 'v8',
+ excludeAfterRemap: true,
+ include: ['src/**/*.{ts,tsx}'],
+ exclude: [
+ 'coverage/**',
+ 'src/**/*.stories.{ts,tsx}',
+ 'src/**/*.mdx',
+ 'src/**/*.d.ts',
+ 'src/assets/**',
+ 'src/app/styles/**',
+ 'src/**/types/**',
+ 'storybook/**',
+ '**/*.{css,png,jpg,jpeg,gif,webp,svg,ico}',
+ ],
+ },
+ projects: [
+ {
+ extends: true,
+ test: {
+ name: 'unit',
+ environment: 'node',
+ include: ['src/**/*.test.{ts,tsx}'],
+ },
+ },
+ {
+ extends: true,
+ plugins: [
+ // The plugin will run tests for the stories defined in your Storybook config
+ // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ storybookTest({
+ configDir: path.join(dirname, 'storybook'),
+ }),
+ ],
+ test: {
+ name: 'storybook',
+ browser: {
+ enabled: true,
+ headless: true,
+ provider: playwright({}),
+ instances: [
+ {
+ browser: 'chromium',
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
})
diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts
new file mode 100644
index 0000000..03b1801
--- /dev/null
+++ b/vitest.shims.d.ts
@@ -0,0 +1 @@
+///