diff --git a/.gitignore b/.gitignore index d3b3c8f4..31c94eba 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,5 @@ out/ ### VS Code ### .vscode/ - -frontend logs .kotlin \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5b4be923..20f59d2f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,6 @@ repositories { dependencies { // Spring implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-validation") @@ -71,8 +70,8 @@ tasks.withType { tasks.withType { compilerOptions { freeCompilerArgs.addAll( - "-Xjsr305=strict", - "-Xannotation-default-target=param-property" + "-Xjsr305=strict", + "-Xannotation-default-target=param-property" ) jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 00000000..2717bd56 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL = "http://localhost:8080" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..f804f4f7 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.DS_Store + +.env \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..7959ce42 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..d94e7deb --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..f3e3a737 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 방탈출 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..57d002b4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3836 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.2", + "bootstrap": "^5.3.3", + "flatpickr": "^4.6.13", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-flatpickr": "^3.10.13", + "react-router-dom": "^6.23.1", + "vite-tsconfig-paths": "^5.1.4" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-flatpickr": "^3.8.11", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-flatpickr": { + "version": "3.8.11", + "resolved": "https://registry.npmjs.org/@types/react-flatpickr/-/react-flatpickr-3.8.11.tgz", + "integrity": "sha512-wXGyGRpUjiGknioxWzWJdNvF2XxKw5lAI7H64Iv7w4iL+1iT7QvAzrigz5FkW4lTg9IJOww6t7g21FzsrmRV6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "flatpickr": "^4.0.6" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bootstrap": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", + "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-flatpickr": { + "version": "3.10.13", + "resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.13.tgz", + "integrity": "sha512-4m+K1K8jhvRFI8J/AHmQfA5hLALzhebEtEK8mLevXjX24MV3u502crzBn+EGFIBOfNUtrL5PId9FsGwgtuz/og==", + "license": "MIT", + "dependencies": { + "flatpickr": "^4.6.2", + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": ">=16, <=18" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..e200fb80 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.2", + "bootstrap": "^5.3.3", + "flatpickr": "^4.6.13", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-flatpickr": "^3.10.13", + "react-router-dom": "^6.23.1", + "vite-tsconfig-paths": "^5.1.4" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-flatpickr": "^3.8.11", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/src/main/resources/static/image/admin-logo.png b/frontend/public/image/admin-logo.png similarity index 100% rename from src/main/resources/static/image/admin-logo.png rename to frontend/public/image/admin-logo.png diff --git a/src/main/resources/static/image/default-profile.png b/frontend/public/image/default-profile.png similarity index 100% rename from src/main/resources/static/image/default-profile.png rename to frontend/public/image/default-profile.png diff --git a/frontend/public/image/service-logo.png b/frontend/public/image/service-logo.png new file mode 100644 index 00000000..10ccbcb8 Binary files /dev/null and b/frontend/public/image/service-logo.png differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..225f6538 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,56 @@ +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import Layout from './components/Layout'; +import HomePage from './pages/HomePage'; +import LoginPage from './pages/LoginPage'; +import SignupPage from './pages/SignupPage'; +import ReservationPage from './pages/ReservationPage'; +import MyReservationPage from './pages/MyReservationPage'; +import AdminLayout from './pages/admin/AdminLayout'; +import AdminPage from './pages/admin/AdminPage'; +import AdminReservationPage from './pages/admin/ReservationPage'; +import AdminTimePage from './pages/admin/TimePage'; +import AdminThemePage from './pages/admin/ThemePage'; +import AdminWaitingPage from './pages/admin/WaitingPage'; +import { AuthProvider } from './context/AuthContext'; +import AdminRoute from './components/AdminRoute'; + +const AdminRoutes = () => ( + + + } /> + } /> + } /> + } /> + } /> + + +); + +function App() { + return ( + + + + + + + } /> + + + } /> + } /> + } /> + } /> + } /> + + + } /> + + + + ); +} + +export default App; diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts new file mode 100644 index 00000000..83090c44 --- /dev/null +++ b/frontend/src/api/apiClient.ts @@ -0,0 +1,81 @@ +import axios, { type AxiosError, type AxiosRequestConfig, type Method } from 'axios'; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', + timeout: 10000, +}); + +export const isLoginRequiredError = (error: any): boolean => { + if (!axios.isAxiosError(error) || !error.response) { + return false; + } + const LOGIN_REQUIRED_ERROR_CODE = ['A001', 'A002', 'A003', 'A006']; + const code = error.response?.data?.code; + return code && LOGIN_REQUIRED_ERROR_CODE.includes(code); +} + +async function request( + method: Method, + endpoint: string, + data: object = {}, + isRequiredAuth: boolean = false +): Promise { + const config: AxiosRequestConfig = { + method, + url: endpoint, + headers: { + 'Content-Type': 'application/json' + }, + }; + + if (isRequiredAuth) { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + if (!config.headers) { + config.headers = {}; + } + config.headers['Authorization'] = `Bearer ${accessToken}`; + } + } + + if (method.toUpperCase() !== 'GET') { + config.data = data; + } + + try { + const response = await apiClient.request(config); + return response.data.data; + } catch (error: unknown) { + const axiosError = error as AxiosError<{ code: string, message: string }>; + console.error('API 요청 실패:', axiosError); + throw axiosError; + } +} + +async function get(endpoint: string, isRequiredAuth: boolean = false): Promise { + return request('GET', endpoint, {}, isRequiredAuth); +} + +async function post(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('POST', endpoint, data, isRequiredAuth); +} + +async function put(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('PUT', endpoint, data, isRequiredAuth); +} + +async function patch(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { + return request('PATCH', endpoint, data, isRequiredAuth); +} + +async function del(endpoint: string, isRequiredAuth: boolean = false): Promise { + return request('DELETE', endpoint, {}, isRequiredAuth); +} + +export default { + get, + post, + put, + patch, + del +}; diff --git a/frontend/src/api/auth/authAPI.ts b/frontend/src/api/auth/authAPI.ts new file mode 100644 index 00000000..a9f34dfd --- /dev/null +++ b/frontend/src/api/auth/authAPI.ts @@ -0,0 +1,19 @@ +import apiClient from '@_api/apiClient'; +import type { LoginRequest, LoginResponse, LoginCheckResponse } from './authTypes'; + + +export const login = async (data: LoginRequest): Promise => { + const response = await apiClient.post('/login', data, false); + localStorage.setItem('accessToken', response.accessToken); + + return response; +}; + +export const checkLogin = async (): Promise => { + return await apiClient.get('/login/check', true); +}; + +export const logout = async (): Promise => { + await apiClient.post('/logout', {}, true); + localStorage.removeItem('accessToken'); +}; diff --git a/frontend/src/api/auth/authTypes.ts b/frontend/src/api/auth/authTypes.ts new file mode 100644 index 00000000..6889425f --- /dev/null +++ b/frontend/src/api/auth/authTypes.ts @@ -0,0 +1,14 @@ +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + accessToken: string; +} + +export interface LoginCheckResponse { + name: string; + role: 'ADMIN' | 'MEMBER'; +} + diff --git a/frontend/src/api/member/memberAPI.ts b/frontend/src/api/member/memberAPI.ts new file mode 100644 index 00000000..219e7f3a --- /dev/null +++ b/frontend/src/api/member/memberAPI.ts @@ -0,0 +1,10 @@ +import apiClient from "@_api/apiClient"; +import type { MemberRetrieveListResponse, SignupRequest, SignupResponse } from "./memberTypes"; + +export const fetchMembers = async (): Promise => { + return await apiClient.get('/members', true); +}; + +export const signup = async (data: SignupRequest): Promise => { + return await apiClient.post('/members', data, false); +}; diff --git a/frontend/src/api/member/memberTypes.ts b/frontend/src/api/member/memberTypes.ts new file mode 100644 index 00000000..45d90e86 --- /dev/null +++ b/frontend/src/api/member/memberTypes.ts @@ -0,0 +1,19 @@ +export interface MemberRetrieveResponse { + id: number; + name: string; +} + +export interface MemberRetrieveListResponse { + members: MemberRetrieveResponse[]; +} + +export interface SignupRequest { + email: string; + password: string; + name: string; +} + +export interface SignupResponse { + id: number; + name: string; +} diff --git a/frontend/src/api/reservation/reservationAPI.ts b/frontend/src/api/reservation/reservationAPI.ts new file mode 100644 index 00000000..276ce06f --- /dev/null +++ b/frontend/src/api/reservation/reservationAPI.ts @@ -0,0 +1,70 @@ +import apiClient from "@_api/apiClient"; +import type { + AdminReservationCreateRequest, + MyReservationRetrieveListResponse, + ReservationCreateWithPaymentRequest, + ReservationRetrieveListResponse, + ReservationRetrieveResponse, + ReservationSearchQuery, + WaitingCreateRequest +} from "./reservationTypes"; + +// GET /reservations +export const fetchReservations = async (): Promise => { + return await apiClient.get('/reservations', true); +}; + +// GET /reservations-mine +export const fetchMyReservations = async (): Promise => { + return await apiClient.get('/reservations-mine', true); +}; + +// GET /reservations/search +export const searchReservations = async (params: ReservationSearchQuery): Promise => { + const query = new URLSearchParams(); + if (params.themeId) query.append('themeId', params.themeId.toString()); + if (params.memberId) query.append('memberId', params.memberId.toString()); + if (params.dateFrom) query.append('dateFrom', params.dateFrom); + if (params.dateTo) query.append('dateTo', params.dateTo); + return await apiClient.get(`/reservations/search?${query.toString()}`, true); +}; + +// DELETE /reservations/{id} +export const cancelReservationByAdmin = async (id: number): Promise => { + return await apiClient.del(`/reservations/${id}`, true); +}; + +// POST /reservations +export const createReservationWithPayment = async (data: ReservationCreateWithPaymentRequest): Promise => { + return await apiClient.post('/reservations', data, true); +}; + +// POST /reservations/admin +export const createReservationByAdmin = async (data: AdminReservationCreateRequest): Promise => { + return await apiClient.post('/reservations/admin', data, true); +}; + +// GET /reservations/waiting +export const fetchWaitingReservations = async (): Promise => { + return await apiClient.get('/reservations/waiting', true); +}; + +// POST /reservations/waiting +export const createWaiting = async (data: WaitingCreateRequest): Promise => { + return await apiClient.post('/reservations/waiting', data, true); +}; + +// DELETE /reservations/waiting/{id} +export const cancelWaiting = async (id: number): Promise => { + return await apiClient.del(`/reservations/waiting/${id}`, true); +}; + +// POST /reservations/waiting/{id}/confirm +export const confirmWaiting = async (id: number): Promise => { + return await apiClient.post(`/reservations/waiting/${id}/confirm`, {}, true); +}; + +// POST /reservations/waiting/{id}/reject +export const rejectWaiting = async (id: number): Promise => { + return await apiClient.post(`/reservations/waiting/${id}/reject`, {}, true); +}; diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts new file mode 100644 index 00000000..5bffd87f --- /dev/null +++ b/frontend/src/api/reservation/reservationTypes.ts @@ -0,0 +1,72 @@ +import type { MemberRetrieveResponse } from '@_api/member/memberTypes'; +import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes'; +import type { TimeRetrieveResponse } from '@_api/time/timeTypes'; + +export const ReservationStatus = { + CONFIRMED: 'CONFIRMED', + CONFIRMED_PAYMENT_REQUIRED: 'CONFIRMED_PAYMENT_REQUIRED', + WAITING: 'WAITING', +} as const; + +export type ReservationStatus = + | typeof ReservationStatus.CONFIRMED + | typeof ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + | typeof ReservationStatus.WAITING; + +export interface MyReservationRetrieveResponse { + id: number; + themeName: string; + date: string; + time: string; + status: ReservationStatus; + rank: number; + paymentKey: string | null; + amount: number | null; +} + +export interface MyReservationRetrieveListResponse { + reservations: MyReservationRetrieveResponse[]; +} + +export interface ReservationRetrieveResponse { + id: number; + date: string; + member: MemberRetrieveResponse; + time: TimeRetrieveResponse; + theme: ThemeRetrieveResponse; + status: ReservationStatus; +} + +export interface ReservationRetrieveListResponse { + reservations: ReservationRetrieveResponse[]; +} + +export interface AdminReservationCreateRequest { + date: string; + timeId: number; + themeId: number; + memberId: number; +} + +export interface ReservationCreateWithPaymentRequest { + date: string; + timeId: number; + themeId: number; + paymentKey: string; + orderId: string; + amount: number; + paymentType: string; +} + +export interface WaitingCreateRequest { + date: string; + timeId: number; + themeId: number; +} + +export interface ReservationSearchQuery { + themeId?: number; + memberId?: number; + dateFrom?: string; + dateTo?: string; +} diff --git a/frontend/src/api/theme/themeAPI.ts b/frontend/src/api/theme/themeAPI.ts new file mode 100644 index 00000000..c653a9e8 --- /dev/null +++ b/frontend/src/api/theme/themeAPI.ts @@ -0,0 +1,18 @@ +import apiClient from "@_api/apiClient"; +import type { ThemeCreateRequest, ThemeCreateResponse, ThemeRetrieveListResponse } from "./themeTypes"; + +export const createTheme = async (data: ThemeCreateRequest): Promise => { + return await apiClient.post('/themes', data, true); +}; + +export const fetchThemes = async (): Promise => { + return await apiClient.get('/themes', true); +}; + +export const mostReservedThemes = async (count: number = 10): Promise => { + return await apiClient.get(`/themes/most-reserved-last-week?count=${count}`, false); +}; + +export const delTheme = async (id: number): Promise => { + return await apiClient.del(`/themes/${id}`, true); +}; diff --git a/frontend/src/api/theme/themeTypes.ts b/frontend/src/api/theme/themeTypes.ts new file mode 100644 index 00000000..d1721953 --- /dev/null +++ b/frontend/src/api/theme/themeTypes.ts @@ -0,0 +1,23 @@ +export interface ThemeCreateRequest { + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeCreateResponse { + id: number; + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeRetrieveResponse { + id: number; + name: string; + description: string; + thumbnail: string; +} + +export interface ThemeRetrieveListResponse { + themes: ThemeRetrieveResponse[]; +} diff --git a/frontend/src/api/time/timeAPI.ts b/frontend/src/api/time/timeAPI.ts new file mode 100644 index 00000000..2a2d6ac2 --- /dev/null +++ b/frontend/src/api/time/timeAPI.ts @@ -0,0 +1,18 @@ +import apiClient from "@_api/apiClient"; +import type { TimeCreateRequest, TimeCreateResponse, TimeRetrieveListResponse, TimeWithAvailabilityListResponse } from "./timeTypes"; + +export const createTime = async (data: TimeCreateRequest): Promise => { + return await apiClient.post('/times', data, true); +} + +export const fetchTimes = async (): Promise => { + return await apiClient.get('/times', true); +}; + +export const delTime = async (id: number): Promise => { + return await apiClient.del(`/times/${id}`, true); +}; + +export const fetchTimesWithAvailability = async (date: string, themeId: number): Promise => { + return await apiClient.get(`/times/search?date=${date}&themeId=${themeId}`, true); +}; diff --git a/frontend/src/api/time/timeTypes.ts b/frontend/src/api/time/timeTypes.ts new file mode 100644 index 00000000..408e8f7f --- /dev/null +++ b/frontend/src/api/time/timeTypes.ts @@ -0,0 +1,27 @@ +export interface TimeCreateRequest { + startAt: string; +} + +export interface TimeCreateResponse { + id: number; + startAt: string; +} + +export interface TimeRetrieveResponse { + id: number; + startAt: string; +} + +export interface TimeRetrieveListResponse { + times: TimeCreateResponse[]; +} + +export interface TimeWithAvailabilityResponse { + id: number; + startAt: string; + isAvailable: boolean; +} + +export interface TimeWithAvailabilityListResponse { + times: TimeWithAvailabilityResponse[]; +} \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AdminRoute.tsx b/frontend/src/components/AdminRoute.tsx new file mode 100644 index 00000000..e8354014 --- /dev/null +++ b/frontend/src/components/AdminRoute.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +const AdminRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { + const { loggedIn, role, loading } = useAuth(); + const location = useLocation(); + + if (loading) { + return
Loading...
; // Or a proper spinner component + } + + if (!loggedIn) { + // Not logged in, redirect to login page. No alert needed here + // as the user is simply redirected. + return ; + } + + if (role !== 'ADMIN') { + // Logged in but not an admin, show alert and redirect. + alert('접근 권한이 없어요. 관리자에게 문의해주세요.'); + return ; + } + + return children; +}; + +export default AdminRoute; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 00000000..9c736580 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; +import Navbar from './Navbar'; + +interface LayoutProps { + children: ReactNode; +} + +const Layout: React.FC = ({ children }) => { + return ( + <> + +
{children}
+ + ); +}; + +export default Layout; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx new file mode 100644 index 00000000..83c75a2c --- /dev/null +++ b/frontend/src/components/Navbar.tsx @@ -0,0 +1,56 @@ +import { checkLogin } from '@_api/auth/authAPI'; +import React, { useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from 'src/context/AuthContext'; + +const Navbar: React.FC = () => { + const { loggedIn, userName, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + await logout(); + navigate('/'); + } catch (error) { + console.error('Logout failed:', error); + } + } + + return ( + + ); +}; + +export default Navbar; \ No newline at end of file diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 00000000..b4e83a61 --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,73 @@ +import React, { createContext, useState, useEffect, ReactNode, useContext } from 'react'; +import { checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout } from '@_api/auth/authAPI'; +import type { LoginRequest, LoginCheckResponse } from '@_api/auth/authTypes'; + +interface AuthContextType { + loggedIn: boolean; + userName: string | null; + role: 'ADMIN' | 'MEMBER' | null; + loading: boolean; // Add loading state to type + login: (data: LoginRequest) => Promise; + logout: () => Promise; + checkLogin: () => Promise; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [loggedIn, setLoggedIn] = useState(false); + const [userName, setUserName] = useState(null); + const [role, setRole] = useState<'ADMIN' | 'MEMBER' | null>(null); + const [loading, setLoading] = useState(true); // Add loading state + + const checkLogin = async () => { + try { + const response = await apiCheckLogin(); + setLoggedIn(true); + setUserName(response.name); + setRole(response.role); + } catch (error) { + setLoggedIn(false); + setUserName(null); + setRole(null); + localStorage.removeItem('accessToken'); + } finally { + setLoading(false); // Set loading to false after check is complete + } + }; + + useEffect(() => { + checkLogin(); + }, []); + + const login = async (data: LoginRequest) => { + const response = await apiLogin(data); + await checkLogin(); + return response; + }; + + const logout = async () => { + try { + await apiLogout(); + } finally { + setLoggedIn(false); + setUserName(null); + setRole(null); + localStorage.removeItem('accessToken'); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/main/resources/static/css/reservation.css b/frontend/src/css/reservation.css similarity index 100% rename from src/main/resources/static/css/reservation.css rename to frontend/src/css/reservation.css diff --git a/src/main/resources/static/css/style.css b/frontend/src/css/style.css similarity index 100% rename from src/main/resources/static/css/style.css rename to frontend/src/css/style.css diff --git a/src/main/resources/static/css/toss-style.css b/frontend/src/css/toss-style.css similarity index 100% rename from src/main/resources/static/css/toss-style.css rename to frontend/src/css/toss-style.css diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 00000000..0909b20b --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,67 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0 auto; + max-width: 60rem; + min-height: 100vh; + height: 100%; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 00000000..2b1afe7d --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import './css/style.css'; +import './css/reservation.css'; +import './css/toss-style.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 00000000..fd83b8a9 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from 'react'; +import { mostReservedThemes } from '@_api/theme/themeAPI'; + +const HomePage: React.FC = () => { + const [ranking, setRanking] = useState([]); + + useEffect(() => { + const fetchData = async () => { + await mostReservedThemes(10).then(response => setRanking(response.themes)) + }; + + fetchData().catch(err => console.error('Error fetching ranking:', err)); + }, []); + + return ( +
+

인기 테마

+
    + {ranking.map(theme => ( +
  • + {theme.name} +
    +
    {theme.name}
    + {theme.description} +
    +
  • + ))} +
+
+ ); +}; + +export default HomePage; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 00000000..104ad3ce --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import type { LoginRequest } from '@_api/auth/authTypes'; +import { useAuth } from '../context/AuthContext'; + +const LoginPage: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const { login } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + const from = location.state?.from?.pathname || '/'; + + const handleLogin = async () => { + try { + const request: LoginRequest = { email, password }; + await login(request); + + alert('로그인에 성공했어요!'); + navigate(from, { replace: true }); + } catch (error: any) { + const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.'; + alert(message); + console.error('로그인 실패:', error); + setEmail(''); + setPassword(''); + } + } + + return ( +
+

Login

+
+ setEmail(e.target.value)} /> +
+
+ setPassword(e.target.value)} /> +
+
+ + +
+
+ ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/frontend/src/pages/MyReservationPage.tsx b/frontend/src/pages/MyReservationPage.tsx new file mode 100644 index 00000000..a599a2e3 --- /dev/null +++ b/frontend/src/pages/MyReservationPage.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { cancelWaiting, fetchMyReservations } from '@_api/reservation/reservationAPI'; +import type { MyReservationRetrieveResponse } from '@_api/reservation/reservationTypes'; +import { ReservationStatus } from '@_api/reservation/reservationTypes'; +import { isLoginRequiredError } from '@_api/apiClient'; + +const MyReservationPage: React.FC = () => { + const [reservations, setReservations] = useState([]); + const navigate = useNavigate(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + fetchMyReservations() + .then(res => setReservations(res.reservations)) + .catch(handleError); + }, []); + + const _cancelWaiting = (id: number) => { + cancelWaiting(id) + .then(() => { + alert('예약 대기가 취소되었습니다.'); + setReservations(reservations.filter(r => r.id !== id)); + }) + .catch(handleError); + }; + + const getStatusText = (status: ReservationStatus, rank: number) => { + if (status === ReservationStatus.CONFIRMED) { + return '예약'; + } + if (status === ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) { + return '예약 - 결제 필요'; + } + if (status === ReservationStatus.WAITING) { + return `${rank}번째 예약 대기`; + } + return ''; + }; + + return ( +
+

내 예약

+
+ + + + + + + + + + + + + + + {reservations.map(r => ( + + + + + + + + + + + ))} + +
테마날짜시간상태대기 취소paymentKey결제금액
{r.themeName}{r.date}{r.time}{getStatusText(r.status, r.rank)} + {r.status === ReservationStatus.WAITING && + } + {r.paymentKey}{r.amount}
+
+ ); +}; + +export default MyReservationPage; diff --git a/frontend/src/pages/ReservationPage.tsx b/frontend/src/pages/ReservationPage.tsx new file mode 100644 index 00000000..00d3e87a --- /dev/null +++ b/frontend/src/pages/ReservationPage.tsx @@ -0,0 +1,199 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import Flatpickr from 'react-flatpickr'; +import 'flatpickr/dist/flatpickr.min.css'; +import { fetchThemes } from '@_api/theme/themeAPI'; +import { fetchTimesWithAvailability } from '@_api/time/timeAPI'; +import { createReservationWithPayment, createWaiting } from '@_api/reservation/reservationAPI'; +import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes'; +import type { TimeWithAvailabilityResponse } from '@_api/time/timeTypes'; +import { isLoginRequiredError } from '@_api/apiClient'; + +declare global { + interface Window { + PaymentWidget: any; + } +} + +const ReservationPage: React.FC = () => { + const [selectedDate, setSelectedDate] = useState(new Date()); + const [themes, setThemes] = useState([]); + const [selectedTheme, setSelectedTheme] = useState(null); + const [times, setTimes] = useState([]); + const [selectedTime, setSelectedTime] = useState<{ id: number, isAvailable: boolean } | null>(null); + const paymentWidgetRef = useRef(null); + const paymentMethodsRef = useRef(null); + const navigate = useNavigate(); + const location = useLocation(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + const script = document.createElement('script'); + script.src = 'https://js.tosspayments.com/v1/payment-widget'; + script.async = true; + document.head.appendChild(script); + + script.onload = () => { + const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm"; + const paymentWidget = window.PaymentWidget(widgetClientKey, window.PaymentWidget.ANONYMOUS); + paymentWidgetRef.current = paymentWidget; + + const paymentMethods = paymentWidget.renderPaymentMethods( + "#payment-method", + { value: 1000 }, + { variantKey: "DEFAULT" } + ); + paymentMethodsRef.current = paymentMethods; + }; + + fetchThemes().then(res => setThemes(res.themes)).catch(handleError); + }, []); + + useEffect(() => { + if (selectedDate && selectedTheme) { + const dateStr = selectedDate.toLocaleDateString('en-CA'); + fetchTimesWithAvailability(dateStr, selectedTheme) + .then(res => { + setTimes(res.times); + setSelectedTime(null); + }) + .catch(handleError); + } + }, [selectedDate, selectedTheme]); + + const handleReservation = () => { + if (!selectedDate || !selectedTheme || !selectedTime || !paymentWidgetRef.current) { + alert('날짜, 테마, 시간을 모두 선택해주세요.'); + return; + } + + const reservationData = { + date: selectedDate.toLocaleDateString('en-CA'), + themeId: selectedTheme, + timeId: selectedTime.id, + }; + + const generateRandomString = () => + window.btoa(Math.random().toString()).slice(0, 20); + const orderIdPrefix = "WTEST"; + + paymentWidgetRef.current.requestPayment({ + orderId: orderIdPrefix + generateRandomString(), + orderName: "테스트 방탈출 예약 결제 1건", + amount: 1000, + }).then(function (data: any) { + const reservationPaymentRequest = { + ...reservationData, + paymentKey: data.paymentKey, + orderId: data.orderId, + amount: data.amount, + paymentType: data.paymentType, + }; + createReservationWithPayment(reservationPaymentRequest) + .then(() => { + alert("예약이 완료되었습니다."); + window.location.href = "/"; + }) + .catch(handleError); + }).catch(function (error: any) { + // This is a client-side error from Toss Payments, not our API + console.error("Payment request error:", error); + alert("결제 요청 중 오류가 발생했습니다."); + }); + }; + + const handleWaiting = () => { + if (!selectedDate || !selectedTheme || !selectedTime) { + alert('날짜, 테마, 시간을 모두 선택해주세요.'); + return; + } + + const reservationData = { + date: selectedDate.toLocaleDateString('en-CA'), + themeId: selectedTheme, + timeId: selectedTime.id, + }; + + createWaiting(reservationData) + .then(() => { + alert('예약 대기가 완료되었습니다.'); + window.location.href = "/"; + }) + .catch(handleError); + } + + const isReserveButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || !selectedTime.isAvailable; + const isWaitButtonDisabled = !selectedDate || !selectedTheme || !selectedTime || selectedTime.isAvailable; + + return ( + <> +
+

예약 페이지

+
+
+

날짜 선택

+
+ setSelectedDate(date)} + options={{ inline: true, defaultDate: new Date() }} + /> +
+
+ +
+

테마 선택

+
+ {themes.map(theme => ( +
setSelectedTheme(theme.id)}> + {theme.name} +
+ ))} +
+
+ +
+

시간 선택

+
+ {times.length > 0 ? times.map(time => ( +
setSelectedTime({ id: time.id, isAvailable: time.isAvailable })}> + {time.startAt} +
+ )) :
선택할 수 있는 시간이 없습니다.
} +
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+
+ + ); +}; + +export default ReservationPage; diff --git a/frontend/src/pages/SignupPage.tsx b/frontend/src/pages/SignupPage.tsx new file mode 100644 index 00000000..ed2c208d --- /dev/null +++ b/frontend/src/pages/SignupPage.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { signup } from '@_api/member/memberAPI'; +import type { SignupRequest } from '@_api/member/memberTypes'; + +const SignupPage: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [name, setName] = useState(''); + const navigate = useNavigate(); + + const handleSignup = async () => { + const request: SignupRequest = { email, password, name }; + await signup(request) + .then((response) => { + alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`); + navigate('/login') + }) + .catch(error => { + console.error(error); + }); + }; + + return ( +
+

Signup

+
+ + setEmail(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+
+ + setName(e.target.value)} /> +
+ +
+ ); +}; + +export default SignupPage; diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx new file mode 100644 index 00000000..26987dbe --- /dev/null +++ b/frontend/src/pages/admin/AdminLayout.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; +import AdminNavbar from './AdminNavbar'; + +interface AdminLayoutProps { + children: ReactNode; +} + +const AdminLayout: React.FC = ({ children }) => { + return ( + <> + +
{children}
+ + ); +}; + +export default AdminLayout; \ No newline at end of file diff --git a/frontend/src/pages/admin/AdminNavbar.tsx b/frontend/src/pages/admin/AdminNavbar.tsx new file mode 100644 index 00000000..e62abc03 --- /dev/null +++ b/frontend/src/pages/admin/AdminNavbar.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; + +const AdminNavbar: React.FC = () => { + const { loggedIn, userName, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + await logout(); + navigate('/'); + } catch (error) { + console.error("Logout failed", error); + // Handle logout error if needed + } + }; + + return ( + + ); +}; + +export default AdminNavbar; diff --git a/frontend/src/pages/admin/AdminPage.tsx b/frontend/src/pages/admin/AdminPage.tsx new file mode 100644 index 00000000..5331922d --- /dev/null +++ b/frontend/src/pages/admin/AdminPage.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const AdminPage: React.FC = () => { + return ( +
+

방탈출 어드민

+
+ ); +}; + +export default AdminPage; \ No newline at end of file diff --git a/frontend/src/pages/admin/ReservationPage.tsx b/frontend/src/pages/admin/ReservationPage.tsx new file mode 100644 index 00000000..5f5d9b60 --- /dev/null +++ b/frontend/src/pages/admin/ReservationPage.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + cancelReservationByAdmin, + createReservationByAdmin, + fetchReservations, + searchReservations +} from '@_api/reservation/reservationAPI'; +import { fetchMembers } from '@_api/member/memberAPI'; +import { fetchThemes } from '@_api/theme/themeAPI'; +import { fetchTimes } from '@_api/time/timeAPI'; +import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes'; +import type { MemberRetrieveResponse } from '@_api/member/memberTypes'; +import type { ThemeRetrieveResponse } from '@_api/theme/themeTypes'; +import type { TimeRetrieveResponse } from '@_api/time/timeTypes'; +import { isLoginRequiredError } from '@_api/apiClient'; + +const AdminReservationPage: React.FC = () => { + const [reservations, setReservations] = useState([]); + const [members, setMembers] = useState([]); + const [themes, setThemes] = useState([]); + const [times, setTimes] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const [newReservation, setNewReservation] = useState({ memberId: '', themeId: '', date: '', timeId: '' }); + const [filter, setFilter] = useState({ memberId: '', themeId: '', dateFrom: '', dateTo: '' }); + const navigate = useNavigate(); + const location = useLocation(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + _fetchReservations(); + fetchMembers().then(res => setMembers(res.members)).catch(handleError); + fetchThemes().then(res => setThemes(res.themes)).catch(handleError); + fetchTimes().then(res => setTimes(res.times)).catch(handleError); + }, []); + + const _fetchReservations = () => { + fetchReservations() + .then(res => setReservations(res.reservations)) + .catch(handleError); + } + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilter({ ...filter, [e.target.name]: e.target.value }); + }; + + const applyFilter = (e: React.FormEvent) => { + e.preventDefault(); + const params = { + memberId: filter.memberId ? Number(filter.memberId) : undefined, + themeId: filter.themeId ? Number(filter.themeId) : undefined, + dateFrom: filter.dateFrom, + dateTo: filter.dateTo, + }; + searchReservations(params) + .then(res => setReservations(res.reservations)) + .catch(handleError); + }; + + const handleAddClick = () => setIsEditing(true); + const handleCancelClick = () => setIsEditing(false); + + const handleSaveClick = async () => { + if (!newReservation.memberId || !newReservation.themeId || !newReservation.date || !newReservation.timeId) { + alert('모든 필드를 입력해주세요.'); + return; + } + const request = { + memberId: Number(newReservation.memberId), + themeId: Number(newReservation.themeId), + date: newReservation.date, + timeId: Number(newReservation.timeId), + }; + await createReservationByAdmin(request) + .then(() => { + alert('예약을 추가했어요. 결제는 별도로 진행해주세요.'); + _fetchReservations(); + handleCancelClick(); + }) + .catch(handleError); + }; + + const deleteReservation = async(id: number) => { + if (!window.confirm('정말 삭제하시겠어요?')) { + return; + } + await cancelReservationByAdmin(id) + .then(() => { + setReservations(reservations.filter(r => r.id !== id)) + alert('예약을 삭제했어요.'); + }).catch(handleError); + }; + + return ( +
+

방탈출 예약 페이지

+
+
+
+ +
+ + + + + + + + + + + + + + {reservations.map(r => ( + + + + + + + + + + ))} + {isEditing && ( + + + + + + + + + + )} + +
예약번호예약자테마날짜시간결제 완료 여부
{r.id}{r.member.name}{r.theme.name}{r.date}{r.time.startAt}{r.status === 'CONFIRMED' ? '결제 완료' : '결제 대기'}
+ + + + setNewReservation({ ...newReservation, date: e.target.value })} /> + + + + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default AdminReservationPage; \ No newline at end of file diff --git a/frontend/src/pages/admin/ThemePage.tsx b/frontend/src/pages/admin/ThemePage.tsx new file mode 100644 index 00000000..2d8dc313 --- /dev/null +++ b/frontend/src/pages/admin/ThemePage.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { createTheme, fetchThemes, delTheme } from '@_api/theme/themeAPI'; +import { isLoginRequiredError } from '@_api/apiClient'; + +const AdminThemePage: React.FC = () => { + const [themes, setThemes] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const [newTheme, setNewTheme] = useState({ name: '', description: '', thumbnail: '' }); + const navigate = useNavigate(); + const location = useLocation(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + const fetchData = async () => { + await fetchThemes() + .then(response => setThemes(response.themes)) + .catch(handleError); + }; + fetchData(); + }, []); + + const handleAddClick = () => { + setIsEditing(true); + }; + + const handleCancelClick = () => { + setIsEditing(false); + setNewTheme({ name: '', description: '', thumbnail: '' }); + }; + + const handleSaveClick = async () => { + await createTheme(newTheme) + .then((response) => { + setThemes([...themes, response]); + alert('테마를 추가했어요.'); + handleCancelClick(); + }) + .catch(handleError); + } + + const deleteTheme = async (id: number) => { + if (!window.confirm('정말 삭제하시겠어요?')) { + return; + } + await delTheme(id) + .then(() => { + setThemes(themes.filter(theme => theme.id !== id)); + alert('테마를 삭제했어요.'); + }) + .catch(handleError); + }; + + return ( +
+

테마 관리 페이지

+
+ +
+
+ + + + + + + + + + + + {themes.map(theme => ( + + + + + + + + ))} + {isEditing && ( + + + + + + + + )} + +
순서제목설명썸네일 URL
{theme.id}{theme.name}{theme.description}{theme.thumbnail} + +
setNewTheme({ ...newTheme, name: e.target.value })} /> setNewTheme({ ...newTheme, description: e.target.value })} /> setNewTheme({ ...newTheme, thumbnail: e.target.value })} /> + + +
+
+ ); +}; + +export default AdminThemePage; \ No newline at end of file diff --git a/frontend/src/pages/admin/TimePage.tsx b/frontend/src/pages/admin/TimePage.tsx new file mode 100644 index 00000000..3912dc93 --- /dev/null +++ b/frontend/src/pages/admin/TimePage.tsx @@ -0,0 +1,119 @@ +import { createTime, delTime, fetchTimes } from '@_api/time/timeAPI'; +import type { TimeCreateRequest } from '@_api/time/timeTypes'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { isLoginRequiredError } from '@_api/apiClient'; + +const AdminTimePage: React.FC = () => { + const [times, setTimes] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const [newTime, setNewTime] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + const fetchData = async () => { + await fetchTimes() + .then(response => setTimes(response.times)) + .catch(handleError); + } + fetchData(); + }, []); + + const handleAddClick = () => { + setIsEditing(true); + }; + + const handleCancelClick = () => { + setIsEditing(false); + setNewTime(''); + }; + + const handleSaveClick = async () => { + if (!newTime) { + alert('시간을 입력해주세요.'); + return; + } + if (!/^\d{2}:\d{2}$/.test(newTime)) { + alert('시간 형식이 올바르지 않습니다. HH:MM 형식으로 입력해주세요.'); + return; + } + const request: TimeCreateRequest = { + startAt: newTime + }; + + await createTime(request) + .then((response) => { + setTimes([...times, response]); + alert('시간을 추가했어요.'); + handleCancelClick(); + }) + .catch(handleError); + }; + + const deleteTime = async (id: number) => { + if (!window.confirm('정말 삭제하시겠어요?')) { + return; + } + + await delTime(id) + .then(() => { + setTimes(times.filter(time => time.id !== id)); + alert('시간을 삭제했어요.'); + }) + .catch(handleError); + }; + + return ( +
+

시간 관리 페이지

+
+ +
+
+ + + + + + + + + + {times.map(time => ( + + + + + + ))} + {isEditing && ( + + + + + + )} + +
순서시간
{time.id}{time.startAt} + +
setNewTime(e.target.value)} /> + + +
+
+ ); +}; + +export default AdminTimePage; diff --git a/frontend/src/pages/admin/WaitingPage.tsx b/frontend/src/pages/admin/WaitingPage.tsx new file mode 100644 index 00000000..3a784377 --- /dev/null +++ b/frontend/src/pages/admin/WaitingPage.tsx @@ -0,0 +1,85 @@ +import { confirmWaiting, fetchWaitingReservations, rejectWaiting } from '@_api/reservation/reservationAPI'; +import type { ReservationRetrieveResponse } from '@_api/reservation/reservationTypes'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { isLoginRequiredError } from '@_api/apiClient'; + +const AdminWaitingPage: React.FC = () => { + const [waitings, setWaitings] = useState([]); + const navigate = useNavigate(); + const location = useLocation(); + + const handleError = (err: any) => { + if (isLoginRequiredError(err)) { + alert('로그인이 필요해요.'); + navigate('/login', { state: { from: location } }); + } else { + const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.'; + alert(message); + console.error(err); + } + }; + + useEffect(() => { + const fetchData = async () => { + await fetchWaitingReservations() + .then(res => setWaitings(res.reservations)) + .catch(handleError); + } + fetchData(); + }, []); + + const approveWaiting = async (id: number) => { + await confirmWaiting(id) + .then(() => { + alert('대기 중인 예약을 승인했어요. 결제는 별도로 진행해주세요.'); + setWaitings(waitings.filter(w => w.id !== id)); + }) + .catch(handleError); + }; + + const denyWaiting = async (id: number) => { + await rejectWaiting(id) + .then(() => { + alert('대기 중인 예약을 거절했어요.'); + setWaitings(waitings.filter(w => w.id !== id)); + }) + .catch(handleError); + }; + + return ( +
+

예약 대기 관리 페이지

+
+ + + + + + + + + + + + + {waitings.map(w => ( + + + + + + + + + ))} + +
예약대기 번호예약자테마날짜시간
{w.id}{w.member.name}{w.theme.name}{w.date}{w.time.startAt} + + +
+
+ ); +}; + +export default AdminWaitingPage; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 00000000..7a47b541 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@_api/*": ["src/api/*"], + "@_assets/*": ["src/assets/*"], + "@_components/*": ["src/components/*"], + "@_css/*": ["src/css/*"], + "@_hooks/*": ["src/hooks/*"], + "@_pages/*": ["src/pages/*"], + "@_types/*": ["/src/types/*"], + } + }, + "include": ["src"], +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..84c8a23a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 00000000..f85a3990 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 00000000..1c97543f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tsconfigPaths from 'vite-tsconfig-paths' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + tsconfigPaths(), + ], +}) diff --git a/src/main/kotlin/roomescape/auth/docs/AuthAPI.kt b/src/main/kotlin/roomescape/auth/docs/AuthAPI.kt index a6e8fbcc..8b498ea2 100644 --- a/src/main/kotlin/roomescape/auth/docs/AuthAPI.kt +++ b/src/main/kotlin/roomescape/auth/docs/AuthAPI.kt @@ -10,7 +10,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RequestBody import roomescape.auth.web.LoginCheckResponse import roomescape.auth.web.LoginRequest -import roomescape.auth.web.support.LoginRequired +import roomescape.auth.web.LoginResponse import roomescape.auth.web.support.MemberId import roomescape.common.dto.response.CommonApiResponse @@ -18,28 +18,27 @@ import roomescape.common.dto.response.CommonApiResponse interface AuthAPI { @Operation(summary = "로그인") @ApiResponses( - ApiResponse(responseCode = "200", description = "로그인 성공시 쿠키에 토큰 정보를 저장합니다."), + ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."), ) fun login( - @Valid @RequestBody loginRequest: LoginRequest - ): ResponseEntity> + @Valid @RequestBody loginRequest: LoginRequest + ): ResponseEntity> @Operation(summary = "로그인 상태 확인") @ApiResponses( - ApiResponse( - responseCode = "200", - description = "로그인 상태이며, 로그인된 회원의 이름을 반환합니다.", - useReturnTypeSchema = true - ), + ApiResponse( + responseCode = "200", + description = "로그인 상태이며, 로그인된 회원의 이름 / 권한을 반환합니다.", + useReturnTypeSchema = true + ), ) fun checkLogin( - @MemberId @Parameter(hidden = true) memberId: Long + @MemberId @Parameter(hidden = true) memberId: Long ): ResponseEntity> - @LoginRequired @Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"]) @ApiResponses( - ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."), + ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."), ) - fun logout(): ResponseEntity> + fun logout(@MemberId memberId: Long): ResponseEntity> } diff --git a/src/main/kotlin/roomescape/auth/service/AuthService.kt b/src/main/kotlin/roomescape/auth/service/AuthService.kt index 2c07146b..39f9e4df 100644 --- a/src/main/kotlin/roomescape/auth/service/AuthService.kt +++ b/src/main/kotlin/roomescape/auth/service/AuthService.kt @@ -1,5 +1,7 @@ package roomescape.auth.service +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Service import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException @@ -10,10 +12,12 @@ import roomescape.auth.web.LoginResponse import roomescape.member.business.MemberService import roomescape.member.infrastructure.persistence.MemberEntity +private val log: KLogger = KotlinLogging.logger {} + @Service class AuthService( - private val memberService: MemberService, - private val jwtHandler: JwtHandler + private val memberService: MemberService, + private val jwtHandler: JwtHandler ) { fun login(request: LoginRequest): LoginResponse { val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.LOGIN_FAILED) { @@ -30,12 +34,12 @@ class AuthService( memberService.findById(memberId) } - return LoginCheckResponse(member.name) + return LoginCheckResponse(member.name, member.role.name) } private fun fetchMemberOrThrow( - errorCode: AuthErrorCode, - block: () -> MemberEntity + errorCode: AuthErrorCode, + block: () -> MemberEntity ): MemberEntity { try { return block() @@ -43,4 +47,10 @@ class AuthService( throw AuthException(errorCode) } } + + fun logout(memberId: Long?) { + if (memberId != null) { + log.info { "requested logout for $memberId" } + } + } } diff --git a/src/main/kotlin/roomescape/auth/web/AuthController.kt b/src/main/kotlin/roomescape/auth/web/AuthController.kt index 65746af7..6e28741a 100644 --- a/src/main/kotlin/roomescape/auth/web/AuthController.kt +++ b/src/main/kotlin/roomescape/auth/web/AuthController.kt @@ -2,7 +2,6 @@ package roomescape.auth.web import io.swagger.v3.oas.annotations.Parameter import jakarta.validation.Valid -import org.springframework.http.HttpHeaders import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -11,29 +10,25 @@ import org.springframework.web.bind.annotation.RestController import roomescape.auth.docs.AuthAPI import roomescape.auth.service.AuthService import roomescape.auth.web.support.MemberId -import roomescape.auth.web.support.expiredAccessTokenCookie -import roomescape.auth.web.support.toResponseCookie import roomescape.common.dto.response.CommonApiResponse @RestController class AuthController( - private val authService: AuthService + private val authService: AuthService ) : AuthAPI { @PostMapping("/login") override fun login( - @Valid @RequestBody loginRequest: LoginRequest, - ): ResponseEntity> { + @Valid @RequestBody loginRequest: LoginRequest, + ): ResponseEntity> { val response: LoginResponse = authService.login(loginRequest) - return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, response.toResponseCookie()) - .body(CommonApiResponse()) + return ResponseEntity.ok(CommonApiResponse(response)) } @GetMapping("/login/check") override fun checkLogin( - @MemberId @Parameter(hidden = true) memberId: Long + @MemberId @Parameter(hidden = true) memberId: Long ): ResponseEntity> { val response: LoginCheckResponse = authService.checkLogin(memberId) @@ -41,7 +36,9 @@ class AuthController( } @PostMapping("/logout") - override fun logout(): ResponseEntity> = ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, expiredAccessTokenCookie()) - .body(CommonApiResponse()) + override fun logout(@MemberId memberId: Long): ResponseEntity> { + authService.logout(memberId) + + return ResponseEntity.noContent().build() + } } diff --git a/src/main/kotlin/roomescape/auth/web/AuthDTO.kt b/src/main/kotlin/roomescape/auth/web/AuthDTO.kt index 6910fe44..f413c439 100644 --- a/src/main/kotlin/roomescape/auth/web/AuthDTO.kt +++ b/src/main/kotlin/roomescape/auth/web/AuthDTO.kt @@ -5,18 +5,20 @@ import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank data class LoginResponse( - val accessToken: String + val accessToken: String ) data class LoginCheckResponse( - @Schema(description = "로그인된 회원의 이름") - val name: String + @Schema(description = "로그인된 회원의 이름") + val name: String, + @Schema(description = "회원(MEMBER) / 관리자(ADMIN)") + val role: String, ) data class LoginRequest( - @Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com") - val email: String, + @Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com") + val email: String, - @NotBlank(message = "비밀번호는 공백일 수 없습니다.") - val password: String + @NotBlank(message = "비밀번호는 공백일 수 없습니다.") + val password: String ) diff --git a/src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt b/src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt index c7b92706..c8b90bcc 100644 --- a/src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt +++ b/src/main/kotlin/roomescape/auth/web/support/AuthInterceptor.kt @@ -13,8 +13,8 @@ import roomescape.member.infrastructure.persistence.MemberEntity @Component class AuthInterceptor( - private val memberService: MemberService, - private val jwtHandler: JwtHandler + private val memberService: MemberService, + private val jwtHandler: JwtHandler ) : HandlerInterceptor { override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { if (handler !is HandlerMethod) { @@ -28,24 +28,22 @@ class AuthInterceptor( return true } - val member: MemberEntity = findMember(request, response) + val member: MemberEntity = findMember(request) if (admin != null && !member.isAdmin()) { - response.sendRedirect("/login") throw AuthException(AuthErrorCode.ACCESS_DENIED) } return true } - private fun findMember(request: HttpServletRequest, response: HttpServletResponse): MemberEntity { + private fun findMember(request: HttpServletRequest): MemberEntity { try { - val token: String? = request.accessTokenCookie().value + val token: String? = request.accessToken() val memberId: Long = jwtHandler.getMemberIdFromToken(token) return memberService.findById(memberId) } catch (e: Exception) { - response.sendRedirect("/login") throw e } } diff --git a/src/main/kotlin/roomescape/auth/web/support/CookieUtils.kt b/src/main/kotlin/roomescape/auth/web/support/CookieUtils.kt index af1e606d..4b0b2b71 100644 --- a/src/main/kotlin/roomescape/auth/web/support/CookieUtils.kt +++ b/src/main/kotlin/roomescape/auth/web/support/CookieUtils.kt @@ -1,26 +1,9 @@ package roomescape.auth.web.support -import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletRequest -import org.springframework.http.ResponseCookie -import roomescape.auth.web.LoginResponse -const val ACCESS_TOKEN_COOKIE_NAME = "accessToken" +const val AUTHORIZATION_HEADER_NAME = "Authorization" +const val AUTHORIZATION_HEADER_PREFIX = "Bearer " -fun HttpServletRequest.accessTokenCookie(): Cookie = this.cookies - ?.firstOrNull { it.name == ACCESS_TOKEN_COOKIE_NAME } - ?: Cookie(ACCESS_TOKEN_COOKIE_NAME, "") - -fun LoginResponse.toResponseCookie(): String = accessTokenCookie(this.accessToken, 1800) - .toString() - -fun expiredAccessTokenCookie(): String = accessTokenCookie("", 0) - .toString() - -private fun accessTokenCookie(token: String, maxAgeSecond: Long): ResponseCookie = - ResponseCookie.from(ACCESS_TOKEN_COOKIE_NAME, token) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(maxAgeSecond) - .build() +fun HttpServletRequest.accessToken(): String? = this.getHeader(AUTHORIZATION_HEADER_NAME) + ?.removePrefix(AUTHORIZATION_HEADER_PREFIX) diff --git a/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt b/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt index 57fec5ef..49cbdaca 100644 --- a/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt +++ b/src/main/kotlin/roomescape/auth/web/support/MemberIdResolver.kt @@ -11,7 +11,7 @@ import roomescape.auth.infrastructure.jwt.JwtHandler @Component class MemberIdResolver( - private val jwtHandler: JwtHandler + private val jwtHandler: JwtHandler ) : HandlerMethodArgumentResolver { override fun supportsParameter(parameter: MethodParameter): Boolean { @@ -19,13 +19,13 @@ class MemberIdResolver( } override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? ): Any { val request: HttpServletRequest = webRequest.nativeRequest as HttpServletRequest - val token: String = request.accessTokenCookie().value + val token: String? = request.accessToken() return jwtHandler.getMemberIdFromToken(token) } diff --git a/src/main/kotlin/roomescape/common/config/CorsConfig.kt b/src/main/kotlin/roomescape/common/config/CorsConfig.kt new file mode 100644 index 00000000..f66ed3fc --- /dev/null +++ b/src/main/kotlin/roomescape/common/config/CorsConfig.kt @@ -0,0 +1,16 @@ +package roomescape.common.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class CorsConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type") + .maxAge(3600) // 1 hour + } +} diff --git a/src/main/kotlin/roomescape/member/business/MemberService.kt b/src/main/kotlin/roomescape/member/business/MemberService.kt index 8ff4072e..3f5e112d 100644 --- a/src/main/kotlin/roomescape/member/business/MemberService.kt +++ b/src/main/kotlin/roomescape/member/business/MemberService.kt @@ -7,16 +7,16 @@ import roomescape.member.exception.MemberErrorCode import roomescape.member.exception.MemberException import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository -import roomescape.member.web.MemberRetrieveListResponse -import roomescape.member.web.toRetrieveResponse +import roomescape.member.infrastructure.persistence.Role +import roomescape.member.web.* @Service @Transactional(readOnly = true) class MemberService( - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository ) { fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse( - members = memberRepository.findAll().map { it.toRetrieveResponse() } + members = memberRepository.findAll().map { it.toRetrieveResponse() } ) fun findById(memberId: Long): MemberEntity = fetchOrThrow { @@ -27,6 +27,21 @@ class MemberService( memberRepository.findByEmailAndPassword(email, password) } + @Transactional + fun create(request: SignupRequest): SignupResponse { + memberRepository.findByEmail(request.email)?.let { + throw MemberException(MemberErrorCode.DUPLICATE_EMAIL) + } + + val member = MemberEntity( + name = request.name, + email = request.email, + password = request.password, + role = Role.MEMBER + ) + return memberRepository.save(member).toSignupResponse() + } + private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity { return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND) } diff --git a/src/main/kotlin/roomescape/member/docs/MemberAPI.kt b/src/main/kotlin/roomescape/member/docs/MemberAPI.kt index 9428cde2..36ebfa7a 100644 --- a/src/main/kotlin/roomescape/member/docs/MemberAPI.kt +++ b/src/main/kotlin/roomescape/member/docs/MemberAPI.kt @@ -5,20 +5,33 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RequestBody import roomescape.auth.web.support.Admin import roomescape.common.dto.response.CommonApiResponse import roomescape.member.web.MemberRetrieveListResponse +import roomescape.member.web.SignupRequest +import roomescape.member.web.SignupResponse @Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") interface MemberAPI { @Admin @Operation(summary = "모든 회원 조회", tags = ["관리자 로그인이 필요한 API"]) @ApiResponses( - ApiResponse( - responseCode = "200", - description = "성공", - useReturnTypeSchema = true - ) + ApiResponse( + responseCode = "200", + description = "성공", + useReturnTypeSchema = true + ) ) fun findMembers(): ResponseEntity> + + @Operation(summary = "회원 가입") + @ApiResponses( + ApiResponse( + responseCode = "201", + description = "성공", + useReturnTypeSchema = true + ) + ) + fun signup(@RequestBody request: SignupRequest): ResponseEntity> } diff --git a/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt b/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt index 3b365311..daf8d9d0 100644 --- a/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt +++ b/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt @@ -8,5 +8,6 @@ enum class MemberErrorCode( override val errorCode: String, override val message: String ) : ErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요.") + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."), + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.") } diff --git a/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt b/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt index 667d9df8..13e2ac6d 100644 --- a/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt +++ b/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface MemberRepository : JpaRepository { fun findByEmailAndPassword(email: String, password: String): MemberEntity? + + fun findByEmail(email: String): MemberEntity? } diff --git a/src/main/kotlin/roomescape/member/web/MemberController.kt b/src/main/kotlin/roomescape/member/web/MemberController.kt index b86d341e..65594ec7 100644 --- a/src/main/kotlin/roomescape/member/web/MemberController.kt +++ b/src/main/kotlin/roomescape/member/web/MemberController.kt @@ -2,16 +2,26 @@ package roomescape.member.web import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController import roomescape.common.dto.response.CommonApiResponse import roomescape.member.business.MemberService import roomescape.member.docs.MemberAPI +import java.net.URI @RestController class MemberController( - private val memberService: MemberService + private val memberService: MemberService ) : MemberAPI { + @PostMapping("/members") + override fun signup(@RequestBody request: SignupRequest): ResponseEntity> { + val response: SignupResponse = memberService.create(request) + return ResponseEntity.created(URI.create("/members/${response.id}")) + .body(CommonApiResponse(response)) + } + @GetMapping("/members") override fun findMembers(): ResponseEntity> { val response: MemberRetrieveListResponse = memberService.findMembers() diff --git a/src/main/kotlin/roomescape/member/web/MemberDTO.kt b/src/main/kotlin/roomescape/member/web/MemberDTO.kt index 00c00c8f..07e76551 100644 --- a/src/main/kotlin/roomescape/member/web/MemberDTO.kt +++ b/src/main/kotlin/roomescape/member/web/MemberDTO.kt @@ -4,18 +4,34 @@ import io.swagger.v3.oas.annotations.media.Schema import roomescape.member.infrastructure.persistence.MemberEntity fun MemberEntity.toRetrieveResponse(): MemberRetrieveResponse = MemberRetrieveResponse( - id = id!!, - name = name + id = id!!, + name = name ) data class MemberRetrieveResponse( - @Schema(description = "회원 식별자") - val id: Long, + @Schema(description = "회원 식별자") + val id: Long, - @Schema(description = "회원 이름") - val name: String + @Schema(description = "회원 이름") + val name: String ) data class MemberRetrieveListResponse( - val members: List + val members: List +) + +data class SignupRequest( + val email: String, + val password: String, + val name: String +) + +data class SignupResponse( + val id: Long, + val name: String, +) + +fun MemberEntity.toSignupResponse(): SignupResponse = SignupResponse( + id = this.id!!, + name = this.name ) diff --git a/src/main/kotlin/roomescape/view/PageController.kt b/src/main/kotlin/roomescape/view/PageController.kt deleted file mode 100644 index 10425b88..00000000 --- a/src/main/kotlin/roomescape/view/PageController.kt +++ /dev/null @@ -1,48 +0,0 @@ -package roomescape.view - -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import roomescape.auth.web.support.Admin -import roomescape.auth.web.support.LoginRequired - -@Controller -class AuthPageController { - - @GetMapping("/login") - fun showLoginPage(): String = "login" -} - -@Controller -@RequestMapping("/admin") -class AdminPageController { - - @Admin - @GetMapping - fun showIndexPage() = "admin/index" - - @Admin - @GetMapping("/{page}") - fun showAdminSubPage(@PathVariable page: String) = when (page) { - "reservation" -> "admin/reservation-new" - "time" -> "admin/time" - "theme" -> "admin/theme" - "waiting" -> "admin/waiting" - else -> "admin/index" - } -} - -@Controller -class ClientPageController { - @GetMapping("/") - fun showPopularThemePage(): String = "index" - - @LoginRequired - @GetMapping("/reservation") - fun showReservationPage(): String = "reservation" - - @LoginRequired - @GetMapping("/reservation-mine") - fun showReservationMinePage(): String = "reservation-mine" -} diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico deleted file mode 100644 index 79a18a8f..00000000 Binary files a/src/main/resources/static/favicon.ico and /dev/null differ diff --git a/src/main/resources/static/js/ranking.js b/src/main/resources/static/js/ranking.js deleted file mode 100644 index 33be64bc..00000000 --- a/src/main/resources/static/js/ranking.js +++ /dev/null @@ -1,45 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - requestRead(`/themes/most-reserved-last-week?count=10`) // 인기 테마 목록 조회 API endpoint - .then(render) - .catch(error => console.error('Error fetching times:', error)); -}); - -function formatDate(dateString) { - let date = new Date(dateString); - let year = date.getFullYear(); - let month = (date.getMonth() + 1).toString().padStart(2, '0'); // '04' - let day = date.getDate().toString().padStart(2, '0'); // '28' - - return `${year}-${month}-${day}`; // '2024-04-28' -} - -function render(data) { - const container = document.getElementById('theme-ranking'); - data.data.themes.forEach(theme => { - const name = theme.name; - const thumbnail = theme.thumbnail; - const description = theme.description; - - const htmlContent = ` - ${name} -
-
${name}
- ${description} -
- `; - - const div = document.createElement('li'); - div.className = 'media my-4'; - div.innerHTML = htmlContent; - - container.appendChild(div); - }) -} - -function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js deleted file mode 100644 index 80a4d903..00000000 --- a/src/main/resources/static/js/reservation-mine.js +++ /dev/null @@ -1,57 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - fetch('/reservations-mine') // 내 예약 목록 조회 API 호출 - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }) - .then(render) - .catch(error => console.error('Error fetching reservations:', error)); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - data.data.reservations.forEach(item => { - const row = tableBody.insertRow(); - - const theme = item.themeName; - const date = item.date; - const time = item.time; - const status = item.status.includes('CONFIRMED') ? (item.status === 'CONFIRMED' ? '예약' : '예약 - 결제 필요') : item.rank + '번째 예약 대기'; - - row.insertCell(0).textContent = theme; - row.insertCell(1).textContent = date; - row.insertCell(2).textContent = time; - row.insertCell(3).textContent = status; - - if (status.includes('대기')) { // 예약 대기 상태일 때 예약 대기 취소 버튼 추가하는 코드, 상태 값은 변경 가능 - const cancelCell = row.insertCell(4); - const cancelButton = document.createElement('button'); - cancelButton.textContent = '취소'; - cancelButton.className = 'btn btn-danger'; - cancelButton.onclick = function () { - requestDeleteWaiting(item.id).then(() => window.location.reload()); - }; - cancelCell.appendChild(cancelButton); - } else { // 예약 완료 상태일 때 - /* - TODO: [미션4 - 2단계] 내 예약 목록 조회 시, - 예약 완료 상태일 때 결제 정보를 함께 보여주기 - 결제 정보 필드명은 자신의 response 에 맞게 변경하기 - */ - row.insertCell(4).textContent = ''; - row.insertCell(5).textContent = item.paymentKey; - row.insertCell(6).textContent = item.amount; - } - }); -} - -function requestDeleteWaiting(id) { - const endpoint = '/reservations/waiting/' + id; - return fetch(endpoint, { - method: 'DELETE' - }).then(response => { - if (response.status === 204) return; - throw new Error('Delete failed'); - }); -} diff --git a/src/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js deleted file mode 100644 index 869d1031..00000000 --- a/src/main/resources/static/js/reservation-new.js +++ /dev/null @@ -1,194 +0,0 @@ -let isEditing = false; -const RESERVATION_API_ENDPOINT = '/reservations'; -const TIME_API_ENDPOINT = '/times'; -const THEME_API_ENDPOINT = '/themes'; -const timesOptions = []; -const themesOptions = []; - -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('add-button').addEventListener('click', addInputRow); - - requestRead(RESERVATION_API_ENDPOINT) - .then(render) - .catch(error => console.error('Error fetching reservations:', error)); - - fetchTimes(); - fetchThemes(); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.reservations.forEach(item => { - const row = tableBody.insertRow(); - - row.insertCell(0).textContent = item.id; // 예약 id - row.insertCell(1).textContent = item.name; // 예약자명 - row.insertCell(2).textContent = item.theme.name; // 테마명 - row.insertCell(3).textContent = item.date; // 예약 날짜 - row.insertCell(4).textContent = item.time.startAt; // 시작 시간 - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); - }); -} - -function fetchTimes() { - requestRead(TIME_API_ENDPOINT) - .then(data => { - timesOptions.push(...data.data.times); - }) - .catch(error => console.error('Error fetching time:', error)); -} - -function fetchThemes() { - requestRead(THEME_API_ENDPOINT) - .then(data => { - themesOptions.push(...data.data.themes); - }) - .catch(error => console.error('Error fetching theme:', error)); -} - -function createSelect(options, defaultText, selectId, textProperty) { - const select = document.createElement('select'); - select.className = 'form-control'; - select.id = selectId; - - // 기본 옵션 추가 - const defaultOption = document.createElement('option'); - defaultOption.textContent = defaultText; - select.appendChild(defaultOption); - - // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 - options.forEach(optionData => { - const option = document.createElement('option'); - option.value = optionData.id; - option.textContent = optionData[textProperty]; // 동적 속성 접근 - select.appendChild(option); - }); - - return select; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function addInputRow() { - if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 - - const tableBody = document.getElementById('table-body'); - const row = tableBody.insertRow(); - isEditing = true; - - const nameInput = createInput('text'); - const dateInput = createInput('date'); - const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); - const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name'); - - const cellFieldsToCreate = ['', nameInput, themeDropdown, dateInput, timeDropdown]; - - cellFieldsToCreate.forEach((field, index) => { - const cell = row.insertCell(index); - if (typeof field === 'string') { - cell.textContent = field; - } else { - cell.appendChild(field); - } - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); - actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { - row.remove(); - isEditing = false; - })); -} - -function createInput(type) { - const input = document.createElement('input'); - input.type = type; - input.className = 'form-control'; - return input; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function saveRow(event) { - // 이벤트 전파를 막는다 - event.stopPropagation(); - - const row = event.target.parentNode.parentNode; - const nameInput = row.querySelector('input[type="text"]'); - const themeSelect = row.querySelector('#theme-select'); - const dateInput = row.querySelector('input[type="date"]'); - const timeSelect = row.querySelector('#time-select'); - - const reservation = { - name: nameInput.value, - themeId: themeSelect.value, - date: dateInput.value, - timeId: timeSelect.value - }; - - requestCreate(reservation) - .then(() => { - location.reload(); - }) - .catch(error => console.error('Error:', error)); - - isEditing = false; // isEditing 값을 false로 설정 -} - -function deleteRow(event) { - const row = event.target.closest('tr'); - const reservationId = row.cells[0].textContent; - - requestDelete(reservationId) - .then(() => row.remove()) - .catch(error => console.error('Error:', error)); -} - -function requestCreate(reservation) { - const requestOptions = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(reservation) - }; - - return fetch(RESERVATION_API_ENDPOINT, requestOptions) - .then(response => { - if (response.status === 201) return response.json(); - throw new Error('Create failed'); - }); -} - -function requestDelete(id) { - const requestOptions = { - method: 'DELETE', - }; - - return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) - .then(response => { - if (response.status !== 204) throw new Error('Delete failed'); - }); -} - -function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} diff --git a/src/main/resources/static/js/reservation-with-member.js b/src/main/resources/static/js/reservation-with-member.js deleted file mode 100644 index c5d804ec..00000000 --- a/src/main/resources/static/js/reservation-with-member.js +++ /dev/null @@ -1,250 +0,0 @@ -let isEditing = false; -const RESERVATION_API_ENDPOINT = '/reservations'; -const TIME_API_ENDPOINT = '/times'; -const THEME_API_ENDPOINT = '/themes'; -const MEMBER_API_ENDPOINT = '/members'; -const timesOptions = []; -const themesOptions = []; -const membersOptions = []; - -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('add-button').addEventListener('click', addInputRow); - document.getElementById('filter-form').addEventListener('submit', applyFilter); - - requestRead(RESERVATION_API_ENDPOINT) - .then(render) - .catch(error => console.error('Error fetching reservations:', error)); - - fetchTimes(); - fetchThemes(); - fetchMembers(); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.reservations.forEach(item => { - const row = tableBody.insertRow(); - const isPaid = item.status === 'CONFIRMED' ? '결제 완료' : '결제 대기'; - - row.insertCell(0).textContent = item.id; // 예약 id - row.insertCell(1).textContent = item.member.name; // 사용자 name - row.insertCell(2).textContent = item.theme.name; // 테마 name - row.insertCell(3).textContent = item.date; // date - row.insertCell(4).textContent = item.time.startAt; // 예약 시간 startAt - row.insertCell(5).textContent = isPaid; // 결제 - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); - }); -} - -function fetchTimes() { - requestRead(TIME_API_ENDPOINT) - .then(data => { - timesOptions.push(...data.data.times); - }) - .catch(error => console.error('Error fetching time:', error)); -} - -function fetchThemes() { - requestRead(THEME_API_ENDPOINT) - .then(data => { - themesOptions.push(...data.data.themes); - populateSelect('theme', themesOptions, 'name'); - }) - .catch(error => console.error('Error fetching theme:', error)); -} - -function fetchMembers() { - requestRead(MEMBER_API_ENDPOINT) - .then(data => { - membersOptions.push(...data.data.members); - populateSelect('member', membersOptions, 'name'); - }) - .catch(error => console.error('Error fetching member:', error)); -} - -function populateSelect(selectId, options, textProperty) { - const select = document.getElementById(selectId); - options.forEach(optionData => { - const option = document.createElement('option'); - option.value = optionData.id; - option.textContent = optionData[textProperty]; - select.appendChild(option); - }); -} - -function createSelect(options, defaultText, selectId, textProperty) { - const select = document.createElement('select'); - select.className = 'form-control'; - select.id = selectId; - - // 기본 옵션 추가 - const defaultOption = document.createElement('option'); - defaultOption.textContent = defaultText; - select.appendChild(defaultOption); - - // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 - options.forEach(optionData => { - const option = document.createElement('option'); - option.value = optionData.id; - option.textContent = optionData[textProperty]; // 동적 속성 접근 - select.appendChild(option); - }); - - return select; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function addInputRow() { - if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 - - const tableBody = document.getElementById('table-body'); - const row = tableBody.insertRow(); - isEditing = true; - - const dateInput = createInput('date'); - const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); - const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name'); - const memberDropdown = createSelect(membersOptions, "멤버 선택", 'member-select', 'name'); - - const cellFieldsToCreate = ['', memberDropdown, themeDropdown, dateInput, timeDropdown]; - - cellFieldsToCreate.forEach((field, index) => { - const cell = row.insertCell(index); - if (typeof field === 'string') { - cell.textContent = field; - } else { - cell.appendChild(field); - } - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); - actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { - row.remove(); - isEditing = false; - })); -} - -function createInput(type) { - const input = document.createElement('input'); - input.type = type; - input.className = 'form-control'; - return input; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function saveRow(event) { - // 이벤트 전파를 막는다 - event.stopPropagation(); - - const row = event.target.parentNode.parentNode; - const dateInput = row.querySelector('input[type="date"]'); - const memberSelect = row.querySelector('#member-select'); - const themeSelect = row.querySelector('#theme-select'); - const timeSelect = row.querySelector('#time-select'); - - const reservation = { - date: dateInput.value, - themeId: themeSelect.value, - timeId: timeSelect.value, - memberId: memberSelect.value, - }; - - requestCreate(reservation) - .then(() => { - location.reload(); - }) - .catch(error => console.error('Error:', error)); - - isEditing = false; // isEditing 값을 false로 설정 -} - -function deleteRow(event) { - const row = event.target.closest('tr'); - const reservationId = row.cells[0].textContent; - - requestDelete(reservationId) - .then(() => row.remove()) - .catch(error => console.error('Error:', error)); -} - -function applyFilter(event) { - event.preventDefault(); - - const themeId = document.getElementById('theme').value; - const memberId = document.getElementById('member').value; - const dateFrom = document.getElementById('date-from').value; - const dateTo = document.getElementById('date-to').value; - - const queryParams = { - themeId: themeId, - memberId: memberId, - dateFrom: dateFrom, - dateTo: dateTo - } - const searchParams = new URLSearchParams(queryParams); - const endpoint = '/reservations/search'; - - const url = `${endpoint}?${searchParams.toString()}`; - fetch(url, { // 예약 검색 API 호출 - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }).then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }).then(render) - .catch(error => console.error("Error fetching available times:", error)); -} - -function requestCreate(reservation) { - const requestOptions = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(reservation) - }; - - return fetch('/reservations/admin', requestOptions) - .then(response => { - if (response.status === 201) return response.json(); - throw new Error('Create failed'); - }); -} - -function requestDelete(id) { - const requestOptions = { - method: 'DELETE', - }; - - return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) - .then(response => { - if (response.status !== 204) throw new Error('Delete failed'); - }); -} - -function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} diff --git a/src/main/resources/static/js/reservation.js b/src/main/resources/static/js/reservation.js deleted file mode 100644 index a64d3dc5..00000000 --- a/src/main/resources/static/js/reservation.js +++ /dev/null @@ -1,179 +0,0 @@ -let isEditing = false; -const RESERVATION_API_ENDPOINT = '/reservations'; -const TIME_API_ENDPOINT = '/times'; -const timesOptions = []; - -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('add-button').addEventListener('click', addInputRow); - - requestRead(RESERVATION_API_ENDPOINT) - .then(render) - .catch(error => console.error('Error fetching reservations:', error)); - - fetchTimes(); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.reservations.forEach(item => { - const row = tableBody.insertRow(); - - row.insertCell(0).textContent = item.id; - row.insertCell(1).textContent = item.name; - row.insertCell(2).textContent = item.date; - row.insertCell(3).textContent = item.time.startAt; - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); - }); -} - -function fetchTimes() { - requestRead(TIME_API_ENDPOINT) - .then(data => { - timesOptions.push(...data.data.times); - }) - .catch(error => console.error('Error fetching time:', error)); -} - -function createSelect(options, defaultText, selectId, textProperty) { - const select = document.createElement('select'); - select.className = 'form-control'; - select.id = selectId; - - // 기본 옵션 추가 - const defaultOption = document.createElement('option'); - defaultOption.textContent = defaultText; - select.appendChild(defaultOption); - - // 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성 - options.forEach(optionData => { - const option = document.createElement('option'); - option.value = optionData.id; - option.textContent = optionData[textProperty]; // 동적 속성 접근 - select.appendChild(option); - }); - - return select; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function addInputRow() { - if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 - - const tableBody = document.getElementById('table-body'); - const row = tableBody.insertRow(); - isEditing = true; - - const nameInput = createInput('text'); - const dateInput = createInput('date'); - const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt'); - - const cellFieldsToCreate = ['', nameInput, dateInput, timeDropdown]; - - cellFieldsToCreate.forEach((field, index) => { - const cell = row.insertCell(index); - if (typeof field === 'string') { - cell.textContent = field; - } else { - cell.appendChild(field); - } - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); - actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { - row.remove(); - isEditing = false; - })); -} - -function createInput(type) { - const input = document.createElement('input'); - input.type = type; - input.className = 'form-control'; - return input; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function saveRow(event) { - // 이벤트 전파를 막는다 - event.stopPropagation(); - - const row = event.target.parentNode.parentNode; - const nameInput = row.querySelector('input[type="text"]'); - const dateInput = row.querySelector('input[type="date"]'); - const timeSelect = row.querySelector('select'); - - const reservation = { - name: nameInput.value, - date: dateInput.value, - timeId: timeSelect.value - }; - - requestCreate(reservation) - .then(() => { - location.reload(); - }) - .catch(error => console.error('Error:', error)); - - isEditing = false; // isEditing 값을 false로 설정 -} - -function deleteRow(event) { - const row = event.target.closest('tr'); - const reservationId = row.cells[0].textContent; - - requestDelete(reservationId) - .then(() => row.remove()) - .catch(error => console.error('Error:', error)); -} - -function requestCreate(reservation) { - const requestOptions = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(reservation) - }; - - return fetch(RESERVATION_API_ENDPOINT, requestOptions) - .then(response => { - if (response.status === 201) return response.json(); - throw new Error('Create failed'); - }); -} - -function requestDelete(id) { - const requestOptions = { - method: 'DELETE', - }; - - return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions) - .then(response => { - if (response.status !== 204) throw new Error('Delete failed'); - }); -} - -function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} diff --git a/src/main/resources/static/js/scripts.js b/src/main/resources/static/js/scripts.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js deleted file mode 100644 index fda35892..00000000 --- a/src/main/resources/static/js/theme.js +++ /dev/null @@ -1,136 +0,0 @@ -let isEditing = false; -const API_ENDPOINT = '/themes'; -const cellFields = ['id', 'name', 'description', 'thumbnail']; -const createCellFields = ['', createInput(), createInput(), createInput()]; - -function createBody(inputs) { - return { - name: inputs[0].value, - description: inputs[1].value, - thumbnail: inputs[2].value, - }; -} - -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('add-button').addEventListener('click', addRow); - requestRead() - .then(render) - .catch(error => console.error('Error fetching times:', error)); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.themes.forEach(item => { - const row = tableBody.insertRow(); - - cellFields.forEach((field, index) => { - row.insertCell(index).textContent = item[field]; - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); - }); -} - -function addRow() { - if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 - - const tableBody = document.getElementById('table-body'); - const row = tableBody.insertRow(); - isEditing = true; - - createAddField(row); -} - -function createAddField(row) { - createCellFields.forEach((field, index) => { - const cell = row.insertCell(index); - if (typeof field === 'string') { - cell.textContent = field; - } else { - cell.appendChild(field); - } - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); - actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { - row.remove(); - isEditing = false; - })); -} - -function createInput() { - const input = document.createElement('input'); - input.className = 'form-control'; - return input; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function saveRow(event) { - const row = event.target.parentNode.parentNode; - const inputs = row.querySelectorAll('input'); - const body = createBody(inputs); - - requestCreate(body) - .then(() => { - location.reload(); - }) - .catch(error => console.error('Error:', error)); - - isEditing = false; // isEditing 값을 false로 설정 -} - -function deleteRow(event) { - const row = event.target.closest('tr'); - const id = row.cells[0].textContent; - - requestDelete(id) - .then(() => row.remove()) - .catch(error => console.error('Error:', error)); -} - - -// request - -function requestCreate(data) { - const requestOptions = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(data) - }; - - return fetch(API_ENDPOINT, requestOptions) - .then(response => { - if (response.status === 201) return response.json(); - throw new Error('Create failed'); - }); -} - -function requestRead() { - return fetch(API_ENDPOINT) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} - -function requestDelete(id) { - const requestOptions = { - method: 'DELETE', - }; - - return fetch(`${API_ENDPOINT}/${id}`, requestOptions) - .then(response => { - if (response.status !== 204) throw new Error('Delete failed'); - }); -} diff --git a/src/main/resources/static/js/time.js b/src/main/resources/static/js/time.js deleted file mode 100644 index 98094df4..00000000 --- a/src/main/resources/static/js/time.js +++ /dev/null @@ -1,135 +0,0 @@ -let isEditing = false; -const API_ENDPOINT = '/times'; -const cellFields = ['id', 'startAt']; -const createCellFields = ['', createInput()]; - -function createBody(inputs) { - return { - startAt: inputs[0].value, - }; -} - -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('add-button').addEventListener('click', addRow); - requestRead() - .then(render) - .catch(error => console.error('Error fetching times:', error)); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.times.forEach(item => { - const row = tableBody.insertRow(); - - cellFields.forEach((field, index) => { - row.insertCell(index).textContent = item[field]; - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow)); - }); -} - -function addRow() { - if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음 - - const tableBody = document.getElementById('table-body'); - const row = tableBody.insertRow(); - isEditing = true; - - createAddField(row); -} - -function createAddField(row) { - createCellFields.forEach((field, index) => { - const cell = row.insertCell(index); - if (typeof field === 'string') { - cell.textContent = field; - } else { - cell.appendChild(field); - } - }); - - const actionCell = row.insertCell(row.cells.length); - actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow)); - actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => { - row.remove(); - isEditing = false; - })); -} - -function createInput() { - const input = document.createElement('input'); - input.type = 'time' - input.className = 'form-control'; - return input; -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} - -function saveRow(event) { - const row = event.target.parentNode.parentNode; - const inputs = row.querySelectorAll('input'); - const body = createBody(inputs); - - requestCreate(body) - .then(() => { - location.reload(); - }) - .catch(error => console.error('Error:', error)); - - isEditing = false; // isEditing 값을 false로 설정 -} - -function deleteRow(event) { - const row = event.target.closest('tr'); - const id = row.cells[0].textContent; - - requestDelete(id) - .then(() => row.remove()) - .catch(error => console.error('Error:', error)); -} - - -// request - -function requestCreate(data) { - const requestOptions = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(data) - }; - - return fetch(API_ENDPOINT, requestOptions) - .then(response => { - if (response.status === 201) return response.json(); - throw new Error('Create failed'); - }); -} - -function requestRead() { - return fetch(API_ENDPOINT) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} - -function requestDelete(id) { - const requestOptions = { - method: 'DELETE', - }; - - return fetch(`${API_ENDPOINT}/${id}`, requestOptions) - .then(response => { - if (response.status !== 204) throw new Error('Delete failed'); - }); -} diff --git a/src/main/resources/static/js/user-reservation.js b/src/main/resources/static/js/user-reservation.js deleted file mode 100644 index 500c016a..00000000 --- a/src/main/resources/static/js/user-reservation.js +++ /dev/null @@ -1,273 +0,0 @@ -const THEME_API_ENDPOINT = '/themes'; - -document.addEventListener('DOMContentLoaded', () => { - requestRead(THEME_API_ENDPOINT) - .then(renderTheme) - .catch(error => console.error('Error fetching times:', error)); - - flatpickr("#datepicker", { - inline: true, - onChange: function (selectedDates, dateStr, instance) { - if (dateStr === '') return; - checkDate(); - } - }); - - // ------ 결제위젯 초기화 ------ - // @docs https://docs.tosspayments.com/reference/widget-sdk#sdk-설치-및-초기화 - // @docs https://docs.tosspayments.com/reference/widget-sdk#renderpaymentmethods선택자-결제-금액-옵션 - const paymentAmount = 1000; - const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm"; - const paymentWidget = PaymentWidget(widgetClientKey, PaymentWidget.ANONYMOUS); - paymentWidget.renderPaymentMethods( - "#payment-method", - {value: paymentAmount}, - {variantKey: "DEFAULT"} - ); - - document.getElementById('theme-slots').addEventListener('click', event => { - if (event.target.classList.contains('theme-slot')) { - document.querySelectorAll('.theme-slot').forEach(slot => slot.classList.remove('active')); - event.target.classList.add('active'); - checkDateAndTheme(); - } - }); - - document.getElementById('time-slots').addEventListener('click', event => { - if (event.target.classList.contains('time-slot') && !event.target.classList.contains('disabled')) { - document.querySelectorAll('.time-slot').forEach(slot => slot.classList.remove('active')); - event.target.classList.add('active'); - checkDateAndThemeAndTime(); - } - }); - - document.getElementById('reserve-button').addEventListener('click', onReservationButtonClickWithPaymentWidget); - document.getElementById('wait-button').addEventListener('click', onWaitButtonClick); - - function onReservationButtonClickWithPaymentWidget(event) { - onReservationButtonClick(event, paymentWidget); - } -}); - -function renderTheme(themes) { - const themeSlots = document.getElementById('theme-slots'); - themeSlots.innerHTML = ''; - themes.data.themes.forEach(theme => { - const name = theme.name; - const themeId = theme.id; - themeSlots.appendChild(createSlot('theme', name, themeId)); - }); -} - -function createSlot(type, text, id, booked) { - const div = document.createElement('div'); - div.className = type + '-slot cursor-pointer bg-light border rounded p-3 mb-2'; - div.textContent = text; - div.setAttribute('data-' + type + '-id', id); - if (type === 'time') { - div.setAttribute('data-time-booked', booked); - } - return div; -} - -function checkDate() { - const selectedDate = document.getElementById("datepicker").value; - if (selectedDate) { - const themeSection = document.getElementById("theme-section"); - if (themeSection.classList.contains("disabled")) { - themeSection.classList.remove("disabled"); - } - const timeSlots = document.getElementById('time-slots'); - timeSlots.innerHTML = ''; - - requestRead(THEME_API_ENDPOINT) - .then(renderTheme) - .catch(error => console.error('Error fetching times:', error)); - } -} - -function checkDateAndTheme() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeElement = document.querySelector('.theme-slot.active'); - if (selectedDate && selectedThemeElement) { - const selectedThemeId = selectedThemeElement.getAttribute('data-theme-id'); - fetchAvailableTimes(selectedDate, selectedThemeId); - } -} - -function fetchAvailableTimes(date, themeId) { - - fetch(`/times/search?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - }).then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }).then(renderAvailableTimes) - .catch(error => console.error("Error fetching available times:", error)); -} - -function renderAvailableTimes(times) { - const timeSection = document.getElementById("time-section"); - if (timeSection.classList.contains("disabled")) { - timeSection.classList.remove("disabled"); - } - - const timeSlots = document.getElementById('time-slots'); - timeSlots.innerHTML = ''; - if (times.length === 0) { - timeSlots.innerHTML = '
선택할 수 있는 시간이 없습니다.
'; - return; - } - times.data.times.forEach(time => { - const startAt = time.startAt; - const timeId = time.id; - const isAvailable = time.isAvailable; - - const div = createSlot('time', startAt, timeId, isAvailable); // createSlot('time', 시작 시간, time id, 예약 여부) - timeSlots.appendChild(div); - }); -} - -function checkDateAndThemeAndTime() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeElement = document.querySelector('.theme-slot.active'); - const selectedTimeElement = document.querySelector('.time-slot.active'); - const reserveButton = document.getElementById("reserve-button"); - const waitButton = document.getElementById("wait-button"); - - if (selectedDate && selectedThemeElement && selectedTimeElement) { - if (selectedTimeElement.getAttribute('data-time-booked') === 'false') { - // 선택된 시간이 이미 예약된 경우 - reserveButton.classList.add("disabled"); - waitButton.classList.remove("disabled"); // 예약 대기 버튼 활성화 - } else { - // 선택된 시간이 예약 가능한 경우 - reserveButton.classList.remove("disabled"); - waitButton.classList.add("disabled"); // 예약 대기 버튼 활성화 - } - } else { - // 날짜, 테마, 시간 중 하나라도 선택되지 않은 경우 - reserveButton.classList.add("disabled"); - waitButton.classList.add("disabled"); - } -} - -function onReservationButtonClick(event, paymentWidget) { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); - const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); - - if (selectedDate && selectedThemeId && selectedTimeId) { - const reservationData = { - date: selectedDate, - themeId: selectedThemeId, - timeId: selectedTimeId, - }; - - const generateRandomString = () => - window.btoa(Math.random()).slice(0, 20); - - // TOSS 결제 위젯 Javascript SDK 연동 방식 중 'Promise로 처리하기'를 적용함 - // https://docs.tosspayments.com/reference/widget-sdk#promise%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0 - const orderIdPrefix = "WTEST"; - paymentWidget.requestPayment({ - orderId: orderIdPrefix + generateRandomString(), - orderName: "테스트 방탈출 예약 결제 1건", - amount: 1000, - }).then(function (data) { - console.debug(data); - fetchReservationPayment(data, reservationData); - }).catch(function (error) { - // TOSS 에러 처리: 에러 목록을 확인하세요 - // https://docs.tosspayments.com/reference/error-codes#failurl 로-전달되는-에러 - alert(error.code + " :" + error.message); - }); - - } else { - alert("Please select a date, theme, and time before making a reservation."); - } -} - -async function fetchReservationPayment(paymentData, reservationData) { - const reservationPaymentRequest = { - date: reservationData.date, - themeId: reservationData.themeId, - timeId: reservationData.timeId, - paymentKey: paymentData.paymentKey, - orderId: paymentData.orderId, - amount: paymentData.amount, - paymentType: paymentData.paymentType, - } - - const reservationURL = "/reservations"; - fetch(reservationURL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(reservationPaymentRequest), - }).then(response => { - if (!response.ok) { - return response.json().then(errorBody => { - console.error("예약 결제 실패 : " + JSON.stringify(errorBody)); - window.alert("예약 결제 실패 메시지: " + errorBody.message); - }); - } else { - response.json().then(successBody => { - alert("예약이 완료되었습니다."); - console.log("예약 결제 성공 : " + JSON.stringify(successBody)); - window.location.href = "/"; - }); - } - }).catch(error => { - console.error(error.message); - }); -} - -function onWaitButtonClick() { - const selectedDate = document.getElementById("datepicker").value; - const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id'); - const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id'); - - if (selectedDate && selectedThemeId && selectedTimeId) { - const reservationData = { - date: selectedDate, - timeId: selectedTimeId, - themeId: selectedThemeId, - }; - - fetch('/reservations/waiting', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(reservationData) - }) - .then(response => { - if (!response.ok) throw new Error('Reservation waiting failed'); - return response.json(); - }) - .then(data => { - alert('Reservation waiting successful!'); - window.location.href = "/"; - }) - .catch(error => { - alert("An error occurred while making the reservation waiting."); - console.error(error); - }); - } else { - alert("Please select a date, theme, and time before making a reservation waiting."); - } -} - - -function requestRead(endpoint) { - return fetch(endpoint) - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }); -} diff --git a/src/main/resources/static/js/user-scripts.js b/src/main/resources/static/js/user-scripts.js deleted file mode 100644 index 2a1e253a..00000000 --- a/src/main/resources/static/js/user-scripts.js +++ /dev/null @@ -1,152 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - updateUIBasedOnLogin(); -}); - -document.getElementById('logout-btn').addEventListener('click', function (event) { - event.preventDefault(); - fetch('/logout', { - method: 'POST', // 또는 서버 설정에 따라 GET 일 수도 있음 - credentials: 'include' // 쿠키를 포함시키기 위해 필요 - }) - .then(response => { - if (response.ok) { - // 로그아웃 성공, 페이지 새로고침 또는 리다이렉트 - window.location.reload(); - } else { - // 로그아웃 실패 처리 - console.error('Logout failed'); - } - }) - .catch(error => { - console.error('Error:', error); - }); -}); - -function updateUIBasedOnLogin() { - fetch('/login/check') // 로그인 상태 확인 API 호출 - .then(response => { - if (!response.ok) { // 요청이 실패하거나 로그인 상태가 아닌 경우 - throw new Error('Not logged in or other error'); - } - return response.json(); // 응답 본문을 JSON으로 파싱 - }) - .then(data => { - // 응답에서 사용자 이름을 추출하여 UI 업데이트 - document.getElementById('profile-name').textContent = data.data.name; // 프로필 이름 설정 - document.querySelector('.nav-item.dropdown').style.display = 'block'; // 드롭다운 메뉴 표시 - document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'none'; // 로그인 버튼 숨김 - }) - .catch(error => { - // 에러 처리 또는 로그아웃 상태일 때 UI 업데이트 - console.error('Error:', error); - document.getElementById('profile-name').textContent = 'Profile'; // 기본 텍스트로 재설정 - document.querySelector('.nav-item.dropdown').style.display = 'none'; // 드롭다운 메뉴 숨김 - document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'block'; // 로그인 버튼 표시 - }); -} - -// 드롭다운 메뉴 토글 -document.getElementById("navbarDropdown").addEventListener('click', function (e) { - e.preventDefault(); - const dropdownMenu = e.target.closest('.nav-item.dropdown').querySelector('.dropdown-menu'); - dropdownMenu.classList.toggle('show'); // Bootstrap 4에서는 data-toggle 사용, Bootstrap 5에서는 JS로 처리 -}); - - -function login() { - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - - // 입력 필드 검증 - if (!email || !password) { - alert('Please fill in all fields.'); - return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단 - } - - fetch('/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: email, - password: password - }) - }) - .then(response => { - if (200 !== response.status) { - alert('Login failed'); // 로그인 실패 시 경고창 표시 - throw new Error('Login failed'); - } - }) - .then(() => { - updateUIBasedOnLogin(); // UI 업데이트 - window.location.href = '/'; - }) - .catch(error => { - console.error('Error during login:', error); - }); -} - -function signup() { - // Redirect to signup page - window.location.href = '/signup'; -} - -function register(event) { - // 폼 데이터 수집 - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - const name = document.getElementById('name').value; - - // 입력 필드 검증 - if (!email || !password || !name) { - alert('Please fill in all fields.'); - return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단 - } - - // 요청 데이터 포맷팅 - const formData = { - email: email, - password: password, - name: name - }; - - // AJAX 요청 생성 및 전송 - fetch('/members', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }) - .then(response => { - if (!response.ok) { - alert('Signup request failed'); - throw new Error('Signup request failed'); - } - return response.json(); // 여기서 응답을 JSON 형태로 변환 - }) - .then(data => { - // 성공적인 응답 처리 - console.log('Signup successful:', data); - window.location.href = '/login'; - }) - .catch(error => { - // 에러 처리 - console.error('Error during signup:', error); - }); - - // 폼 제출에 의한 페이지 리로드 방지 - event.preventDefault(); -} - -function base64DecodeUnicode(str) { - // Base64 디코딩 - const decodedBytes = atob(str); - // UTF-8 바이트를 문자열로 변환 - const encodedUriComponent = decodedBytes.split('').map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join(''); - return decodeURIComponent(encodedUriComponent); -} diff --git a/src/main/resources/static/js/waiting.js b/src/main/resources/static/js/waiting.js deleted file mode 100644 index eaaf963e..00000000 --- a/src/main/resources/static/js/waiting.js +++ /dev/null @@ -1,69 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - fetch('/reservations/waiting') // 내 예약 목록 조회 API 호출 - .then(response => { - if (response.status === 200) return response.json(); - throw new Error('Read failed'); - }) - .then(render) - .catch(error => console.error('Error fetching reservations:', error)); -}); - -function render(data) { - const tableBody = document.getElementById('table-body'); - tableBody.innerHTML = ''; - - data.data.reservations.forEach(item => { - const row = tableBody.insertRow(); - - const id = item.id; - const name = item.member.name; - const theme = item.theme.name; - const date = item.date; - const startAt = item.time.startAt; - - row.insertCell(0).textContent = id; // 예약 대기 id - row.insertCell(1).textContent = name; // 예약자명 - row.insertCell(2).textContent = theme; // 테마명 - row.insertCell(3).textContent = date; // 예약 날짜 - row.insertCell(4).textContent = startAt; // 시작 시간 - - const actionCell = row.insertCell(row.cells.length); - - actionCell.appendChild(createActionButton('승인', 'btn-primary', approve)); - actionCell.appendChild(createActionButton('거절', 'btn-danger', deny)); - }); -} - -function approve(event) { - const row = event.target.closest('tr'); - const id = row.cells[0].textContent; - - const endpoint = `/reservations/waiting/${id}/confirm` - return fetch(endpoint, { - method: 'POST' - }).then(response => { - if (response.status === 200) return; - throw new Error('Delete failed'); - }).then(() => location.reload()); -} - -function deny(event) { - const row = event.target.closest('tr'); - const id = row.cells[0].textContent; - - const endpoint = `/reservations/waiting/${id}/reject` - return fetch(endpoint, { - method: 'POST' - }).then(response => { - if (response.status === 204) return; - throw new Error('Delete failed'); - }).then(() => location.reload()); -} - -function createActionButton(label, className, eventListener) { - const button = document.createElement('button'); - button.textContent = label; - button.classList.add('btn', className, 'mr-2'); - button.addEventListener('click', eventListener); - return button; -} diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html deleted file mode 100644 index 3a4a7254..00000000 --- a/src/main/resources/templates/admin/index.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

방탈출 어드민

-
- - - - diff --git a/src/main/resources/templates/admin/reservation-new.html b/src/main/resources/templates/admin/reservation-new.html deleted file mode 100644 index 527f0475..00000000 --- a/src/main/resources/templates/admin/reservation-new.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

방탈출 예약 페이지

-
-
-
- -
- - - - - - - - - - - - - - -
예약번호예약자테마날짜시간결제 완료 여부
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-
- - - - - diff --git a/src/main/resources/templates/admin/reservation.html b/src/main/resources/templates/admin/reservation.html deleted file mode 100644 index a827db6b..00000000 --- a/src/main/resources/templates/admin/reservation.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

방탈출 예약 페이지

-
- -
-
- - - - - - - - - - - - -
예약번호예약자날짜시간
-
- - - - diff --git a/src/main/resources/templates/admin/theme.html b/src/main/resources/templates/admin/theme.html deleted file mode 100644 index 70b39718..00000000 --- a/src/main/resources/templates/admin/theme.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

테마 관리 페이지

-
- -
-
- - - - - - - - - - - - -
순서제목설명썸네일 URL
-
- - - - - - diff --git a/src/main/resources/templates/admin/time.html b/src/main/resources/templates/admin/time.html deleted file mode 100644 index f0152542..00000000 --- a/src/main/resources/templates/admin/time.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

시간 관리 페이지

-
- -
-
- - - - - - - - - - -
순서시간
-
- - - - - - diff --git a/src/main/resources/templates/admin/waiting.html b/src/main/resources/templates/admin/waiting.html deleted file mode 100644 index 32c30370..00000000 --- a/src/main/resources/templates/admin/waiting.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

예약 대기 관리 페이지

-
- - - - - - - - - - - - - -
예약대기 번호예약자테마날짜시간
-
- - - - - diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 9740e2ef..00000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - 방탈출 예약 페이지 - - - - - - - - -
-

인기 테마

-
    -
-
- - - - - - diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html deleted file mode 100644 index 8faab43f..00000000 --- a/src/main/resources/templates/login.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - Login - - - - - - - - -
-

Login

-
-
- -
-
- -
-
- - -
-
-
- - - - diff --git a/src/main/resources/templates/reservation-mine.html b/src/main/resources/templates/reservation-mine.html deleted file mode 100644 index f2310ca0..00000000 --- a/src/main/resources/templates/reservation-mine.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - 방탈출 어드민 - - - - - - - - -
-

내 예약

-
- - - - - - - - - - - - - - - -
테마날짜시간상태대기 취소paymentKey결제금액
-
- - - - - diff --git a/src/main/resources/templates/reservation.html b/src/main/resources/templates/reservation.html deleted file mode 100644 index 29c9c8ba..00000000 --- a/src/main/resources/templates/reservation.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - 방탈출 예약 페이지 - - - - - - - - - - - - - -
-

예약 페이지

-
- -
-

날짜 선택

-
-
-
-
- - -
-

테마 선택

-
- -
-
- - -
-

시간 선택

-
- -
-
-
- - -
- -
-
-
- - -
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html deleted file mode 100644 index 6f044d2f..00000000 --- a/src/main/resources/templates/signup.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - Signup - - - - - - - - -
-

Signup

-
-
- - -
-
- - -
-
- - -
- -
-
- - - - diff --git a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt index 9fc7b3f3..4861f4ba 100644 --- a/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt +++ b/src/test/kotlin/roomescape/auth/web/AuthControllerTest.kt @@ -2,7 +2,6 @@ package roomescape.auth.web import com.ninjasquad.springmockk.SpykBean import io.mockk.every -import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.data.repository.findByIdOrNull @@ -16,7 +15,7 @@ import roomescape.util.RoomescapeApiTest @WebMvcTest(controllers = [AuthController::class]) class AuthControllerTest( - val mockMvc: MockMvc + val mockMvc: MockMvc ) : RoomescapeApiTest() { @SpykBean @@ -39,19 +38,14 @@ class AuthControllerTest( jwtHandler.createToken(user.id!!) } returns expectedToken - Then("토큰을 쿠키에 담아 응답한다") { + Then("토큰을 반환한다.") { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = userRequest, + mockMvc = mockMvc, + endpoint = endpoint, + body = userRequest, ) { status { isOk() } - header { - string("Set-Cookie", containsString("accessToken=$expectedToken")) - string("Set-Cookie", containsString("Max-Age=1800")) - string("Set-Cookie", containsString("HttpOnly")) - string("Set-Cookie", containsString("Secure")) - } + jsonPath("$.data.accessToken", equalTo(expectedToken)) } } } @@ -61,12 +55,12 @@ class AuthControllerTest( memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password) } returns null - Then("에러 응답") { + Then("에러 응답을 받는다.") { val expectedError = AuthErrorCode.LOGIN_FAILED runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = userRequest, + mockMvc = mockMvc, + endpoint = endpoint, + body = userRequest, ) { status { isEqualTo(expectedError.httpStatus.value()) } jsonPath("$.code", equalTo(expectedError.errorCode)) @@ -78,14 +72,14 @@ class AuthControllerTest( Then("400 에러를 응답한다") { listOf( - userRequest.copy(email = "invalid"), - userRequest.copy(password = " "), - "{\"email\": \"null\", \"password\": \"null\"}" + userRequest.copy(email = "invalid"), + userRequest.copy(password = " "), + "{\"email\": \"null\", \"password\": \"null\"}" ).forEach { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = it, + mockMvc = mockMvc, + endpoint = endpoint, + body = it, ) { status { isEqualTo(expectedErrorCode.httpStatus.value()) } jsonPath("$.code", equalTo(expectedErrorCode.errorCode)) @@ -101,13 +95,14 @@ class AuthControllerTest( When("로그인된 회원의 ID로 요청하면") { loginAsUser() - Then("회원의 이름을 응답한다") { + Then("회원의 이름과 권한을 응답한다") { runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isOk() } jsonPath("$.data.name", equalTo(user.name)) + jsonPath("$.data.role", equalTo(user.role.name)) } } } @@ -118,11 +113,11 @@ class AuthControllerTest( every { jwtHandler.getMemberIdFromToken(any()) } returns invalidMemberId every { memberRepository.findByIdOrNull(invalidMemberId) } returns null - Then("에러 응답.") { + Then("에러 응답을 받는다.") { val expectedError = AuthErrorCode.UNIDENTIFIABLE_MEMBER runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isEqualTo(expectedError.httpStatus.value()) } jsonPath("$.code", equalTo(expectedError.errorCode)) @@ -130,42 +125,20 @@ class AuthControllerTest( } } } - Given("로그아웃 요청을 보낼 때") { val endpoint = "/logout" - When("로그인 상태가 아니라면") { - doNotLogin() + When("토큰으로 memberId 조회가 가능하면") { + every { + jwtHandler.getMemberIdFromToken(any()) + } returns 1L - Then("로그인 페이지로 이동한다.") { + Then("정상 응답한다.") { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } - } - } - } - - When("로그인 상태라면") { - loginAsUser() - - Then("토큰의 존재 여부와 무관하게 토큰을 만료시킨다.") { - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - ) { - status { isOk() } - header { - string("Set-Cookie", containsString("Max-Age=0")) - string("Set-Cookie", containsString("accessToken=")) - string("Set-Cookie", containsString("Path=/")) - string("Set-Cookie", containsString("HttpOnly")) - string("Set-Cookie", containsString("Secure")) - } + status { isNoContent() } } } } diff --git a/src/test/kotlin/roomescape/auth/web/support/CookieUtilsTest.kt b/src/test/kotlin/roomescape/auth/web/support/CookieUtilsTest.kt index 486885ee..2ec94157 100644 --- a/src/test/kotlin/roomescape/auth/web/support/CookieUtilsTest.kt +++ b/src/test/kotlin/roomescape/auth/web/support/CookieUtilsTest.kt @@ -1,72 +1,26 @@ package roomescape.auth.web.support -import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk -import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletRequest -import roomescape.auth.web.LoginResponse class CookieUtilsTest : FunSpec({ - context("HttpServletRequest에서 accessToken 쿠키를 가져온다.") { + context("accessToken 쿠키를 가져온다.") { val httpServletRequest: HttpServletRequest = mockk() test("accessToken이 있으면 해당 쿠키를 반환한다.") { val token = "test-token" - val cookie = Cookie(ACCESS_TOKEN_COOKIE_NAME, token) - every { httpServletRequest.cookies } returns arrayOf(cookie) + every { httpServletRequest.getHeader("Authorization") } returns "Bearer $token" - assertSoftly(httpServletRequest.accessTokenCookie()) { - this.name shouldBe ACCESS_TOKEN_COOKIE_NAME - this.value shouldBe token - } + httpServletRequest.accessToken() shouldBe token } - test("accessToken이 없으면 accessToken에 빈 값을 담은 쿠키를 반환한다.") { - every { httpServletRequest.cookies } returns arrayOf() + test("accessToken이 없으면 null을 반환한다.") { + every { httpServletRequest.getHeader("Authorization") } returns null - assertSoftly(httpServletRequest.accessTokenCookie()) { - this.name shouldBe ACCESS_TOKEN_COOKIE_NAME - this.value shouldBe "" - } + httpServletRequest.accessToken() shouldBe null } - - test("httpServletRequest.cookies가 null이면 accessToken에 빈 값을 담은 쿠키를 반환한다.") { - every { httpServletRequest.cookies } returns null - - assertSoftly(httpServletRequest.accessTokenCookie()) { - this.name shouldBe ACCESS_TOKEN_COOKIE_NAME - this.value shouldBe "" - } - } - } - - context("TokenResponse를 쿠키로 반환한다.") { - val loginResponse = LoginResponse("test-token") - - val result: String = loginResponse.toResponseCookie() - - result.split("; ") shouldContainAll listOf( - "accessToken=test-token", - "HttpOnly", - "Secure", - "Path=/", - "Max-Age=1800" - ) - } - - context("만료된 accessToken 쿠키를 반환한다.") { - val result: String = expiredAccessTokenCookie() - - result.split("; ") shouldContainAll listOf( - "accessToken=", - "HttpOnly", - "Secure", - "Path=/", - "Max-Age=0" - ) } }) diff --git a/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt b/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt index b34e2555..1d87a7d7 100644 --- a/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt +++ b/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt @@ -4,18 +4,23 @@ import io.kotest.assertions.assertSoftly import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.mockk.every +import io.mockk.mockk import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.test.web.servlet.MockMvc +import roomescape.auth.exception.AuthErrorCode +import roomescape.member.exception.MemberErrorCode +import roomescape.member.infrastructure.persistence.Role import roomescape.member.web.MemberController import roomescape.member.web.MemberRetrieveListResponse +import roomescape.member.web.SignupRequest import roomescape.util.MemberFixture import roomescape.util.RoomescapeApiTest import kotlin.random.Random @WebMvcTest(controllers = [MemberController::class]) class MemberControllerTest( - @Autowired private val mockMvc: MockMvc + @Autowired private val mockMvc: MockMvc ) : RoomescapeApiTest() { init { @@ -23,9 +28,9 @@ class MemberControllerTest( val endpoint = "/members" every { memberRepository.findAll() } returns listOf( - MemberFixture.create(id = Random.nextLong(), name = "name1"), - MemberFixture.create(id = Random.nextLong(), name = "name2"), - MemberFixture.create(id = Random.nextLong(), name = "name3"), + MemberFixture.create(id = Random.nextLong(), name = "name1"), + MemberFixture.create(id = Random.nextLong(), name = "name2"), + MemberFixture.create(id = Random.nextLong(), name = "name3"), ) `when`("관리자가 보내면") { @@ -33,15 +38,15 @@ class MemberControllerTest( then("성공한다.") { val result: String = runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isOk() } }.andReturn().response.contentAsString val response: MemberRetrieveListResponse = readValue( - responseJson = result, - valueType = MemberRetrieveListResponse::class.java + responseJson = result, + valueType = MemberRetrieveListResponse::class.java ) assertSoftly(response.members) { @@ -51,35 +56,91 @@ class MemberControllerTest( } } - `when`("관리자가 아니면 로그인 페이지로 이동한다.") { + `when`("관리자가 아니면 에러 응답을 받는다.") { then("비회원") { doNotLogin() + val expectedError = AuthErrorCode.INVALID_TOKEN runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } + status { isEqualTo(expectedError.httpStatus.value()) } + }.andExpect { + jsonPath("$.code") { value(expectedError.errorCode) } } } then("일반 회원") { loginAsUser() + val expectedError = AuthErrorCode.ACCESS_DENIED runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } + status { isEqualTo(expectedError.httpStatus.value()) } + }.andExpect { + jsonPath("$.code") { value(expectedError.errorCode) } } } } } + + given("POST /members") { + val endpoint = "/members" + val request = SignupRequest( + name = "name", + email = "email@email.com", + password = "password" + ) + `when`("같은 이메일이 없으면") { + every { + memberRepository.findByEmail(request.email) + } returns null + + every { + memberRepository.save(any()) + } returns MemberFixture.create( + id = 1, + name = request.name, + account = request.email, + password = request.password, + role = Role.MEMBER + ) + + then("id과 이름을 담아 성공 응답") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request + ) { + status { isCreated() } + jsonPath("$.data.name") { value(request.name) } + jsonPath("$.data.id") { value(1) } + } + } + } + + `when`("같은 이메일이 있으면") { + every { + memberRepository.findByEmail(request.email) + } returns mockk() + + then("에러 응답") { + val expectedError = MemberErrorCode.DUPLICATE_EMAIL + + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request + ) { + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } + } + + } + } + } } } diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index 04e3fb2a..054ffe3b 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -9,14 +9,13 @@ import io.restassured.module.kotlin.extensions.Given import io.restassured.module.kotlin.extensions.Then import io.restassured.module.kotlin.extensions.When import jakarta.persistence.EntityManager -import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.transaction.support.TransactionTemplate +import roomescape.auth.exception.AuthErrorCode import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.support.MemberIdResolver import roomescape.member.business.MemberService @@ -38,9 +37,9 @@ import java.time.LocalTime @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ReservationControllerTest( - @LocalServerPort val port: Int, - val entityManager: EntityManager, - val transactionTemplate: TransactionTemplate + @LocalServerPort val port: Int, + val entityManager: EntityManager, + val transactionTemplate: TransactionTemplate ) : FunSpec({ extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST)) }) { @@ -66,9 +65,9 @@ class ReservationControllerTest( test("정상 응답") { val reservationRequest = createRequest() val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( - paymentKey = reservationRequest.paymentKey, - orderId = reservationRequest.orderId, - totalAmount = reservationRequest.amount, + paymentKey = reservationRequest.paymentKey, + orderId = reservationRequest.orderId, + totalAmount = reservationRequest.amount, ) every { @@ -108,12 +107,12 @@ class ReservationControllerTest( } } - test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답") { + test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답을 받는다.") { val reservationRequest = createRequest() val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( - paymentKey = reservationRequest.paymentKey, - orderId = reservationRequest.orderId, - totalAmount = reservationRequest.amount, + paymentKey = reservationRequest.paymentKey, + orderId = reservationRequest.orderId, + totalAmount = reservationRequest.amount, ) every { @@ -129,8 +128,8 @@ class ReservationControllerTest( } returns PaymentFixture.createCancelResponse() val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( - "SELECT COUNT(c) FROM CanceledPaymentEntity c", - Long::class.java + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java ).singleResult Given { @@ -145,8 +144,8 @@ class ReservationControllerTest( } val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery( - "SELECT COUNT(c) FROM CanceledPaymentEntity c", - Long::class.java + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java ).singleResult canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L @@ -203,6 +202,7 @@ class ReservationControllerTest( test("관리자만 검색할 수 있다.") { login(reservations.keys.first()) + val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) @@ -210,7 +210,8 @@ class ReservationControllerTest( }.When { get("/reservations/search") }.Then { - header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE)) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } @@ -309,14 +310,15 @@ class ReservationControllerTest( test("관리자만 예약을 삭제할 수 있다.") { login(MemberFixture.create(role = Role.MEMBER)) val reservation: ReservationEntity = reservations.values.flatten().first() + val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) }.When { delete("/reservations/${reservation.id}") }.Then { - statusCode(302) - header(HttpHeaders.LOCATION, containsString("/login")) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } @@ -326,8 +328,8 @@ class ReservationControllerTest( transactionTemplate.execute { val reservation: ReservationEntity = entityManager.find( - ReservationEntity::class.java, - reservationId + ReservationEntity::class.java, + reservationId ) reservation.reservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED entityManager.persist(reservation) @@ -346,8 +348,8 @@ class ReservationControllerTest( // 예약이 삭제되었는지 확인 transactionTemplate.executeWithoutResult { val deletedReservation = entityManager.find( - ReservationEntity::class.java, - reservationId + ReservationEntity::class.java, + reservationId ) deletedReservation shouldBe null } @@ -371,8 +373,8 @@ class ReservationControllerTest( } returns PaymentFixture.createCancelResponse() val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( - "SELECT COUNT(c) FROM CanceledPaymentEntity c", - Long::class.java + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java ).singleResult Given { @@ -384,8 +386,8 @@ class ReservationControllerTest( } val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery( - "SELECT COUNT(c) FROM CanceledPaymentEntity c", - Long::class.java + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java ).singleResult canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L @@ -397,10 +399,10 @@ class ReservationControllerTest( val member = login(MemberFixture.create(role = Role.ADMIN)) val adminRequest: AdminReservationCreateRequest = createRequest().let { AdminReservationCreateRequest( - date = it.date, - themeId = it.themeId, - timeId = it.timeId, - memberId = member.id!!, + date = it.date, + themeId = it.themeId, + timeId = it.timeId, + memberId = member.id!!, ) } @@ -425,6 +427,7 @@ class ReservationControllerTest( test("관리자가 아니면 조회할 수 없다.") { login(MemberFixture.create(role = Role.MEMBER)) + val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) @@ -432,14 +435,15 @@ class ReservationControllerTest( }.When { get("/reservations/waiting") }.Then { - header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE)) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } test("대기 중인 예약 목록을 조회한다.") { login(MemberFixture.create(role = Role.ADMIN)) val expected = reservations.values.flatten() - .count { it.reservationStatus == ReservationStatus.WAITING } + .count { it.reservationStatus == ReservationStatus.WAITING } Given { port(port) @@ -458,9 +462,9 @@ class ReservationControllerTest( val member = login(MemberFixture.create(role = Role.MEMBER)) val waitingCreateRequest: WaitingCreateRequest = createRequest().let { WaitingCreateRequest( - date = it.date, - themeId = it.themeId, - timeId = it.timeId + date = it.date, + themeId = it.themeId, + timeId = it.timeId ) } @@ -483,11 +487,11 @@ class ReservationControllerTest( transactionTemplate.executeWithoutResult { val reservation = ReservationFixture.create( - date = reservationRequest.date, - theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId), - time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId), - member = member, - status = ReservationStatus.WAITING + date = reservationRequest.date, + theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId), + time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId), + member = member, + status = ReservationStatus.WAITING ) entityManager.persist(reservation) entityManager.flush() @@ -496,9 +500,9 @@ class ReservationControllerTest( // 이미 예약된 시간, 테마로 대기 예약 요청 val waitingCreateRequest = WaitingCreateRequest( - date = reservationRequest.date, - themeId = reservationRequest.themeId, - timeId = reservationRequest.timeId + date = reservationRequest.date, + themeId = reservationRequest.themeId, + timeId = reservationRequest.timeId ) val expectedError = ReservationErrorCode.ALREADY_RESERVE @@ -524,8 +528,8 @@ class ReservationControllerTest( test("대기 중인 예약을 취소한다.") { val member = login(MemberFixture.create(role = Role.MEMBER)) val waiting: ReservationEntity = createSingleReservation( - member = member, - status = ReservationStatus.WAITING + member = member, + status = ReservationStatus.WAITING ) Given { @@ -538,8 +542,8 @@ class ReservationControllerTest( transactionTemplate.executeWithoutResult { _ -> entityManager.find( - ReservationEntity::class.java, - waiting.id + ReservationEntity::class.java, + waiting.id ) shouldBe null } } @@ -547,8 +551,8 @@ class ReservationControllerTest( test("이미 확정된 예약을 삭제하면 예외 응답") { val member = login(MemberFixture.create(role = Role.MEMBER)) val reservation: ReservationEntity = createSingleReservation( - member = member, - status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + member = member, + status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ) val expectedError = ReservationErrorCode.ALREADY_CONFIRMED @@ -566,22 +570,22 @@ class ReservationControllerTest( context("POST /reservations/waiting/{id}/confirm") { test("관리자만 승인할 수 있다.") { login(MemberFixture.create(role = Role.MEMBER)) - + val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) }.When { post("/reservations/waiting/1/confirm") }.Then { - statusCode(302) - header(HttpHeaders.LOCATION, containsString("/login")) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } test("대기 예약을 승인하면 결제 대기 상태로 변경") { val member = login(MemberFixture.create(role = Role.ADMIN)) val reservation = createSingleReservation( - member = member, - status = ReservationStatus.WAITING + member = member, + status = ReservationStatus.WAITING ) Given { @@ -594,8 +598,8 @@ class ReservationControllerTest( transactionTemplate.executeWithoutResult { _ -> entityManager.find( - ReservationEntity::class.java, - reservation.id + ReservationEntity::class.java, + reservation.id )?.also { it.reservationStatus shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED } ?: throw AssertionError("Reservation not found") @@ -605,8 +609,8 @@ class ReservationControllerTest( test("다른 확정된 예약을 승인하면 예외 응답") { val admin = login(MemberFixture.create(role = Role.ADMIN)) val alreadyReserved = createSingleReservation( - member = admin, - status = ReservationStatus.CONFIRMED + member = admin, + status = ReservationStatus.CONFIRMED ) val member = MemberFixture.create(account = "account", role = Role.MEMBER).also { it -> @@ -615,11 +619,11 @@ class ReservationControllerTest( } } val waiting = ReservationFixture.create( - date = alreadyReserved.date, - time = alreadyReserved.time, - theme = alreadyReserved.theme, - member = member, - status = ReservationStatus.WAITING + date = alreadyReserved.date, + time = alreadyReserved.time, + theme = alreadyReserved.theme, + member = member, + status = ReservationStatus.WAITING ).also { transactionTemplate.executeWithoutResult { _ -> entityManager.persist(it) @@ -642,22 +646,23 @@ class ReservationControllerTest( context("POST /reservations/waiting/{id}/reject") { test("관리자만 거절할 수 있다.") { login(MemberFixture.create(role = Role.MEMBER)) + val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) }.When { post("/reservations/waiting/1/reject") }.Then { - statusCode(302) - header(HttpHeaders.LOCATION, containsString("/login")) + statusCode(expectedError.httpStatus.value()) + body("code", equalTo(expectedError.errorCode)) } } test("거절된 예약은 삭제된다.") { val member = login(MemberFixture.create(role = Role.ADMIN)) val reservation = createSingleReservation( - member = member, - status = ReservationStatus.WAITING + member = member, + status = ReservationStatus.WAITING ) Given { @@ -670,8 +675,8 @@ class ReservationControllerTest( transactionTemplate.executeWithoutResult { _ -> entityManager.find( - ReservationEntity::class.java, - reservation.id + ReservationEntity::class.java, + reservation.id ) shouldBe null } } @@ -679,18 +684,18 @@ class ReservationControllerTest( } fun createSingleReservation( - date: LocalDate = LocalDate.now().plusDays(1), - time: LocalTime = LocalTime.now(), - themeName: String = "Default Theme", - member: MemberEntity = MemberFixture.create(role = Role.MEMBER), - status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + date: LocalDate = LocalDate.now().plusDays(1), + time: LocalTime = LocalTime.now(), + themeName: String = "Default Theme", + member: MemberEntity = MemberFixture.create(role = Role.MEMBER), + status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ): ReservationEntity { return ReservationFixture.create( - date = date, - theme = ThemeFixture.create(name = themeName), - time = TimeFixture.create(startAt = time), - member = member, - status = status + date = date, + theme = ThemeFixture.create(name = themeName), + time = TimeFixture.create(startAt = time), + member = member, + status = status ).also { it -> transactionTemplate.execute { _ -> if (member.id == null) { @@ -708,8 +713,8 @@ class ReservationControllerTest( fun createDummyReservations(): MutableMap> { val reservations: MutableMap> = mutableMapOf() val members: List = listOf( - MemberFixture.create(role = Role.MEMBER), - MemberFixture.create(role = Role.MEMBER) + MemberFixture.create(role = Role.MEMBER), + MemberFixture.create(role = Role.MEMBER) ) transactionTemplate.executeWithoutResult { @@ -728,11 +733,11 @@ class ReservationControllerTest( entityManager.persist(time) val reservation = ReservationFixture.create( - date = LocalDate.now().plusDays(index.toLong()), - theme = theme, - time = time, - member = members[index % members.size], - status = ReservationStatus.CONFIRMED + date = LocalDate.now().plusDays(index.toLong()), + theme = theme, + time = time, + member = members[index % members.size], + status = ReservationStatus.CONFIRMED ) entityManager.persist(reservation) reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation) @@ -745,8 +750,8 @@ class ReservationControllerTest( } fun createRequest( - theme: ThemeEntity = ThemeFixture.create(), - time: TimeEntity = TimeFixture.create(), + theme: ThemeEntity = ThemeFixture.create(), + time: TimeEntity = TimeFixture.create(), ): ReservationCreateWithPaymentRequest { lateinit var reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest @@ -755,8 +760,8 @@ class ReservationControllerTest( entityManager.persist(time) reservationCreateWithPaymentRequest = ReservationFixture.createRequest( - themeId = theme.id!!, - timeId = time.id!!, + themeId = theme.id!!, + timeId = time.id!!, ) entityManager.flush() diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index eb959887..c8fb56d6 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -8,6 +8,7 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.just import io.mockk.runs +import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc @@ -34,15 +35,15 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { When("로그인 상태가 아니라면") { doNotLogin() - Then("로그인 페이지로 이동한다.") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.INVALID_TOKEN runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } + status { isEqualTo(expectedError.httpStatus.value()) } + }.andExpect { + jsonPath("$.code") { value(expectedError.errorCode) } } } } @@ -54,14 +55,14 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { every { themeRepository.findAll() } returns listOf( - ThemeFixture.create(id = 1, name = "theme1"), - ThemeFixture.create(id = 2, name = "theme2"), - ThemeFixture.create(id = 3, name = "theme3") + ThemeFixture.create(id = 1, name = "theme1"), + ThemeFixture.create(id = 2, name = "theme2"), + ThemeFixture.create(id = 3, name = "theme3") ) val response: ThemeRetrieveListResponse = runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isOk() } content { @@ -80,37 +81,37 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { Given("테마를 추가할 때") { val endpoint = "/themes" val request = ThemeCreateRequest( - name = "theme1", - description = "description1", - thumbnail = "http://example.com/thumbnail1.jpg" + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" ) When("로그인 상태가 아니라면") { doNotLogin() - Then("로그인 페이지로 이동한다.") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.INVALID_TOKEN runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } When("관리자가 아닌 회원은") { loginAsUser() - Then("로그인 페이지로 이동한다.") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.ACCESS_DENIED runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { - status { is3xxRedirection() } - jsonPath("$.code") { value(AuthErrorCode.ACCESS_DENIED.errorCode) } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } } } } @@ -120,15 +121,15 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED - Then("에러 응답.") { + Then("에러 응답을 받는다.") { every { themeRepository.existsByName(request.name) } returns true runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { status { isEqualTo(expectedError.httpStatus.value()) } jsonPath("$.code") { value(expectedError.errorCode) } @@ -142,16 +143,16 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } val request = ThemeCreateRequest( - name = "theme1", - description = "description1", - thumbnail = "http://example.com/thumbnail1.jpg" + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" ) fun runTest(request: ThemeCreateRequest) { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { status { isBadRequest() } } @@ -192,26 +193,26 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { loginAsAdmin() val theme = ThemeFixture.create( - id = 1, - name = request.name, - description = request.description, - thumbnail = request.thumbnail + id = 1, + name = request.name, + description = request.description, + thumbnail = request.thumbnail ) every { themeService.createTheme(request) } returns ThemeRetrieveResponse( - id = theme.id!!, - name = theme.name, - description = theme.description, - thumbnail = theme.thumbnail + id = theme.id!!, + name = theme.name, + description = theme.description, + thumbnail = theme.thumbnail ) Then("201 응답을 받는다.") { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { status { isCreated() } header { @@ -232,28 +233,28 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { When("로그인 상태가 아니라면") { doNotLogin() - Then("로그인 페이지로 이동한다.") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.INVALID_TOKEN runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } When("관리자가 아닌 회원은") { loginAsUser() - Then("로그인 페이지로 이동한다.") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.ACCESS_DENIED runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - jsonPath("$.code") { value(AuthErrorCode.ACCESS_DENIED.errorCode) } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } @@ -262,14 +263,14 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { loginAsAdmin() val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED - Then("에러 응답") { + Then("에러 응답을 받는다.") { every { themeRepository.isReservedTheme(themeId) } returns true runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isEqualTo(expectedError.httpStatus.value()) } jsonPath("$.code") { value(expectedError.errorCode) } @@ -290,8 +291,8 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { Then("204 응답을 받는다.") { runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isNoContent() } } diff --git a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt index 19ae8465..b9ef49a9 100644 --- a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt +++ b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt @@ -6,11 +6,13 @@ import io.kotest.assertions.assertSoftly import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import io.mockk.every +import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Import import org.springframework.data.repository.findByIdOrNull import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc +import roomescape.auth.exception.AuthErrorCode import roomescape.common.config.JacksonConfig import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.time.business.TimeService @@ -27,7 +29,7 @@ import java.time.LocalTime @WebMvcTest(TimeController::class) @Import(JacksonConfig::class) class TimeControllerTest( - val mockMvc: MockMvc, + val mockMvc: MockMvc, ) : RoomescapeApiTest() { @SpykBean @@ -52,13 +54,13 @@ class TimeControllerTest( every { timeRepository.findAll() } returns listOf( - TimeFixture.create(id = 1L), - TimeFixture.create(id = 2L) + TimeFixture.create(id = 1L), + TimeFixture.create(id = 2L) ) runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isOk() } content { @@ -72,14 +74,19 @@ class TimeControllerTest( When("관리자가 아닌 경우") { loginAsUser() + val expectedError = AuthErrorCode.ACCESS_DENIED - Then("로그인 페이지로 이동") { + Then("에러 응답을 받는다.") { runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { string("Location", "/login") } + status { isEqualTo(expectedError.httpStatus.value()) } + }.andExpect { + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.code") { value(expectedError.errorCode) } + } } } } @@ -97,13 +104,13 @@ class TimeControllerTest( Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") { listOf( - "{\"startAt\": \"23:30:30\"}", - "{\"startAt\": \"24:59\"}", + "{\"startAt\": \"23:30:30\"}", + "{\"startAt\": \"24:59\"}", ).forEach { runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = it, + mockMvc = mockMvc, + endpoint = endpoint, + body = it, ) { status { isBadRequest() } } @@ -116,9 +123,9 @@ class TimeControllerTest( } returns TimeCreateResponse(id = 1, startAt = time) runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { status { isCreated() } content { @@ -136,9 +143,9 @@ class TimeControllerTest( } returns true runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, + mockMvc = mockMvc, + endpoint = endpoint, + body = request, ) { status { isEqualTo(expectedError.httpStatus.value()) } content { @@ -152,14 +159,15 @@ class TimeControllerTest( When("관리자가 아닌 경우") { loginAsUser() - Then("로그인 페이지로 이동") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.ACCESS_DENIED runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = TimeFixture.create(), + mockMvc = mockMvc, + endpoint = endpoint, + body = TimeFixture.create(), ) { - status { is3xxRedirection() } - header { string("Location", "/login") } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } @@ -179,8 +187,8 @@ class TimeControllerTest( } returns Unit runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { status { isNoContent() } } @@ -194,8 +202,8 @@ class TimeControllerTest( } returns null runDeleteTest( - mockMvc = mockMvc, - endpoint = "/times/$id", + mockMvc = mockMvc, + endpoint = "/times/$id", ) { status { isEqualTo(expectedError.httpStatus.value()) } content { @@ -217,8 +225,8 @@ class TimeControllerTest( } returns listOf(ReservationFixture.create()) runDeleteTest( - mockMvc = mockMvc, - endpoint = "/times/$id", + mockMvc = mockMvc, + endpoint = "/times/$id", ) { status { isEqualTo(expectedError.httpStatus.value()) } content { @@ -232,13 +240,14 @@ class TimeControllerTest( When("관리자가 아닌 경우") { loginAsUser() - Then("로그인 페이지로 이동") { + Then("에러 응답을 받는다.") { + val expectedError = AuthErrorCode.ACCESS_DENIED runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, + mockMvc = mockMvc, + endpoint = endpoint, ) { - status { is3xxRedirection() } - header { string("Location", "/login") } + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code", equalTo(expectedError.errorCode)) } } } @@ -252,8 +261,8 @@ class TimeControllerTest( When("저장된 예약 시간이 있으면") { val times: List = listOf( - TimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)), - TimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0)) + TimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)), + TimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0)) ) every { @@ -265,17 +274,17 @@ class TimeControllerTest( every { reservationRepository.findByDateAndThemeId(date, themeId) } returns listOf( - ReservationFixture.create( - id = 1L, - date = date, - theme = ThemeFixture.create(id = themeId), - time = times[0] - ) + ReservationFixture.create( + id = 1L, + date = date, + theme = ThemeFixture.create(id = themeId), + time = times[0] + ) ) val response = runGetTest( - mockMvc = mockMvc, - endpoint = "/times/search?date=$date&themeId=$themeId", + mockMvc = mockMvc, + endpoint = "/times/search?date=$date&themeId=$themeId", ) { status { isOk() } content { diff --git a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt index e1874a72..b393d50a 100644 --- a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt @@ -42,24 +42,24 @@ abstract class RoomescapeApiTest : BehaviorSpec() { val user: MemberEntity = MemberFixture.user() fun runGetTest( - mockMvc: MockMvc, - endpoint: String, - log: Boolean = false, - assert: MockMvcResultMatchersDsl.() -> Unit + mockMvc: MockMvc, + endpoint: String, + log: Boolean = false, + assert: MockMvcResultMatchersDsl.() -> Unit ): ResultActionsDsl = mockMvc.get(endpoint) { - header(HttpHeaders.COOKIE, "accessToken=token") + header(HttpHeaders.AUTHORIZATION, "Bearer token") }.apply { log.takeIf { it }?.let { this.andDo { print() } } }.andExpect(assert) fun runPostTest( - mockMvc: MockMvc, - endpoint: String, - body: Any? = null, - log: Boolean = false, - assert: MockMvcResultMatchersDsl.() -> Unit + mockMvc: MockMvc, + endpoint: String, + body: Any? = null, + log: Boolean = false, + assert: MockMvcResultMatchersDsl.() -> Unit ): ResultActionsDsl = mockMvc.post(endpoint) { - this.header(HttpHeaders.COOKIE, "accessToken=token") + this.header(HttpHeaders.AUTHORIZATION, "Bearer token") body?.let { this.contentType = MediaType.APPLICATION_JSON this.content = objectMapper.writeValueAsString(it) @@ -69,12 +69,12 @@ abstract class RoomescapeApiTest : BehaviorSpec() { }.andExpect(assert) fun runDeleteTest( - mockMvc: MockMvc, - endpoint: String, - log: Boolean = false, - assert: MockMvcResultMatchersDsl.() -> Unit + mockMvc: MockMvc, + endpoint: String, + log: Boolean = false, + assert: MockMvcResultMatchersDsl.() -> Unit ): ResultActionsDsl = mockMvc.delete(endpoint) { - header(HttpHeaders.COOKIE, "accessToken=token") + header(HttpHeaders.AUTHORIZATION, "Bearer token") }.apply { log.takeIf { it }?.let { this.andDo { print() } } }.andExpect(assert) @@ -107,16 +107,20 @@ abstract class RoomescapeApiTest : BehaviorSpec() { } fun MvcResult.readValue(valueType: Class): T = this.response.contentAsString - .takeIf { it.isNotBlank() } - ?.let { readValue(it, valueType) } - ?: throw RuntimeException(""" + .takeIf { it.isNotBlank() } + ?.let { readValue(it, valueType) } + ?: throw RuntimeException( + """ [Test] Exception occurred while reading response json: ${this.response.contentAsString} with value type: $valueType - """.trimIndent()) + """.trimIndent() + ) fun readValue(responseJson: String, valueType: Class): T = objectMapper - .readTree(responseJson)["data"] - ?.let { objectMapper.convertValue(it, valueType) } - ?: throw RuntimeException(""" + .readTree(responseJson)["data"] + ?.let { objectMapper.convertValue(it, valueType) } + ?: throw RuntimeException( + """ [Test] Exception occurred while reading response json: $responseJson with value type: $valueType - """.trimIndent()) + """.trimIndent() + ) } diff --git a/src/test/kotlin/roomescape/view/PageControllerTest.kt b/src/test/kotlin/roomescape/view/PageControllerTest.kt deleted file mode 100644 index 0b223c7b..00000000 --- a/src/test/kotlin/roomescape/view/PageControllerTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package roomescape.view - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.test.web.servlet.MockMvc -import roomescape.util.RoomescapeApiTest - -@WebMvcTest(controllers = [ - AuthPageController::class, - AdminPageController::class, - ClientPageController::class -]) -class PageControllerTest( - @Autowired private val mockMvc: MockMvc -) : RoomescapeApiTest() { - - init { - listOf("/", "/login").forEach { - given("GET $it 요청은") { - `when`("로그인 및 권한 여부와 관계없이 성공한다.") { - then("비회원") { - doNotLogin() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - - then("회원") { - loginAsUser() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - - then("관리자") { - loginAsAdmin() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - } - } - } - - listOf("/admin", "/admin/reservation", "/admin/time", "/admin/theme", "/admin/waiting").forEach { - given("GET $it 요청을") { - `when`("관리자가 보내면") { - loginAsAdmin() - - then("성공한다.") { - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - } - - `when`("회원이 보내면") { - loginAsUser() - - then("로그인 페이지로 이동한다.") { - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } - } - } - } - } - } - - listOf("/reservation", "/reservation-mine").forEach { - given("GET $it 요청을") { - `when`("로그인 된 회원이 보내면 성공한다.") { - then("회원") { - loginAsUser() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - then("관리자") { - loginAsAdmin() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { isOk() } - } - } - } - - `when`("로그인 없이 보내면") { - then("로그인 페이지로 이동한다.") { - doNotLogin() - - runGetTest( - mockMvc = mockMvc, - endpoint = it, - ) { - status { is3xxRedirection() } - header { - string("Location", "/login") - } - } - } - } - } - } - } -} \ No newline at end of file