diff --git a/.firebase/hosting.ZGlzdA.cache b/.firebase/hosting.ZGlzdA.cache new file mode 100644 index 0000000..9052cf9 --- /dev/null +++ b/.firebase/hosting.ZGlzdA.cache @@ -0,0 +1,21 @@ +mockServiceWorker.js,1701150747621,ec13ae364749a2b7f62d03f26c23f8d3b8bd87f0f2548e440a93ba9eca00bb01 +vite.svg,1700891169273,59ec4b6085a0cb1bf712a5e48dd5f35b08e34830d49c2026c18241be04e05d5a +index.html,1701155544817,968c5dce8e7f4c4062554f5df07f5dfe5b3be0a1800ccf4aec28d53164b9cc5c +assets/ajax-loader-XJzBMpIy.gif,1701155544817,e9699a225e244ba8bb48532313e5c56a76fe08358c6dd684fb3234f2f63e7961 +assets/logo-lxiO5Ccs.png,1701155544817,a75812f98bbf330ff261ed1cc539ffa5200370988720075f6fef7d3c4fefcb1b +assets/slick-Zcw5u4Ni.svg,1701155544817,d00477d40ed40fa1dd3bdee27d9286804121b15fea83bae7d5415d902b055de0 +assets/index--cVBK-Bo.css,1701155544817,49b2ac7524cd45974a048151ca4854b8329413867a8c500863da7b93bc9ac60e +assets/option2-kUkB2nNl.svg,1701155544817,827f70b1ffdd41094aad846d5e463918ac2450b9559488b75db63502d13ee29e +assets/type4-GLHtvs05.jpg,1701155544817,c0f82eda037586246bac553eba4f673cf969495c40d182a7d972ea3871802cae +assets/type1-AlJ1Dfbp.jpeg,1701155544817,123fddc8c9a531559cb52297531ab48861b282bfa7a2196f1ad76bbca273842f +assets/type2-OLa2L4XE.jpg,1701155544816,b9027e150b8594b04be7664b45b05c4df0033acba25128d0daf7bad43833b1fc +assets/browser-kOiMfIzF.js,1701155544817,c13121552a9bff71a074112caae7129f70a908a8d02f5bdd2cf0df4d11f1f441 +assets/type7-CT-b9aiE.jpg,1701155544817,d5c748e21f620f09e9c1ab87af030b9a7230ed2c425c6cb5612faafd47b5a147 +assets/type3-pxTTyho3.jpg,1701155544817,fb16202e7816d05ac78978945bcc0a7d009c0af436cb94d2c1d3ce5adc58b256 +assets/type5-VjGfLRVS.jpg,1701155544817,4fa94e9a96a76156d51cc8d43f2953f8b0be76d5d8f4f15d006c17810aba58c1 +assets/type6-dxlAL5Ol.png,1701155544817,eb76cdb243961f02e77dbda6cb6b125e402082cfb97e2f1a979bf9097b7d8bb0 +assets/hotel-default-r7VDbsny.jpg,1701155544817,4a28169357fef027876db65caedf24b99024f5a7a6be958cbc5db9ba9e6e1971 +assets/type8-sJeA8EB-.png,1701155544817,dd139aca2065ef05f0fe27304aeb9712e72a0816cbf991b501b2d1991b57e129 +assets/type0-DInKlT4R.png,1701155544817,59fab688312360a5f573c8d2361df66d3b57b3b91668ed976b0f97170de4eb05 +assets/type9-xpFQHIgZ.png,1701155544817,e14cd4c44dce0488773495acfdbcaed8029c659588caa468258480ad7697a191 +assets/index-9bmLIOoD.js,1701155544817,ed73b1e1b34ea84c1b658484926c732498f180a9c5cb4e0d737bf4fef656215a diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache new file mode 100644 index 0000000..8b998cb --- /dev/null +++ b/.firebase/hosting.cHVibGlj.cache @@ -0,0 +1,4 @@ +404.html,1701151600723,daa499dd96d8229e73235345702ba32f0793f0c8e5c0d30e40e37a5872be57aa +vite.svg,1700891169273,59ec4b6085a0cb1bf712a5e48dd5f35b08e34830d49c2026c18241be04e05d5a +index.html,1701151600926,060773d390ab3ab8974ab90c5186ef556b2f4abbc2f538b9b964d1b16c0defcb +mockServiceWorker.js,1701150747621,ec13ae364749a2b7f62d03f26c23f8d3b8bd87f0f2548e440a93ba9eca00bb01 diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..0d20308 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "yaja-project" + } +} diff --git a/.github/ISSUE_TEMPLATE/fe_pull_request_template.md b/.github/ISSUE_TEMPLATE/fe_pull_request_template.md index 9aed217..c08feba 100644 --- a/.github/ISSUE_TEMPLATE/fe_pull_request_template.md +++ b/.github/ISSUE_TEMPLATE/fe_pull_request_template.md @@ -1,5 +1,3 @@ ---- - ## Issue Number diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..a955cb8 --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +'on': + push: + branches: + - develop +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm i && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_YAJA_PROJECT }}' + channelId: live + projectId: yaja-project diff --git a/.gitignore b/.gitignore index a547bf3..d823d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + + +.env \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index ba50068..d24fdfc 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npm run format -npm run lint +npx lint-staged diff --git a/babel.config.cjs b/babel.config.cjs index 877f1b6..a60dba4 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -1,4 +1,7 @@ // eslint-disable-next-line no-undef module.exports = { - presets: [["@babel/preset-env", { targets: { node: "current" } }]], + presets: [ + "babel-plugin-transform-import-meta", + ["@babel/preset-env", { targets: { node: "current" } }], + ], }; diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..6564edd --- /dev/null +++ b/firebase.json @@ -0,0 +1,12 @@ +{ + "hosting": { + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/index.html b/index.html index e4b78ea..8a0e197 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Hey놀자
diff --git a/jest.config.cjs b/jest.config.cjs index f78ab14..b04c0f7 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -5,6 +5,8 @@ module.exports = { // https://jestjs.io/docs/configuration#testenvironment-string testEnvironment: "jsdom", + preset: "ts-jest/presets/default-esm", + // A list of paths to directories that Jest should use to search for files in // https://jestjs.io/docs/configuration#roots-arraystring roots: ["/src/"], @@ -16,7 +18,26 @@ module.exports = { // Jest transformations // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object transform: { - "^.+\\.tsx?$": "ts-jest", // Transform TypeScript files using ts-jest + "^.+\\.tsx?$": [ + "ts-jest", + { + diagnostics: { + ignoreCodes: [1343], + }, + astTransformers: { + before: [ + { + path: "node_modules/ts-jest-mock-import-meta", // or, alternatively, 'ts-jest-mock-import-meta' directly, without node_modules. + options: { + metaObjectReplacement: { + url: "http://43.200.54.142:8080/api/v1", + }, + }, + }, + ], + }, + }, + ], }, // A list of paths to modules that run some code to configure or set up the testing framework before each test file in the suite is executed @@ -45,12 +66,15 @@ module.exports = { // Handle static assets // https://jestjs.io/docs/webpack#handling-static-assets - "^.+\\.(jpg|jpeg|png|gif|webp|avif|svg|ttf|woff|woff2)$": `/__mocks__/fileMock.js`, + "^.+\\.(jpg|jpeg|png|gif|webp|avif|svg|ttf|woff|woff2)$": + "jest-svg-transformer", // Handle TypeScript path aliases "^@/(.*)$": "/src/$1", }, + snapshotSerializers: ["@emotion/jest/serializer"], + verbose: true, testTimeout: 30000, }; diff --git a/jest.setup.ts b/jest.setup.ts index d0de870..b46bc2f 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,7 @@ import "@testing-library/jest-dom"; +import "@emotion/jest"; +import "@emotion/react"; +import "@emotion/styled"; +import { matchers } from "@emotion/jest"; + +expect.extend(matchers); diff --git a/package-lock.json b/package-lock.json index 206277a..07cd2ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@types/react-slick": "^0.23.12", "axios": "^1.6.2", "emotion-reset": "^3.0.1", + "firebase": "^10.7.0", "framer-motion": "^10.16.5", "lodash": "^4.17.21", "lottie-react": "^2.4.0", @@ -26,16 +27,20 @@ "react-hook-form": "^7.48.2", "react-infinite-scroll-component": "^6.1.0", "react-infinite-scroller": "^1.2.6", + "react-intersection-observer": "^9.5.3", "react-router-dom": "^6.19.0", "react-slick": "^0.29.0", + "react-spinners": "^0.13.8", "recoil": "^0.7.7", "recoil-persist": "^5.1.0", - "slick-carousel": "^1.8.1" + "slick-carousel": "^1.8.1", + "sweetalert2": "^11.10.1" }, "devDependencies": { "@babel/preset-env": "^7.23.3", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@emotion/jest": "^11.11.0", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", @@ -48,20 +53,27 @@ "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.2.0", "babel-jest": "^29.7.0", + "babel-plugin-transform-import-meta": "^2.2.1", + "dotenv": "^16.3.1", "eslint": "^8.53.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", + "gh-pages": "^6.1.0", "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-svg-transformer": "^1.0.0", "jsdom": "^22.1.0", "lint-staged": "^15.1.0", "msw": "^2.0.8", "prettier": "3.1.0", + "terser": "^5.24.0", "ts-jest": "^29.1.1", + "ts-jest-mock-import-meta": "^1.1.0", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vite-plugin-environment": "^1.1.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -93,11 +105,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", + "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -143,22 +155,13 @@ "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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", + "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", "dev": true, "dependencies": { - "@babel/types": "^7.23.3", + "@babel/types": "^7.23.4", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -207,15 +210,6 @@ "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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", @@ -239,15 +233,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", @@ -265,15 +250,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", @@ -497,23 +473,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", + "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.4", + "@babel/types": "^7.23.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -524,9 +500,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", + "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1851,15 +1827,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -1920,9 +1887,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", + "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1945,19 +1912,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", + "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.4", + "@babel/generator": "^7.23.4", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/parser": "^7.23.4", + "@babel/types": "^7.23.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2034,17 +2001,6 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, - "node_modules/@emotion/babel-plugin/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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@emotion/cache": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", @@ -2057,6 +2013,16 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/css-prettifier": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/css-prettifier/-/css-prettifier-1.1.3.tgz", + "integrity": "sha512-KNv23+VQ+pcw3ebd1vSEl11CQ6SKAG5EQkrinjVGsfw3ZTWe6/tpWQrsvFLqCtU2LRiLPi04KgFCE4A9+crfpQ==", + "dev": true, + "dependencies": { + "@emotion/memoize": "^0.8.1", + "stylis": "4.2.0" + } + }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", @@ -2070,6 +2036,101 @@ "@emotion/memoize": "^0.8.1" } }, + "node_modules/@emotion/jest": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/jest/-/jest-11.11.0.tgz", + "integrity": "sha512-XZlnmdUZ32YjQnInsCFk/plKpkV/NXN1Ab4YoNvXN887MeR3Hr5ZsTyoblIW8AWwdfQiZHHphaPMb56lk6Ofdw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/css-prettifier": "^1.1.3", + "chalk": "^4.1.0", + "specificity": "^0.4.1", + "stylis": "4.2.0" + }, + "peerDependencies": { + "@types/jest": "^26.0.14 || ^27.0.0 || ^28.0.0 || ^29.0.0", + "enzyme-to-json": "^3.2.1" + }, + "peerDependenciesMeta": { + "@types/jest": { + "optional": true + }, + "enzyme-to-json": { + "optional": true + } + } + }, + "node_modules/@emotion/jest/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@emotion/jest/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, + "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/@emotion/jest/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@emotion/jest/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 + }, + "node_modules/@emotion/jest/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/@emotion/jest/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", @@ -2160,255 +2221,1064 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.6.tgz", - "integrity": "sha512-OE7yIdbDif2kKfrGa+V0vx/B3FJv2L4KnIiLlvtibPyO9UkgO3rzYE0HhpREo2vmJ1Ixq1zwm9/0er+3VOSZJA==", + "node_modules/@esbuild/android-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", + "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", "cpu": [ - "x64" + "arm" ], "dev": true, "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", + "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=12" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "node_modules/@esbuild/android-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", + "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", + "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "cpu": [ + "arm64" + ], "dev": true, - "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" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", + "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", + "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", - "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", - "dependencies": { - "@floating-ui/utils": "^0.1.3" + "node": ">=12" } }, - "node_modules/@floating-ui/dom": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", - "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", + "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", + "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", + "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", + "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", + "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", + "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", + "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", + "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", + "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", + "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", + "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", + "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", + "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", + "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", + "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", + "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, + "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/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 + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.24", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.24.tgz", + "integrity": "sha512-hka/F1zNZ1mNE1cEj0uP6WNltuTZNldHkyqNLYCvfkXT4Ly+pChuUheRl0qccDWFLws9HyVXCNTtlfMmR+iq4w==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", - "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", "dependencies": { - "@floating-ui/dom": "^1.5.1" + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@firebase/app-compat": "0.x" } }, - "node_modules/@floating-ui/utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", - "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.24.tgz", + "integrity": "sha512-+l+vvxXGfPtb1oRQaqbNJWq/QWuC2n2njI/XLNMu2lu5sSLbdDOXHzidr6fbaLOOpESo4Gnagimp5dSnGQnaVg==", + "dependencies": { + "@firebase/app": "0.9.24", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.5.0.tgz", + "integrity": "sha512-GWkG0j/vy7MVK8qN5DLToJ/UdaP7cjJ2ksHeb8oqWZe5KoJJVqz2+Wg2fqH/hLRIXarj6KQH0ZvEOXGvFXFHmA==", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" }, - "engines": { - "node": ">=10.10.0" + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, - "node_modules/@humanwhocodes/module-importer": { + "node_modules/@firebase/auth-compat": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.0.tgz", + "integrity": "sha512-jinbExdXRIDHEcNRQiQbz3qykWl7mvIXKNNLbbBqv04LWrsflhDgX54axfy3RIrZhiD8nD1btwSzJrZkt8jL8A==", + "dependencies": { + "@firebase/auth": "1.5.0", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.1.tgz", + "integrity": "sha512-VAhF7gYwunW4Lw/+RQZvW8dlsf2r0YYqV9W0Gi2Mz8+0TGg1mBJWoUtsHfOr8kPJXhcLsC4eP/z3x6L/Fvjk/A==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.1.tgz", + "integrity": "sha512-ky82yLIboLxtAIWyW/52a6HLMVTzD2kpZlEilVDok73pNPLjkJYowj8iaIWK5nTy7+6Gxt7d00zfjL6zckGdXQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "1.0.1", + "@firebase/database-types": "1.0.0", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", + "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.0.tgz", + "integrity": "sha512-VeDXD9PUjvcWY1tInBOMTIu2pijR3YYy+QAe5cxCo1Q1vW+aA/mpQHhebPM1J6b4Zd1MuUh8xpBRvH9ujKR56A==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, "engines": { - "node": ">=12.22" + "node": ">=10.10.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true + "node_modules/@firebase/firestore-compat": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.23.tgz", + "integrity": "sha512-uUTBiP0GLVBETaOCfB11d33OWB8x1r2G1Xrl0sRK3Va0N5LJ/GRvKVSGfM7VScj+ypeHe8RpdwKoCqLpN1e+uA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-types": "3.0.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, + "node_modules/@firebase/firestore-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.0.tgz", + "integrity": "sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.0.tgz", + "integrity": "sha512-n1PZxKnJ++k73Q8khTPwihlbeKo6emnGzE0hX6QVQJsMq82y/XKmNpw2t/q30VJgwaia3ZXU1fd1C5wHncL+Zg==", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, + "node_modules/@firebase/functions-compat": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.6.tgz", + "integrity": "sha512-RQpO3yuHtnkqLqExuAT2d0u3zh8SDbeBYK5EwSCBKI9mjrFeJRXBnd3pEG+x5SxGJLy56/5pQf73mwt0OuH5yg==", "dependencies": { - "sprintf-js": "~1.0.2" + "@firebase/component": "0.6.4", + "@firebase/functions": "0.11.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", "dependencies": { - "p-locate": "^4.1.0" + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.5.tgz", + "integrity": "sha512-i/rrEI2k9ueFhdIr8KQsptWGskrsnkC5TkohCTrJKz9P0C/PbNv14IAMkwhMJTqIur5VwuOnrUkc9Kdz7awekw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.5.tgz", + "integrity": "sha512-qHQZxm4hEG8/HFU/ls5/bU+rpnlPDoZoqi3ATMeb6s4hovYV9+PfV5I7ZrKV5eFFv47Hx1PWLe5uPnS4e7gMwQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.5", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.0.tgz", + "integrity": "sha512-SGs02Y/mmWBRsqZiYLpv4Sf7uZYZzMWVNN+aKiDqPsFBCzD6hLvGkXz+u98KAl8FqcjgB8BtSu01wm4pm76KHA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.3.tgz", + "integrity": "sha512-WNtjYPhpOA1nKcRu5lIodX0wZtP8pI0VxDJnk6lr+av7QZNS1s6zvr+ERDTve+Qu4Hq/ZnNaf3kBEQR2ccXn6A==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.12.0", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz", + "integrity": "sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==" + }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.10.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "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, - "dependencies": { - "p-limit": "^2.2.0" - }, "engines": { - "node": ">=8" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, "engines": { "node": ">=8" } @@ -2556,21 +3426,6 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@jest/core/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2629,28 +3484,42 @@ "node": ">=8" } }, - "node_modules/@jest/core/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==", + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/core/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@jest/environment": { @@ -2826,6 +3695,49 @@ "node": ">=8" } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2838,6 +3750,12 @@ "node": ">=8" } }, + "node_modules/@jest/reporters/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -3109,6 +4027,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -3422,33 +4350,230 @@ "outvariant": "^1.4.0" } }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "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==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@remix-run/router": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", + "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.0.tgz", + "integrity": "sha512-keHkkWAe7OtdALGoutLY3utvthkGF+Y17ws9LYT8pxMBYXaCoH/8dXS2uzo6e8+sEhY7y/zi5RFo22Dy2lFpDw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.0.tgz", + "integrity": "sha512-y3Kt+34smKQNWilicPbBz/MXEY7QwDzMFNgwEWeYiOhUt9MTWKjHqe3EVkXwT2fR7izOvHpDWZ0o2IyD9SWX7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.0.tgz", + "integrity": "sha512-oLzzxcUIHltHxOCmaXl+pkIlU+uhSxef5HfntW7RsLh1eHm+vJzjD9Oo4oUKso4YuP4PpbFJNlZjJuOrxo8dPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.0.tgz", + "integrity": "sha512-+ANnmjkcOBaV25n0+M0Bere3roeVAnwlKW65qagtuAfIxXF9YxUneRyAn/RDcIdRa7QrjRNJL3jR7T43ObGe8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.0.tgz", + "integrity": "sha512-tBTSIkjSVUyrekddpkAqKOosnj1Fc0ZY0rJL2bIEWPKqlEQk0paORL9pUIlt7lcGJi3LzMIlUGXvtNi1Z6MOCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.0.tgz", + "integrity": "sha512-Ed8uJI3kM11de9S0j67wAV07JUNhbAqIrDYhQBrQW42jGopgheyk/cdcshgGO4fW5Wjq97COCY/BHogdGvKVNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.0.tgz", + "integrity": "sha512-mZoNQ/qK4D7SSY8v6kEsAAyDgznzLLuSFCA3aBHZTmf3HP/dW4tNLTtWh9+LfyO0Z1aUn+ecpT7IQ3WtIg3ViQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.0.tgz", + "integrity": "sha512-rouezFHpwCqdEXsqAfNsTgSWO0FoZ5hKv5p+TGO5KFhyN/dvYXNMqMolOb8BkyKcPqjYRBeT+Z6V3aM26rPaYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.0.tgz", + "integrity": "sha512-Bbm+fyn3S6u51urfj3YnqBXg5vI2jQPncRRELaucmhBVyZkbWClQ1fEsRmdnCPpQOQfkpg9gZArvtMVkOMsh1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "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==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.0.tgz", + "integrity": "sha512-+MRMcyx9L2kTrTUzYmR61+XVsliMG4odFb5UmqtiT8xOfEicfYAGEuF/D1Pww1+uZkYhBqAHpvju7VN+GnC3ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@remix-run/router": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.12.0.tgz", - "integrity": "sha512-2hXv036Bux90e1GXTWSMfNzfDDK8LA8JYEWfyHxzvwdp6GyoWEovKc9cotb3KCKmkdwsIBuFGX7ScTWyiHv7Eg==", - "engines": { - "node": ">=14.0.0" - } + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.0.tgz", + "integrity": "sha512-rxfeE6K6s/Xl2HGeK6cO8SiQq3k/3BYpw7cfhW5Bk2euXNEpuzi2cc7llxx1si1QgwfjNtdRNTGqdBzGlFZGFw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.0.tgz", - "integrity": "sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.0.tgz", + "integrity": "sha512-QqmCsydHS172Y0Kc13bkMXvipbJSvzeglBncJG3LsYJSiPlxYACz7MmJBs4A8l1oU+jfhYEIC/+AUSlvjmiX/g==", "cpu": [ "x64" ], @@ -3483,29 +4608,29 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.8.3.tgz", - "integrity": "sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==", + "version": "5.8.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.8.7.tgz", + "integrity": "sha512-58xOSkxxZK4SGQ/uzX8MDZHLGZCkxlgkPxnfhxUOL2uchnNHyay2UVcR3mQNMgaMwH1e2l+0n+zfS7+UJ/MAJw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-devtools": { - "version": "5.8.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.8.4.tgz", - "integrity": "sha512-F1dRbITNt9tMUoM9WCH8WQ2c54116hv52m/PKK8ZiN/pO2wGVzTZtKuLanF8pFpwmNchjIixcMw/a57HY5ivcw==", + "version": "5.8.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.8.8.tgz", + "integrity": "sha512-JbmXB0+aSYDLd5PtUijD82YqS1xodtgeuaAixuopmcdp4CpEO3DF35Soa88PEpM8Dgrt+piwAhHlbNvrY+4CnA==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.8.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.8.4.tgz", - "integrity": "sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==", + "version": "5.8.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.8.7.tgz", + "integrity": "sha512-RYSSMmkhbJ7tPkf8w+MSRIXQLoUCm7DRnTLDcdf+uampupnriEsob3fVWTt9oaEj+AJWEKeCErDBdZeNcAzURQ==", "dependencies": { - "@tanstack/query-core": "5.8.3" + "@tanstack/query-core": "5.8.7" }, "funding": { "type": "github", @@ -3526,18 +4651,18 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.8.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.8.4.tgz", - "integrity": "sha512-mffs51FJqXU/5rwhbwv393DccL6et7uK2pRLwOcmMrWbPyW8vpxr9oidaghHX4cdVeP/7u5owW9yMpBhBAJfcQ==", + "version": "5.8.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.8.8.tgz", + "integrity": "sha512-oC60LSopUHptugBOJ/N56AwiT4tsc9U0bp3lPWX087faw949XEve8TRfA1e/qUkwQN8/IrzCRjuDOPpHr8gmdw==", "dependencies": { - "@tanstack/query-devtools": "5.8.4" + "@tanstack/query-devtools": "5.8.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.8.4", + "@tanstack/react-query": "^5.8.7", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -3576,15 +4701,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, "node_modules/@testing-library/dom/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3628,38 +4744,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/@testing-library/dom/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3827,9 +4911,9 @@ "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz", - "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -3916,6 +5000,32 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@types/js-levenshtein": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", @@ -3946,10 +5056,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", - "integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==", - "dev": true, + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -3960,14 +5069,14 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { - "version": "15.7.10", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", - "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "18.2.37", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", - "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", + "version": "18.2.39", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", + "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3986,9 +5095,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.15", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", - "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "dependencies": { "@types/react": "*" @@ -4020,14 +5129,14 @@ } }, "node_modules/@types/scheduler": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", - "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/stack-utils": { @@ -4064,16 +5173,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", - "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.0.tgz", + "integrity": "sha512-HTvbSd0JceI2GW5DHS3R9zbarOqjkM9XDR7zL8eCsBUO/eSiHcoNE7kSL5sjGXmVa9fjH5LCfHDXNnH4QLp7tQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/type-utils": "6.11.0", - "@typescript-eslint/utils": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.0", + "@typescript-eslint/type-utils": "6.13.0", + "@typescript-eslint/utils": "6.13.0", + "@typescript-eslint/visitor-keys": "6.13.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4098,16 +5207,49 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.0.tgz", + "integrity": "sha512-VpG+M7GNhHLI/aTDctqAV0XbzB16vf+qDX9DXuMZSe/0bahzDA9AKZB15NDbd+D9M4cDsJvfkbGOA7qiZ/bWJw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.0", + "@typescript-eslint/types": "6.13.0", + "@typescript-eslint/typescript-estree": "6.13.0", + "@typescript-eslint/visitor-keys": "6.13.0", "debug": "^4.3.4" }, "engines": { @@ -4127,13 +5269,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.0.tgz", + "integrity": "sha512-2x0K2/CujsokIv+LN2T0l5FVDMtsCjkUyYtlcY4xxnxLAW+x41LXr16duoicHpGtLhmtN7kqvuFJ3zbz00Ikhw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "6.13.0", + "@typescript-eslint/visitor-keys": "6.13.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4144,13 +5286,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", - "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.0.tgz", + "integrity": "sha512-YHufAmZd/yP2XdoD3YeFEjq+/Tl+myhzv+GJHSOz+ro/NFGS84mIIuLU3pVwUcauSmwlCrVXbBclkn1HfjY0qQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/typescript-estree": "6.13.0", + "@typescript-eslint/utils": "6.13.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -4171,9 +5313,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.0.tgz", + "integrity": "sha512-oXg7DFxx/GmTrKXKKLSoR2rwiutOC7jCQ5nDH5p5VS6cmHE1TcPTaYQ0VPSSUvj7BnNqCgQ/NXcTBxn59pfPTQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4184,13 +5326,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.0.tgz", + "integrity": "sha512-IT4O/YKJDoiy/mPEDsfOfp+473A9GVqXlBKckfrAOuVbTqM8xbc0LuqyFCcgeFWpqu3WjQexolgqN2CuWBYbog==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "6.13.0", + "@typescript-eslint/visitor-keys": "6.13.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4210,18 +5352,51 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", - "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.0.tgz", + "integrity": "sha512-V+txaxARI8yznDkcQ6FNRXxG+T37qT3+2NsDTZ/nKLxv6VfGrRhTnuvxPUxpVuWWr+eVeIxU53PioOXbz8ratQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.0", + "@typescript-eslint/types": "6.13.0", + "@typescript-eslint/typescript-estree": "6.13.0", "semver": "^7.5.4" }, "engines": { @@ -4235,13 +5410,46 @@ "eslint": "^7.0.0 || ^8.0.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.0.tgz", + "integrity": "sha512-UQklteCEMCRoq/1UhKFZsHv5E4dN1wQSzJoxTfABasWk1HgJRdg1xNUve/Kv/Sdymt4x+iEzpESOqRFlQr/9Aw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.13.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4281,6 +5489,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/acorn": { @@ -4352,27 +5561,15 @@ } }, "node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" + "type-fest": "^0.21.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4382,7 +5579,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4412,18 +5608,21 @@ } }, "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 + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, "dependencies": { - "dequal": "^2.0.3" + "deep-equal": "^2.0.5" } }, "node_modules/array-buffer-byte-length": { @@ -4448,6 +5647,21 @@ "node": ">=8" } }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4582,31 +5796,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -4650,15 +5839,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2/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, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", @@ -4684,6 +5864,19 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-transform-import-meta": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.1.tgz", + "integrity": "sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.4.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.0" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -4906,9 +6099,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001563", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz", - "integrity": "sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw==", + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "dev": true, "funding": [ { @@ -4938,6 +6131,14 @@ "node": ">=4" } }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -5034,9 +6235,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -5074,7 +6275,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5088,7 +6288,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5103,7 +6302,6 @@ "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, "dependencies": { "color-name": "~1.1.4" }, @@ -5114,20 +6312,17 @@ "node_modules/cliui/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 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -5136,7 +6331,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5150,7 +6344,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5235,6 +6428,12 @@ "node": ">=16" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5284,14 +6483,6 @@ "node": ">=10" } }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5590,15 +6781,6 @@ "node": ">=0.4.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5660,6 +6842,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { "webidl-conversions": "^7.0.0" @@ -5668,6 +6851,18 @@ "node": ">=12" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5675,9 +6870,15 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.588", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz", - "integrity": "sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==", + "version": "1.4.595", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.595.tgz", + "integrity": "sha512-+ozvXuamBhDOKvMNUQvecxfbyICmIAwS4GpLmR0bsiSBlGnLaOcs2Cj7J8XSbW+YEaN3Xl3ffgpm+srTUWFwFQ==", + "dev": true + }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", "dev": true }, "node_modules/emittery": { @@ -5752,9 +6953,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.6.tgz", - "integrity": "sha512-Xl7dntjA2OEIvpr9j0DVxxnog2fyTGnyVoQXAMQI6eR3mf9zCQds7VIKUDCotDgE/p4ncTgeRqgX8t5d6oP4Gw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", + "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", "dev": true, "hasInstallScript": true, "bin": { @@ -5764,45 +6965,47 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.19.6", - "@esbuild/android-arm64": "0.19.6", - "@esbuild/android-x64": "0.19.6", - "@esbuild/darwin-arm64": "0.19.6", - "@esbuild/darwin-x64": "0.19.6", - "@esbuild/freebsd-arm64": "0.19.6", - "@esbuild/freebsd-x64": "0.19.6", - "@esbuild/linux-arm": "0.19.6", - "@esbuild/linux-arm64": "0.19.6", - "@esbuild/linux-ia32": "0.19.6", - "@esbuild/linux-loong64": "0.19.6", - "@esbuild/linux-mips64el": "0.19.6", - "@esbuild/linux-ppc64": "0.19.6", - "@esbuild/linux-riscv64": "0.19.6", - "@esbuild/linux-s390x": "0.19.6", - "@esbuild/linux-x64": "0.19.6", - "@esbuild/netbsd-x64": "0.19.6", - "@esbuild/openbsd-x64": "0.19.6", - "@esbuild/sunos-x64": "0.19.6", - "@esbuild/win32-arm64": "0.19.6", - "@esbuild/win32-ia32": "0.19.6", - "@esbuild/win32-x64": "0.19.6" + "@esbuild/android-arm": "0.19.8", + "@esbuild/android-arm64": "0.19.8", + "@esbuild/android-x64": "0.19.8", + "@esbuild/darwin-arm64": "0.19.8", + "@esbuild/darwin-x64": "0.19.8", + "@esbuild/freebsd-arm64": "0.19.8", + "@esbuild/freebsd-x64": "0.19.8", + "@esbuild/linux-arm": "0.19.8", + "@esbuild/linux-arm64": "0.19.8", + "@esbuild/linux-ia32": "0.19.8", + "@esbuild/linux-loong64": "0.19.8", + "@esbuild/linux-mips64el": "0.19.8", + "@esbuild/linux-ppc64": "0.19.8", + "@esbuild/linux-riscv64": "0.19.8", + "@esbuild/linux-s390x": "0.19.8", + "@esbuild/linux-x64": "0.19.8", + "@esbuild/netbsd-x64": "0.19.8", + "@esbuild/openbsd-x64": "0.19.8", + "@esbuild/sunos-x64": "0.19.8", + "@esbuild/win32-arm64": "0.19.8", + "@esbuild/win32-ia32": "0.19.8", + "@esbuild/win32-x64": "0.19.8" } }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/escodegen": { @@ -5955,6 +7158,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/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 + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5989,11 +7198,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/eslint/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==", + "node_modules/eslint/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, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=10" }, @@ -6025,6 +7238,48 @@ "node": ">=8" } }, + "node_modules/eslint/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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6037,6 +7292,18 @@ "node": ">=8" } }, + "node_modules/eslint/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -6116,23 +7383,23 @@ "dev": true }, "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=16.17" + "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" @@ -6232,6 +7499,17 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -6256,6 +7534,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6268,6 +7555,32 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6280,25 +7593,87 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", + "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.7.0.tgz", + "integrity": "sha512-t6ZwJQhmq0m7kSssVeu5a1DdmZ0YEBWgNFtpmcvU3PiffWdGVlri6yaX/BK5i4cRtGuQjVPPAEmB90TCpLF5GQ==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.24", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.24", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "1.5.0", + "@firebase/auth-compat": "0.5.0", + "@firebase/database": "1.0.1", + "@firebase/database-compat": "1.0.1", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-compat": "0.3.23", + "@firebase/functions": "0.11.0", + "@firebase/functions-compat": "0.3.6", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.5", + "@firebase/messaging-compat": "0.2.5", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.12.0", + "@firebase/storage-compat": "0.3.3", + "@firebase/util": "1.9.3" } }, "node_modules/flat-cache": { @@ -6400,6 +7775,29 @@ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", "optional": true }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6450,7 +7848,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -6480,17 +7877,67 @@ } }, "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gh-pages": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.0.tgz", + "integrity": "sha512-MdXigvqN3I66Y+tAZsQJMzpBWQOI1snD6BYuECmP+GEdryYMMOQvzn4AConk/+qNg/XIuQhB1xNGrl3Rmj1iow==", + "dev": true, + "dependencies": { + "async": "^3.2.4", + "commander": "^11.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6712,6 +8159,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -6740,12 +8192,12 @@ } }, "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "engines": { - "node": ">=16.17.0" + "node": ">=10.17.0" } }, "node_modules/husky": { @@ -6775,6 +8227,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -6831,6 +8288,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-fresh/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==", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -6910,21 +8375,6 @@ "node": ">=12.0.0" } }, - "node_modules/inquirer/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inquirer/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7010,30 +8460,6 @@ "node": ">=8" } }, - "node_modules/inquirer/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inquirer/node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -7047,12 +8473,6 @@ "node": ">=8" } }, - "node_modules/inquirer/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/inquirer/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7079,18 +8499,6 @@ "node": ">=8" } }, - "node_modules/inquirer/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inquirer/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -7375,12 +8783,12 @@ } }, "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7487,19 +8895,19 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "semver": "^6.3.0" }, "engines": { - "node": ">=10" + "node": ">=8" } }, "node_modules/istanbul-lib-report": { @@ -7519,205 +8927,98 @@ "node_modules/istanbul-lib-report/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=10.17.0" + "node": ">=8" } }, - "node_modules/jest-changed-files/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/istanbul-lib-report/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, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=10" } }, - "node_modules/jest-changed-files/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/jest-changed-files/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { - "path-key": "^3.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/jest-changed-files/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "mimic-fn": "^2.1.0" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/jest-changed-files/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/jest-changed-files/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus": { @@ -7809,6 +9110,32 @@ "node": ">=8" } }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-circus/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8027,6 +9354,32 @@ "node": ">=8" } }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8112,6 +9465,32 @@ "node": ">=8" } }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8210,6 +9589,32 @@ "node": ">=8" } }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-each/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8415,6 +9820,32 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-matcher-utils": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", @@ -8488,6 +9919,32 @@ "node": ">=8" } }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8578,6 +10035,32 @@ "node": ">=8" } }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-message-util/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9027,6 +10510,59 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9039,6 +10575,22 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-svg-transformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jest-svg-transformer/-/jest-svg-transformer-1.0.0.tgz", + "integrity": "sha512-+kD21VthJFHIbI3DZRz+jo4sBOSR1qWEMXhVC28owRMqC5nA+zEiJrHOlj+EqQIztYMouRc1dIjE8SJfFPJUXA==", + "dev": true, + "peerDependencies": { + "jest": ">22", + "react": ">=16" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -9213,6 +10765,32 @@ "node": ">=8" } }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-validate/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9244,21 +10822,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-watcher/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9329,18 +10892,6 @@ "node": ">=8" } }, - "node_modules/jest-watcher/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -9401,12 +10952,13 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "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==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "dependencies": { - "argparse": "^2.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -9509,6 +11061,27 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9599,7 +11172,150 @@ "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" } }, "node_modules/listr2": { @@ -9620,18 +11336,15 @@ } }, "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==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/lodash": { @@ -9639,6 +11352,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9761,6 +11479,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -9788,6 +11521,23 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9849,6 +11599,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -9912,15 +11695,12 @@ } }, "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/min-indent": { @@ -9951,9 +11731,9 @@ "dev": true }, "node_modules/msw": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.0.8.tgz", - "integrity": "sha512-/5nQCotVka62lvubQ3tMfUS3TukyeBwvWyvAthcXvDlXGhkA/85HlEwZyFlJ3ZsPW45Ty+ao0S4oFvuM12R/kA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.0.9.tgz", + "integrity": "sha512-rl3kuGS+wuB4EKyL5nJ5WIoteCIzIgySp5b2hmIQ7fuXFfDfn/yD+E0k/3lDNFnC79tM3cuws6XmJEdPr7y+Zg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -10132,30 +11912,15 @@ } }, "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "path-key": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/nwsapi": { @@ -10234,15 +11999,15 @@ } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=12" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10358,30 +12123,6 @@ "node": ">=8" } }, - "node_modules/ora/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ora/node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -10395,12 +12136,6 @@ "node": ">=8" } }, - "node_modules/ora/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/ora/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10444,15 +12179,27 @@ } }, "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10583,74 +12330,52 @@ "node": ">=0.10" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "pinkie": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "find-up": "^4.0.0" }, "engines": { "node": ">=8" @@ -10709,17 +12434,17 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.3", + "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "react-is": "^17.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -10734,6 +12459,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10762,6 +12493,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -10906,6 +12660,14 @@ "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-intersection-observer": { + "version": "9.5.3", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.3.tgz", + "integrity": "sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10948,11 +12710,11 @@ } }, "node_modules/react-router": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.19.0.tgz", - "integrity": "sha512-0W63PKCZ7+OuQd7Tm+RbkI8kCLmn4GPjDbX61tWljPxWgqTKlEpeQUwPkT1DRjYhF8KSihK0hQpmhU4uxVMcdw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", + "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", "dependencies": { - "@remix-run/router": "1.12.0" + "@remix-run/router": "1.13.0" }, "engines": { "node": ">=14.0.0" @@ -10962,12 +12724,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.19.0.tgz", - "integrity": "sha512-N6dWlcgL2w0U5HZUUqU2wlmOrSb3ighJmtQ438SWbhB1yuLTXQ8yyTBMK3BSvVjp7gBtKurT554nCtMOgxCZmQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", + "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", "dependencies": { - "@remix-run/router": "1.12.0", - "react-router": "6.19.0" + "@remix-run/router": "1.13.0", + "react-router": "6.20.0" }, "engines": { "node": ">=14.0.0" @@ -10993,6 +12755,15 @@ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -11165,7 +12936,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11209,7 +12979,7 @@ "node": ">=8" } }, - "node_modules/resolve-cwd/node_modules/resolve-from": { + "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", @@ -11218,14 +12988,6 @@ "node": ">=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==", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -11251,36 +13013,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -11313,9 +13045,9 @@ } }, "node_modules/rollup": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.0.tgz", - "integrity": "sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.0.tgz", + "integrity": "sha512-R8i5Her4oO1LiMQ3jKf7MUglYV/mhQ5g5OKeld5CnkmPdIGo79FDDQYqPhq/PCVuTQVuxsWgIbDy9F+zdHn80w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -11325,18 +13057,18 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.5.0", - "@rollup/rollup-android-arm64": "4.5.0", - "@rollup/rollup-darwin-arm64": "4.5.0", - "@rollup/rollup-darwin-x64": "4.5.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.5.0", - "@rollup/rollup-linux-arm64-gnu": "4.5.0", - "@rollup/rollup-linux-arm64-musl": "4.5.0", - "@rollup/rollup-linux-x64-gnu": "4.5.0", - "@rollup/rollup-linux-x64-musl": "4.5.0", - "@rollup/rollup-win32-arm64-msvc": "4.5.0", - "@rollup/rollup-win32-ia32-msvc": "4.5.0", - "@rollup/rollup-win32-x64-msvc": "4.5.0", + "@rollup/rollup-android-arm-eabi": "4.6.0", + "@rollup/rollup-android-arm64": "4.6.0", + "@rollup/rollup-darwin-arm64": "4.6.0", + "@rollup/rollup-darwin-x64": "4.6.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.6.0", + "@rollup/rollup-linux-arm64-gnu": "4.6.0", + "@rollup/rollup-linux-arm64-musl": "4.6.0", + "@rollup/rollup-linux-x64-gnu": "4.6.0", + "@rollup/rollup-linux-x64-musl": "4.6.0", + "@rollup/rollup-win32-arm64-msvc": "4.6.0", + "@rollup/rollup-win32-ia32-msvc": "4.6.0", + "@rollup/rollup-win32-x64-msvc": "4.6.0", "fsevents": "~2.3.2" } }, @@ -11391,7 +13123,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11434,38 +13165,14 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -11531,16 +13238,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/sisteransi": { "version": "1.0.5", @@ -11629,6 +13330,15 @@ "node": ">=0.10.0" } }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "dev": true, + "bin": { + "specificity": "bin/specificity" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11767,7 +13477,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11785,15 +13494,12 @@ } }, "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/strip-indent": { @@ -11820,6 +13526,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -11847,12 +13574,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sweetalert2": { + "version": "11.10.1", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.10.1.tgz", + "integrity": "sha512-qu145oBuFfjYr5yZW9OSdG6YmRxDf8CnkgT/sXMfrXGe+asFy2imC2vlaLQ/L/naZ/JZna1MPAY56G4qYM0VUQ==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11952,6 +13731,27 @@ "node": ">=14" } }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -12007,6 +13807,48 @@ } } }, + "node_modules/ts-jest-mock-import-meta": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ts-jest-mock-import-meta/-/ts-jest-mock-import-meta-1.1.0.tgz", + "integrity": "sha512-PTmdWGbDZOPh8vyZUmCTK5PjeD2X3YO25MQPTbm0lMlNFigUDwz3opwXOlsrgD0i5u/MpDX0gdZKoVONxVjVEw==", + "dev": true, + "peerDependencies": { + "ts-jest": ">=20.0.0" + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -12034,9 +13876,9 @@ } }, "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==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "engines": { "node": ">=10" @@ -12058,11 +13900,21 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -12183,9 +14035,9 @@ } }, "node_modules/vite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.0.tgz", - "integrity": "sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", + "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -12237,6 +14089,15 @@ } } }, + "node_modules/vite-plugin-environment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vite-plugin-environment/-/vite-plugin-environment-1.1.3.tgz", + "integrity": "sha512-9LBhB0lx+2lXVBEWxFZC+WO7PKEyE/ykJ7EPWCq95NEcCpblxamTbs5Dm3DLBGzwODpJMEnzQywJU8fw6XGGGA==", + "dev": true, + "peerDependencies": { + "vite": ">= 2.7" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -12284,6 +14145,27 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -12470,12 +14352,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", @@ -12516,7 +14392,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -12528,19 +14403,17 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -12558,7 +14431,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -12566,14 +14438,12 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -12582,7 +14452,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", diff --git a/package.json b/package.json index f4a3c60..2ca4f79 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,27 @@ { "name": "mini-1", - "private": true, "version": "0.0.0", + "private": false, "type": "module", "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "format": "prettier --write --cache .", - "lint": "eslint src/**/*.{ts,tsx} --fix", + "build": "vite build", + "deploy": "gh-pages -d dist", + "dev": "vite --host 0.0.0.0 --port 5174", + "format": "prettier . --write --ignore-path .gitignore && git update-index --again", + "lint": "eslint . --ext .ts, .tsx --fix --ignore-path .gitignore", "prepare": "husky install", - "test": "jest" + "preview": "vite preview", + "test": "jest --watchAll --verbose" + }, + "lint-staged": { + "*.{css,md,json}": [ + "prettier --write" + ], + "*.ts?(x)": [ + "eslint", + "prettier --write" + ], + "package.json": "npx sort-package-json" }, "dependencies": { "@emotion/react": "^11.11.1", @@ -22,6 +33,7 @@ "@types/react-slick": "^0.23.12", "axios": "^1.6.2", "emotion-reset": "^3.0.1", + "firebase": "^10.7.0", "framer-motion": "^10.16.5", "lodash": "^4.17.21", "lottie-react": "^2.4.0", @@ -31,16 +43,20 @@ "react-hook-form": "^7.48.2", "react-infinite-scroll-component": "^6.1.0", "react-infinite-scroller": "^1.2.6", + "react-intersection-observer": "^9.5.3", "react-router-dom": "^6.19.0", "react-slick": "^0.29.0", + "react-spinners": "^0.13.8", "recoil": "^0.7.7", "recoil-persist": "^5.1.0", - "slick-carousel": "^1.8.1" + "slick-carousel": "^1.8.1", + "sweetalert2": "^11.10.1" }, "devDependencies": { "@babel/preset-env": "^7.23.3", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@emotion/jest": "^11.11.0", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", @@ -53,20 +69,27 @@ "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.2.0", "babel-jest": "^29.7.0", + "babel-plugin-transform-import-meta": "^2.2.1", + "dotenv": "^16.3.1", "eslint": "^8.53.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", + "gh-pages": "^6.1.0", "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-svg-transformer": "^1.0.0", "jsdom": "^22.1.0", "lint-staged": "^15.1.0", "msw": "^2.0.8", "prettier": "3.1.0", + "terser": "^5.24.0", "ts-jest": "^29.1.1", + "ts-jest-mock-import-meta": "^1.1.0", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vite-plugin-environment": "^1.1.3" }, "msw": { "workerDirectory": "public" diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 4d3426e..0fe9a8e 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -2,7 +2,7 @@ /* tslint:disable */ /** - * Mock Service Worker (2.0.8). + * Mock Service Worker (2.0.9). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. diff --git a/public/vite.svg b/public/vite.svg index e7b8dfb..44db77b 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index 60d8ee9..853d779 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,8 @@ import { globalStyle } from "./components/Common/Common.styles"; import Search from "./pages/Search"; import SearchList from "./pages/SearchList"; import MyWishs from "./pages/MyPage/MyWishs"; +import DetailList from "./pages/DetailList"; +import { Loader } from "./components/Common/Loader"; const router = createBrowserRouter([ { @@ -26,26 +28,25 @@ const router = createBrowserRouter([ { path: "signin", element: }, { path: "signup", element: }, { path: "detail", element: }, + { path: "detailList", element: }, { path: "cart", element: }, { path: "payment", element: }, - { path: "complete", element: }, + { path: "complete/:id", element: }, { path: "mypage", element: }, { path: "category", element: }, { path: "mypage/myorder", element: }, { path: "search", element: }, { path: "mypage/mywish", element: }, { path: "results", element: }, + { path: "*", element: }, ], }, - { - path: "*", - element: , - }, ]); function App() { return ( <> + diff --git a/src/api/Auth/index.tsx b/src/api/Auth/index.tsx new file mode 100644 index 0000000..80def31 --- /dev/null +++ b/src/api/Auth/index.tsx @@ -0,0 +1,51 @@ +import { baseInstance } from "../../hooks/useAxios"; +import { SignInInputs } from "../../pages/Signin/Signin.types"; +import { Inputs } from "../../pages/Signup/Signup.types"; +import { setSessionStorage } from "../../utils/setSessionStorage"; + +export const fetchSignin = async (data: SignInInputs) => { + const response = await baseInstance.post("members/login", { + email: data.email, + password: data.password, + }); + + const returnData = { + accessToken: response.headers["access-token"], + refreshToken: response.headers["refresh-token"], + message: response.data.message, + memberId: response.data.data.memberId, + nickname: response.data.data.nickname, + }; + + return returnData; +}; + +export const fetchSignup = async (data: Inputs) => { + const response = await baseInstance.post("members/join", { + email: data.email, + password: data.password, + nickname: data.nickname, + phoneNumber: data.phone, + }); + + return response.data; +}; + +export type fetchTokenProps = { + accessToken: string; + refreshToken: string; +}; + +export const fetchToken = async (refreshToken: string) => { + try { + const response = await baseInstance.post("/refresh", { + refreshToken: refreshToken, + }); + + setSessionStorage(response.data.accessToken, response.data.refreshToken); + + return response.data.accessToken; + } catch (error) { + return error; + } +}; diff --git a/src/api/Auth/query.ts b/src/api/Auth/query.ts new file mode 100644 index 0000000..4166912 --- /dev/null +++ b/src/api/Auth/query.ts @@ -0,0 +1,58 @@ +import { useMutation } from "@tanstack/react-query"; +import { SignInInputs } from "../../pages/Signin/Signin.types"; +import { fetchSignin, fetchSignup } from "."; +import { useNavigate } from "react-router-dom"; +import { Inputs } from "../../pages/Signup/Signup.types"; +import { userDataAtom } from "../../store/userDataAtom"; +import { useSetRecoilState } from "recoil"; +import { setSessionStorage } from "../../utils/setSessionStorage"; + +type ErrorType = { + message: string; + data: string; +}; + +export const useLogin = () => { + const router = useNavigate(); + const setUserData = useSetRecoilState(userDataAtom); + + const mutation = useMutation({ + mutationFn: (data: SignInInputs) => fetchSignin(data), + onSuccess: (data) => { + setSessionStorage(data.accessToken, data.refreshToken); + setUserData((prev) => ({ + ...prev, + nickName: data.nickname, + memberId: data.memberId, + })); + alert(data.message); + router("/"); + }, + onError: (error: ErrorType) => { + const { data } = error; + if (data === "아이디 또는 비밀번호가 맞지 않습니다. ") { + alert("아이디 또는 비밀번호가 맞지 않습니다."); + } else { + alert("존재하지 않는 아이디입니다."); + } + }, + }); + + return mutation; +}; + +export const useSignUp = () => { + const router = useNavigate(); + const mutation = useMutation({ + mutationFn: (data: Inputs) => fetchSignup(data), + onSuccess: (data) => { + alert(data.message); + router("/"); + }, + onError: () => { + alert("잘못된 회원가입 정보입니다."); + }, + }); + + return mutation; +}; diff --git a/src/api/Category/index.tsx b/src/api/Category/index.tsx index 2ae8b39..029ef2b 100644 --- a/src/api/Category/index.tsx +++ b/src/api/Category/index.tsx @@ -1,26 +1,34 @@ -import axios from "axios"; import { fetchCatgoryProps } from "../../pages/Category/Category.types"; +import { baseInstance, authInstance } from "../../hooks/useAxios"; +import { isLogined } from "../../utils/isLogined"; export const fetchCatgory = async ({ pageParam, regionUrl, typeUrl, - category_parkingUrl, - category_cookingUrl, - category_pickupUrl, + categoryParkingUrl, + categoryCookingUrl, + categoryPickupUrl, }: fetchCatgoryProps) => { - const data = await axios.get( - `/api/v1/accommodations?page=${pageParam}${regionUrl}${typeUrl}${category_parkingUrl}${category_cookingUrl}${category_pickupUrl}`, + if (isLogined()) { + const data = await authInstance.get( + `accommodations?page=${pageParam}${regionUrl}${typeUrl}${categoryParkingUrl}${categoryCookingUrl}${categoryPickupUrl}`, + ); + return data.data; + } + + const data = await baseInstance.get( + `accommodations?page=${pageParam}${regionUrl}${typeUrl}${categoryParkingUrl}${categoryCookingUrl}${categoryPickupUrl}`, ); return data.data; }; export const postClickHeart = async (accommodationId: string) => { - const data = await axios.post(`/api/vi/wish/${accommodationId}`); + const data = await authInstance.post(`wish/${accommodationId}`); return data.data; }; export const deleteClickHeart = async (accommodationId: string) => { - const data = await axios.delete(`/api/vi/wish/${accommodationId}`); + const data = await authInstance.delete(`wish/${accommodationId}`); return data.data; }; diff --git a/src/api/MyPage/index.ts b/src/api/MyPage/index.ts index 33b1844..b3a287e 100644 --- a/src/api/MyPage/index.ts +++ b/src/api/MyPage/index.ts @@ -1,10 +1,6 @@ -import axios from "axios"; +import { authInstance } from "../../hooks/useAxios"; export const getMyWishList = async () => { - const response = await axios.get("/api/v1/wish", { - headers: { - Authorization: "김토큰", - }, - }); + const response = await authInstance.get("wish"); return response.data; }; diff --git a/src/assets/image/empty.png b/src/assets/image/empty.png new file mode 100644 index 0000000..1ccb65c Binary files /dev/null and b/src/assets/image/empty.png differ diff --git a/src/assets/image/empty_large.png b/src/assets/image/empty_large.png new file mode 100644 index 0000000..5e1c296 Binary files /dev/null and b/src/assets/image/empty_large.png differ diff --git a/src/assets/image/empty_medium.png b/src/assets/image/empty_medium.png new file mode 100644 index 0000000..a9fe388 Binary files /dev/null and b/src/assets/image/empty_medium.png differ diff --git a/src/assets/svg/arrow-down-icon.svg b/src/assets/svg/arrow-down-icon.svg new file mode 100644 index 0000000..05906d8 --- /dev/null +++ b/src/assets/svg/arrow-down-icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/svg/automobile-icon.svg b/src/assets/svg/automobile-icon.svg new file mode 100644 index 0000000..3b87dbd --- /dev/null +++ b/src/assets/svg/automobile-icon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/svg/calendar-icon.svg b/src/assets/svg/calendar-icon.svg new file mode 100644 index 0000000..4a2b4b9 --- /dev/null +++ b/src/assets/svg/calendar-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/cart-icon.svg b/src/assets/svg/cart-icon.svg index fa4e120..7a6dfc5 100644 --- a/src/assets/svg/cart-icon.svg +++ b/src/assets/svg/cart-icon.svg @@ -1,3 +1,13 @@ - - + + + + Layer 1 + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/cooking-icon.svg b/src/assets/svg/cooking-icon.svg new file mode 100644 index 0000000..4b1b49f --- /dev/null +++ b/src/assets/svg/cooking-icon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/svg/github-icon.svg b/src/assets/svg/github-icon.svg new file mode 100644 index 0000000..aa05db9 --- /dev/null +++ b/src/assets/svg/github-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/like-icon.svg b/src/assets/svg/like-icon.svg deleted file mode 100644 index 2cb03c2..0000000 --- a/src/assets/svg/like-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/svg/liked-icon.svg b/src/assets/svg/liked-icon.svg deleted file mode 100644 index 82ec749..0000000 --- a/src/assets/svg/liked-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/svg/logo.svg b/src/assets/svg/logo.svg new file mode 100644 index 0000000..5803e98 --- /dev/null +++ b/src/assets/svg/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/svg/logout-icon.svg b/src/assets/svg/logout-icon.svg new file mode 100644 index 0000000..3b7b7cc --- /dev/null +++ b/src/assets/svg/logout-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/notfound1.svg b/src/assets/svg/notfound1.svg new file mode 100644 index 0000000..fcbd85a --- /dev/null +++ b/src/assets/svg/notfound1.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/notfound2.svg b/src/assets/svg/notfound2.svg new file mode 100644 index 0000000..9fe9342 --- /dev/null +++ b/src/assets/svg/notfound2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/person-icon.svg b/src/assets/svg/person-icon.svg new file mode 100644 index 0000000..5619624 --- /dev/null +++ b/src/assets/svg/person-icon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/svg/pickup-icon.svg b/src/assets/svg/pickup-icon.svg new file mode 100644 index 0000000..7116bf4 --- /dev/null +++ b/src/assets/svg/pickup-icon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/search-icon.svg b/src/assets/svg/search-icon.svg index d4a0d1d..1451c4b 100644 --- a/src/assets/svg/search-icon.svg +++ b/src/assets/svg/search-icon.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/assets/svg/signin-icon.svg b/src/assets/svg/signin-icon.svg new file mode 100644 index 0000000..e855462 --- /dev/null +++ b/src/assets/svg/signin-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/signup-icon.svg b/src/assets/svg/signup-icon.svg index b1eab2a..225e9ae 100644 --- a/src/assets/svg/signup-icon.svg +++ b/src/assets/svg/signup-icon.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/assets/svg/totop-icon.svg b/src/assets/svg/totop-icon.svg index c6fec67..6ee9e81 100644 --- a/src/assets/svg/totop-icon.svg +++ b/src/assets/svg/totop-icon.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/assets/svg/user-icon.svg b/src/assets/svg/user-icon.svg index 36ba387..618d4ee 100644 --- a/src/assets/svg/user-icon.svg +++ b/src/assets/svg/user-icon.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/components/Cart/CartItem/CartItem.styles.ts b/src/components/Cart/CartItem/CartItem.styles.ts index 8174271..6b8ee05 100644 --- a/src/components/Cart/CartItem/CartItem.styles.ts +++ b/src/components/Cart/CartItem/CartItem.styles.ts @@ -21,6 +21,9 @@ export const itemTop = styled.div` margin-bottom: 1rem; font-size: 1.2rem; + + @media (max-width: 768px) { + font-size: 1rem; } `; @@ -33,13 +36,23 @@ export const itemBottom = styled.div` export const Image = styled.div` margin-right: 1.5rem; - background-image: url("https://picsum.photos/200"); + background-image: url("/src/assets/image/empty.png"); background-repeat: no-repeat; background-size: cover; object-fit: cover; width: 8.5rem; height: 8.5rem; + + @media (max-width: 768px) { + width: 8rem; + height: 8rem; + } + + img { + width: 8.5rem; + height: 8.5rem; + } `; export const Info = styled.div` @@ -63,5 +76,13 @@ export const Info = styled.div` font-size: 1.2rem; font-weight: bold; + + @media (max-width: 768px) { + font-size: 1rem; + } + } + + @media (max-width: 768px) { + font-size: 0.9rem; } `; diff --git a/src/components/Cart/CartItem/CartItem.test.tsx b/src/components/Cart/CartItem/CartItem.test.tsx new file mode 100644 index 0000000..c7a2da7 --- /dev/null +++ b/src/components/Cart/CartItem/CartItem.test.tsx @@ -0,0 +1,83 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import CartItem from "."; +import calculateNightCount from "../../../utils/calculateNightCount"; +import formatNumber from "../../../utils/formatNumber"; + +export const testData = { + id: 1, + accommodationName: "라마다 제주시티 호텔", + roomUrl: "", + roomName: "스위트 더블", + price: 210000, + numberOfGuests: 2, + checkInAt: "2023-11-30", + checkOutAt: "2023-12-02", +}; + +const createWrapper = () => { + const queryClient = new QueryClient(); + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +const props = { + item: testData, + cart: [testData], + select: [], + setSelect: () => {}, + index: 0, + estimatedPrice: [testData], + setSelected: () => {}, + setEstimatedPrice: () => {}, + setCart: () => {}, +}; + +describe("장바구니 페이지 개별 아이템 테스트", () => { + test("아이템이 선택된 상태가 아니면 색상이 #ececec 이여야 한다.", async () => { + render(, { + wrapper: createWrapper(), + }); + + const checkbox = screen.getAllByRole("checkbox"); + expect(checkbox.length).toBe(1); + + const checkboxStyle = getComputedStyle(checkbox[0]); + expect(checkboxStyle.borderColor).toBe("#ececec"); + }); + + test("아이템이 선택된 상태면 색상이 #ff5100 이여야 한다.", async () => { + render(, { + wrapper: createWrapper(), + }); + + const checkbox = screen.getAllByRole("checkbox"); + expect(checkbox.length).toBe(1); + + const checkboxStyle = getComputedStyle(checkbox[0]); + expect(checkboxStyle.borderColor).toBe("#ff5100"); + }); + + test("아이템의 정보가 데이터에 맞게 표시되어야 한다.", async () => { + render(, { + wrapper: createWrapper(), + }); + + const name = screen.queryByText(`${testData.accommodationName}`); + const type = screen.queryByText(`: ${testData.roomName}`); + const number = screen.queryByText(`: ${testData.numberOfGuests}명`); + const price = screen.queryByText( + `₩${formatNumber( + testData.price * + calculateNightCount(testData.checkInAt, testData.checkOutAt), + )}`, + ); + + expect(name).toBeInTheDocument(); + expect(type).toBeInTheDocument(); + expect(number).toBeInTheDocument(); + expect(price).toBeInTheDocument(); + }); +}); diff --git a/src/components/Cart/CartItem/CartItems.utils.ts b/src/components/Cart/CartItem/CartItems.utils.ts index 80b6ab9..7634ea8 100644 --- a/src/components/Cart/CartItem/CartItems.utils.ts +++ b/src/components/Cart/CartItem/CartItems.utils.ts @@ -35,9 +35,7 @@ export const handleCheck = ( setSelect(copy); // 예상 구매 내역 상태 변화 - const filtered = estimatedPrice.filter( - (value) => value.room_basket_id !== item.room_basket_id, - ); + const filtered = estimatedPrice.filter((value) => value.id !== item.id); setEstimatedPrice(filtered); // 체크 개수 표시 @@ -49,21 +47,22 @@ export const handeleDelete = ( item: CartItemType, cart: CartItemType[], estimatedPrice: CartItemType[], + index: number, + select: boolean[], + setSelect: React.Dispatch>, setCart: React.Dispatch>, setSelected: React.Dispatch>, setEstimatedPrice: React.Dispatch>, ) => { - const filtered = cart.filter( - (value) => value.room_basket_id !== item.room_basket_id, - ); + const filtered = cart.filter((value) => value.id !== item.id); + + const filteredEst = estimatedPrice.filter((value) => value.id !== item.id); + + const newSelect = select.slice(0, index).concat(select.slice(index + 1)); - const filteredEst = estimatedPrice.filter( - (value) => value.room_basket_id !== item.room_basket_id, - ); + setSelect(newSelect); - if ( - estimatedPrice.find((value) => value.room_basket_id === item.room_basket_id) - ) + if (estimatedPrice.find((value) => value.id === item.id)) setSelected((prev) => prev - 1); setCart(filtered); diff --git a/src/components/Cart/CartItem/index.tsx b/src/components/Cart/CartItem/index.tsx index b657255..bdedf19 100644 --- a/src/components/Cart/CartItem/index.tsx +++ b/src/components/Cart/CartItem/index.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; import exitLogo from "../../../assets/exit.svg"; +import { authInstance } from "../../../hooks/useAxios"; import calculateNightCount from "../../../utils/calculateNightCount"; import formatNumber from "../../../utils/formatNumber"; import Checkbox from "../Checkbox"; import * as Styled from "./CartItem.styles"; import { CartItemProps } from "./CartItem.type"; import { handeleDelete, handleCheck } from "./CartItems.utils"; -import { useDeleteCartItem } from "../../../hooks/useCartFetch"; const CartItem = ({ item, // 해당 아이템에 대한 정보 @@ -19,41 +19,41 @@ const CartItem = ({ setEstimatedPrice, setCart, }: CartItemProps) => { - const deleteCartMutation = useDeleteCartItem(); const [check, setCheck] = useState(select[index]); // 디자인을 위한 체크 상태 여부 // 장바구니에서 해당 상품 제거 const fetch = () => { - deleteCartMutation.mutate( - { room_basket_id: item.room_basket_id }, - { - onSuccess: (responseData) => { - console.log(responseData.data); - handeleDelete( - item, - cart, - estimatedPrice, - setCart, - setSelected, - setEstimatedPrice, - ); - }, - }, - ); + authInstance + .put("/baskets", { + ids: [item.id], + }) + .then((res) => { + console.log(res.data); + handeleDelete( + item, + cart, + estimatedPrice, + index, + select, + setSelect, + setCart, + setSelected, + setEstimatedPrice, + ); + }); }; // 해당 아이템이 체크 여부 지속적인 확인 useEffect(() => { setCheck(select[index]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [select]); + }, [select, index]); return ( 0 ? select[index] : false} onChange={(event) => { handleCheck( event, @@ -68,9 +68,7 @@ const CartItem = ({ ); }} /> - + - + + +

- 방 타입: {item.room_name} + 방 타입: {item.roomName}

- 숙박일: {item.check_in_at} ~ {item.check_out_at} |{" "} - {calculateNightCount(item.check_in_at, item.check_out_at)}박 + 숙박일: {item.checkInAt} ~ {item.checkOutAt} |{" "} + {calculateNightCount(item.checkInAt, item.checkOutAt)}박

- 숙박인원: {item.number_guests}명 + 숙박인원: {item.numberOfGuests}명 +

+

+ ₩ + {formatNumber( + item.price * calculateNightCount(item.checkInAt, item.checkOutAt), + )}

-

₩{formatNumber(item.price)}

diff --git a/src/components/Cart/CartList/CartList.styles.ts b/src/components/Cart/CartList/CartList.styles.ts new file mode 100644 index 0000000..fd784f5 --- /dev/null +++ b/src/components/Cart/CartList/CartList.styles.ts @@ -0,0 +1,87 @@ +import styled from "@emotion/styled"; + +const Wrapper = styled.div` + padding-top: 1rem; + + font-weight: bold; +`; + +export const Empty = styled.div` + flex-grow: 1; +`; + +export const Container = styled.div` + display: flex; + + padding: 0 5rem; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +export const WrapTitle = styled.p` + padding: 1rem 0; +`; + +export const AllSelect = styled.div` + display: flex; + + position: relative; + margin-bottom: 1rem; + + font-size: 0.9rem; + font-weight: normal; + + & > input { + margin-right: 0.5rem; + } + + & > label { + display: flex; + align-items: center; + justify-content: center; + } +`; + +export const CheckBoxWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +export const ListWrapper = styled(Wrapper)` + margin-right: 1rem; + + width: 70%; + + & label { + user-select: none; + } + + @media (max-width: 768px) { + width: 100%; + } +`; + +export const EstimateWrapper = styled(Wrapper)` + width: 30%; + + @media (max-width: 768px) { + width: 100%; + margin-bottom: 2rem; + } +`; + +// @media (max-width: 1200px) { +// grid-template-columns: ${({ isRandomAccomData }) => +// isRandomAccomData ? "repeat(5, auto)" : "repeat(3, auto)"}; +// } + +// @media (max-width: 768px) { +// grid-template-columns: repeat(2, auto); +// } + +// @media (max-width: 480px) { +// grid-template-columns: repeat(1, auto); +// } diff --git a/src/components/Cart/CartList/CartList.test.tsx b/src/components/Cart/CartList/CartList.test.tsx new file mode 100644 index 0000000..21731c9 --- /dev/null +++ b/src/components/Cart/CartList/CartList.test.tsx @@ -0,0 +1,105 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { BrowserRouter } from "react-router-dom"; +import { RecoilRoot } from "recoil"; +import CartList from "."; + +export const testData = [ + { + id: 1, + accommodationName: "라마다 제주시티 호텔", + roomUrl: "", + roomName: "스위트 더블", + price: 210000, + numberOfGuests: 2, + checkInAt: "2023-11-30", + checkOutAt: "2023-12-02", + }, + { + id: 2, + accommodationName: "라마다 제주시티 호텔", + roomUrl: "", + roomName: "스탠다드 트윈", + price: 95000, + numberOfGuests: 2, + checkInAt: "2023-11-26", + checkOutAt: "2023-11-28", + }, +]; + +const createWrapper = () => { + const queryClient = new QueryClient(); + + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); +}; + +const mockResponse = jest.fn(); +Object.defineProperty(window, "location", { + value: { + hash: { + endsWith: mockResponse, + includes: mockResponse, + }, + assign: mockResponse, + }, + writable: true, +}); + +describe("장바구니 페이지 데이터 받아오기 테스트", () => { + const user = userEvent.setup(); + + afterEach(() => { + mockResponse.mockClear(); + }); + test("데이터가 없을 때 텅이라는 문구가 보여야 한다.", () => { + render(, { wrapper: createWrapper() }); + + const textElement = screen.queryByText("텅"); + expect(textElement).toBeInTheDocument(); + }); + + test("데이터가 있을 때 CartItem 컴포넌트가 보여야 한다.", () => { + render(, { wrapper: createWrapper() }); + + testData.forEach((item, i) => { + const textElement = screen.getAllByText(item.accommodationName); + expect(textElement[i]).toBeInTheDocument(); + }); + + const textElement = screen.queryByText( + `전체 선택(${testData.length}/${testData.length})`, + ); + expect(textElement).toBeInTheDocument(); + }); + + test("전체 선택을 누르면 모든 아이템이 선택 되어야 한다.", () => { + render(, { wrapper: createWrapper() }); + + const checkbox = screen.getByRole("checkbox", { + name: `전체 선택(${testData.length}/${testData.length})`, + }); + + user.click(checkbox); + + const allCheckbox = screen.getAllByRole("checkbox"); + + expect(allCheckbox.length).toBe(testData.length + 1); + }); + + test("선택된 아이템은 예상 구매 내역에 포함이 된다.", async () => { + render(, { wrapper: createWrapper() }); + + const allCheckbox = screen.getAllByRole("checkbox"); + + const estimateItems = screen.queryAllByTestId("estimate-item"); + + expect(allCheckbox.length - 1).toBe(estimateItems.length); + }); +}); diff --git a/src/components/Cart/CartList/CartList.types.ts b/src/components/Cart/CartList/CartList.types.ts new file mode 100644 index 0000000..944989e --- /dev/null +++ b/src/components/Cart/CartList/CartList.types.ts @@ -0,0 +1,5 @@ +import { CartItemType } from "../../../types"; + +export interface CartListProps { + data: CartItemType[]; +} diff --git a/src/components/Cart/CartList/CartList.utils.ts b/src/components/Cart/CartList/CartList.utils.ts new file mode 100644 index 0000000..db11694 --- /dev/null +++ b/src/components/Cart/CartList/CartList.utils.ts @@ -0,0 +1,54 @@ +import { ChangeEvent } from "react"; +import { CartItemType } from "../../../types"; + +// 전체 선택 체크박스 클릭 +export const handleAllCheck = ( + event: ChangeEvent, + cart: CartItemType[], + length: number, + setSelected: React.Dispatch>, + setAllSelected: React.Dispatch>, + setEstimatedPrice: React.Dispatch>, + setSelect: React.Dispatch>, +) => { + // 전체 체크에 대한 상태 저장 + setAllSelected(event.target.checked); + if (event.target.checked) { + // 개별 아이템에 대한 체크 여부 모두 true + setSelect(Array.from({ length: cart.length }, () => true)); + + // 예상 구매 내역 상태 저장 + setEstimatedPrice([...cart]); + + // 선택 개수 상태 저장 + setSelected(length); + } else { + // 개별 아이템에 대한 체크 여부 모두 false + setSelect(Array.from({ length: cart.length }, () => false)); + + // 예상 구매 내역 상태 저장 + setEstimatedPrice([]); + + // 선택 개수 상태 저장 + setSelected(0); + } +}; + +export const handleSelectDeleteClick = ( + cart: CartItemType[], + estimatedPrice: CartItemType[], + select: boolean[], + setSelect: React.Dispatch>, + setCart: React.Dispatch>, + setEstimatedPrice: React.Dispatch>, + setSelected: React.Dispatch>, +) => { + const set = new Set(estimatedPrice.map((item) => JSON.stringify(item))); + const filtered = cart.filter((item) => !set.has(JSON.stringify(item))); + const newSelect = select.filter((item) => item === false); + + setSelect(newSelect); + setSelected(0); + setCart(filtered); + setEstimatedPrice([]); +}; diff --git a/src/components/Cart/CartList/index.tsx b/src/components/Cart/CartList/index.tsx new file mode 100644 index 0000000..454ce9a --- /dev/null +++ b/src/components/Cart/CartList/index.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from "react"; +import { CartItemType } from "../../../types"; +import Button from "../../Common/Button"; +import CartItem from "../CartItem"; +import CartZero from "../CartZero"; +import Checkbox from "../Checkbox"; +import Estimate from "../Estimate"; +import * as Styled from "./CartList.styles"; +import { CartListProps } from "./CartList.types"; +import { handleAllCheck, handleSelectDeleteClick } from "./CartList.utils"; +import { authInstance } from "../../../hooks/useAxios"; + +const CartList = ({ data }: CartListProps) => { + const [cart, setCart] = useState([]); // 실제 api로 받을 데이터 + const [selected, setSelected] = useState(0); // 선택된 아이템 개수 + const [allSelected, setAllSelected] = useState(true); // 전체 선택 여부 + const [estimatedPrice, setEstimatedPrice] = useState([]); // 예상 구매 내역 리스트 + const [select, setSelect] = useState([]); // 개별 아이템에 대한 체크 여부 + const [deleteId, setDeleteId] = useState([]); + + // 데이터 저장 + useEffect(() => { + if (data) { + setCart([...data]); + setSelected(data.length); + setEstimatedPrice([...data]); + setSelect(Array.from({ length: data.length }, () => true)); + } + }, [data]); + + useEffect(() => { + const ids = estimatedPrice.map((item) => item.id); + setDeleteId([...ids]); + }, [estimatedPrice]); + + // 장바구니에서 해당 상품 제거 + const fetch = () => { + authInstance + .put("/baskets", { + ids: deleteId, + }) + .then((res) => { + console.log(res.data); + handleSelectDeleteClick( + cart, + estimatedPrice, + select, + setSelect, + setCart, + setEstimatedPrice, + setSelected, + ); + }); + }; + + // 개별 아이템 중 1개라도 체크 해제 시 전체 채크 비활성 + useEffect(() => { + if (select.includes(false)) setAllSelected(false); + }, [select]); + + if (cart.length === 0) { + return ; + } + + return ( + + + 장바구니 목록 + + + + { + handleAllCheck( + event, + cart, + cart.length, + setSelected, + setAllSelected, + setEstimatedPrice, + setSelect, + ); + }} + /> + + + + + + + logo + + + 숙소 둘러보기 + + + + {loggedin ? ( <> - - search - - - cart - {" "} - - mypage - {" "} - - logout - + + + search + + + + + cart + + {" "} + + + mypage + + {" "} + + + logout + + ) : ( <> - - search - - 로그인 회원가입{" "} + + + search + + + + + signup + + {" "} + + + signin + + {" "} )} - + ); }; diff --git a/src/components/Common/Layout.tsx b/src/components/Common/Layout.tsx index e6565d1..54386cf 100644 --- a/src/components/Common/Layout.tsx +++ b/src/components/Common/Layout.tsx @@ -6,7 +6,9 @@ import Footer from "./Footer"; function Layout() { return ( <> -
+ +
+ diff --git a/src/components/Common/Loader.test.tsx b/src/components/Common/Loader.test.tsx new file mode 100644 index 0000000..7b09d51 --- /dev/null +++ b/src/components/Common/Loader.test.tsx @@ -0,0 +1,26 @@ +import { render } from "@testing-library/react"; +import { Loader } from "./Loader"; +import { useIsFetching } from "@tanstack/react-query"; + +jest.mock("@tanstack/react-query", () => ({ + ...jest.requireActual("@tanstack/react-query"), + useIsFetching: jest.fn(), +})); + +describe("로딩 애니메이션 렌더링 테스트", () => { + it("데이터 로딩시 해당 로딩 애니메이션이 출력되어야 한다.", () => { + (useIsFetching as jest.Mock).mockImplementation(() => 1); + + const { getByText } = render(); + + expect(getByText("잠시만 기다려주세요.")).toBeInTheDocument(); + }); + + it("데이터 로딩 중이 아닐 시, 로딩 애니메이션은 출력되지 않는다.", () => { + (useIsFetching as jest.Mock).mockImplementation(() => 0); + + const { queryByText } = render(); + + expect(queryByText("잠시만 기다려주세요.")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Common/Loader.tsx b/src/components/Common/Loader.tsx new file mode 100644 index 0000000..74e96fe --- /dev/null +++ b/src/components/Common/Loader.tsx @@ -0,0 +1,16 @@ +import { PacmanLoader } from "react-spinners"; +import { LoaderContainer, LoaderWrapper } from "./Common.styles"; +import { useIsFetching } from "@tanstack/react-query"; + +export const Loader = () => { + const isLoading = useIsFetching(); + + return isLoading > 0 ? ( + + + +

잠시만 기다려주세요.

+
+
+ ) : null; +}; diff --git a/src/components/Common/Sidebar.test.tsx b/src/components/Common/Sidebar.test.tsx new file mode 100644 index 0000000..a2f6344 --- /dev/null +++ b/src/components/Common/Sidebar.test.tsx @@ -0,0 +1,20 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import Sidebar from "./Sidebar"; + +describe("사이드바 버튼 테스트", () => { + beforeEach(() => { + window.scrollTo = jest.fn(); + }); + + test("TOP 버튼 클릭시 화면 최상단으로 스크롤이 이동되어야 한다", () => { + render(); + + const topButton = screen.getByText("TOP"); + fireEvent.click(topButton); + + expect(window.scrollTo).toHaveBeenCalledWith({ + top: 0, + behavior: "smooth", + }); + }); +}); diff --git a/src/components/Common/Sidebar.tsx b/src/components/Common/Sidebar.tsx index 73ba49d..3630236 100644 --- a/src/components/Common/Sidebar.tsx +++ b/src/components/Common/Sidebar.tsx @@ -12,7 +12,6 @@ function Sidebar() { return ( - 최근 본 상품 TOP diff --git a/src/components/Complete/CompleteMessage/CompleteMessage.styles.ts b/src/components/Complete/CompleteMessage/CompleteMessage.styles.ts index db2e8ac..35dbbcb 100644 --- a/src/components/Complete/CompleteMessage/CompleteMessage.styles.ts +++ b/src/components/Complete/CompleteMessage/CompleteMessage.styles.ts @@ -17,6 +17,10 @@ export const TextWrapper = styled.div` border: 1px solid #e6e6e6; border-radius: 10px; + + @media (max-width: 768px) { + height: 14rem; + } `; export const TextTop = styled.p` @@ -24,6 +28,10 @@ export const TextTop = styled.p` font-size: 1.9rem; font-weight: bold; + + @media (max-width: 768px) { + font-size: 1.4rem; + } `; export const TextMid = styled.p` @@ -36,6 +44,10 @@ export const TextMid = styled.p` color: #ff5100; font-weight: bold; } + + @media (max-width: 768px) { + font-size: 1rem; + } `; export const AmountWrapper = styled.div` diff --git a/src/components/Complete/CompleteMessage/CompleteMessage.test.tsx b/src/components/Complete/CompleteMessage/CompleteMessage.test.tsx new file mode 100644 index 0000000..82eee47 --- /dev/null +++ b/src/components/Complete/CompleteMessage/CompleteMessage.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from "@testing-library/react"; +import CompleteMessage from "."; +import formatNumber from "../../../utils/formatNumber"; +import { leftDateUntilTheTrip } from "./CompleteMessage.utils"; + +const testData = { + payment_id: 1, + total_price: 30000, + total_count: 2, + payment_at: "2023-11-25T12:00:01", + rooms: [ + { + id: 1, + accommodationName: "부산 앙코르 호텔", + roomName: "스위트룸", + price: 300000, + numberOfGuests: 2, + checkInAt: "2023-12-25", + checkOutAt: "2023-12-26", + roomUrl: "", + }, + { + id: 2, + accommodationName: "신라민박", + roomName: "거지존", + price: 100, + numberOfGuests: 1, + checkInAt: "2023-12-25", + checkOutAt: "2023-12-26", + roomUrl: "", + }, + ], +}; + +describe("결제 완료 정보 컴포넌트 테스트", () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + test("첫 번째 여행까지의 남은 일수가 화면에 정확히 표시되어야 한다.", () => { + render( + , + ); + + const dayElement = screen.getByTestId("day").innerHTML; + + expect(dayElement).toBe(leftDateUntilTheTrip(testData.rooms) + "일"); + }); + + test("총 결제 금액이 화면에 정확이 표시되어야 한다.", () => { + render( + , + ); + + const priceElement = screen.queryByText( + `${formatNumber(testData.total_price)}원`, + ); + + expect(priceElement).toBeInTheDocument(); + }); +}); diff --git a/src/components/Complete/CompleteMessage/CompleteMessage.types.ts b/src/components/Complete/CompleteMessage/CompleteMessage.types.ts index 90d1aff..fce43d6 100644 --- a/src/components/Complete/CompleteMessage/CompleteMessage.types.ts +++ b/src/components/Complete/CompleteMessage/CompleteMessage.types.ts @@ -2,5 +2,4 @@ import { CartItemType } from "../../../types"; export interface CompleteMessageProps { data: CartItemType[]; - totalPrice: number; } diff --git a/src/components/Complete/CompleteMessage/CompleteMessage.utils.ts b/src/components/Complete/CompleteMessage/CompleteMessage.utils.ts index 02c809f..fdc4c98 100644 --- a/src/components/Complete/CompleteMessage/CompleteMessage.utils.ts +++ b/src/components/Complete/CompleteMessage/CompleteMessage.utils.ts @@ -6,7 +6,7 @@ export const leftDateUntilTheTrip = (itemList: CartItemType[]) => { let day = Number.MAX_SAFE_INTEGER; itemList.forEach((item) => { - const checkInDate: Date = new Date(item.check_in_at); + const checkInDate: Date = new Date(item.checkInAt); const diff: number = Math.ceil( (checkInDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24), diff --git a/src/components/Complete/CompleteMessage/index.tsx b/src/components/Complete/CompleteMessage/index.tsx index 5812a31..acb3268 100644 --- a/src/components/Complete/CompleteMessage/index.tsx +++ b/src/components/Complete/CompleteMessage/index.tsx @@ -1,15 +1,32 @@ +import { useEffect, useState } from "react"; +import calculateNightCount from "../../../utils/calculateNightCount"; +import calculateTotalPrice from "../../../utils/calculateTotalPrice"; import formatNumber from "../../../utils/formatNumber"; import * as Styled from "./CompleteMessage.styles"; import { CompleteMessageProps } from "./CompleteMessage.types"; import { leftDateUntilTheTrip } from "./CompleteMessage.utils"; -const CompleteMessage = ({ data, totalPrice }: CompleteMessageProps) => { +const CompleteMessage = ({ data }: CompleteMessageProps) => { + const [totalPrice, setTotalPrice] = useState(0); + + useEffect(() => { + const newPrice = data.map((item) => { + return { + ...item, + price: + item.price * calculateNightCount(item.checkInAt, item.checkOutAt), + }; + }); + setTotalPrice(calculateTotalPrice(newPrice)); + }, []); + return ( 결제 완료🎉 - 첫번째 여행까지 {leftDateUntilTheTrip(data)}일{" "} + 첫번째 여행까지{" "} + {leftDateUntilTheTrip(data)}일{" "} 남았습니다. diff --git a/src/components/Complete/PaymentItems/PaymentItems.test.tsx b/src/components/Complete/PaymentItems/PaymentItems.test.tsx new file mode 100644 index 0000000..4a0c9aa --- /dev/null +++ b/src/components/Complete/PaymentItems/PaymentItems.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from "@testing-library/react"; +import PaymentItems from "."; + +const testData = { + payment_id: 1, + total_price: 30000, + total_count: 2, + payment_at: "2023-11-25T12:00:01", + rooms: [ + { + id: 1, + accommodationName: "부산 앙코르 호텔", + roomName: "스위트룸", + price: 300000, + numberOfGuests: 2, + checkInAt: "2023-12-25", + checkOutAt: "2023-12-26", + roomUrl: "", + }, + { + id: 2, + accommodationName: "신라민박", + roomName: "거지존", + price: 100, + numberOfGuests: 1, + checkInAt: "2023-12-25", + checkOutAt: "2023-12-26", + roomUrl: "", + }, + ], +}; + +describe("결제 완료 정보 컴포넌트 테스트", () => { + test("결제 항목의 리스트 개수가 알맞게 화면에 표시되어야 한다..", () => { + render(); + + const listElement = screen.getAllByTestId("payment_list"); + + expect(listElement.length).toBe(testData.rooms.length); + }); +}); diff --git a/src/components/Complete/PaymentItems/index.tsx b/src/components/Complete/PaymentItems/index.tsx index cf538ab..9474843 100644 --- a/src/components/Complete/PaymentItems/index.tsx +++ b/src/components/Complete/PaymentItems/index.tsx @@ -1,20 +1,32 @@ +import Item from "../../Payment/Item"; import * as Styled from "./PaymentItems.styles"; -// import { PaymentItemsProps } from "./PaymentItems.types"; -// import Item from "../../Payment/Item"; +import { PaymentItemsProps } from "./PaymentItems.types"; -const PaymentItems = () => { +const PaymentItems = ({ data }: PaymentItemsProps) => { return ( 결제항목 - {/* + {data.map((item) => { return ( -
- +
+
); })} - */} + ); }; diff --git a/src/components/Detail/ActionButtonGroup/ActionButtonGroup.types.ts b/src/components/Detail/ActionButtonGroup/ActionButtonGroup.types.ts index b2fd73e..6f858d9 100644 --- a/src/components/Detail/ActionButtonGroup/ActionButtonGroup.types.ts +++ b/src/components/Detail/ActionButtonGroup/ActionButtonGroup.types.ts @@ -1,4 +1,15 @@ export interface ActionButtonGroupProps { + checkInAt: string; + checkOutAt: string; + numberGuests: number; + roomDetail: { + id: number; + accommodation_name: string; + room_name: string; + price: number; + stock: number; + room_image_url: string[]; + }; onAddToCart: () => void; - onBuyNow: () => void; + handleBuyNow: () => void; } diff --git a/src/components/Detail/ActionButtonGroup/index.tsx b/src/components/Detail/ActionButtonGroup/index.tsx index c12d6ca..811e3ae 100644 --- a/src/components/Detail/ActionButtonGroup/index.tsx +++ b/src/components/Detail/ActionButtonGroup/index.tsx @@ -1,17 +1,94 @@ -import React from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useSetRecoilState } from "recoil"; +import { usePostOrder, usePostRoomToCart } from "../../../hooks/useDetailFetch"; +import { purchaseState } from "../../../store/purchaseAtom"; import * as Styled from "./ActionButtonGroup.styles"; import { ActionButtonGroupProps } from "./ActionButtonGroup.types"; +import { formatDate } from "../../../utils/formatDate"; +import { useRecoilValue } from "recoil"; +import { userDataAtom } from "../../../store/userDataAtom"; -const ActionButtonGroup: React.FC = ({ - onAddToCart, - onBuyNow, -}) => ( - - - 장바구니 담기 - - 바로 구매하기 - -); +const ActionButtonGroup = ({ + checkInAt, + checkOutAt, + numberGuests, +}: ActionButtonGroupProps) => { + const setPurchase = useSetRecoilState(purchaseState); + const navigate = useNavigate(); + const postRoomToCart = usePostRoomToCart(); + const postOrder = usePostOrder(); + + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const roomId = parseInt(queryParams.get("room-id") || "0", 10); + + const userData = useRecoilValue(userDataAtom); + const isLoggedIn = userData.memberId !== ""; + + const onAddToCart = () => { + const formattedCheckInAt = formatDate(new Date(checkInAt)); + const formattedCheckOutAt = formatDate(new Date(checkOutAt)); + + postRoomToCart.mutate( + { + checkInAt: formattedCheckInAt, + checkOutAt: formattedCheckOutAt, + numberOfGuests: numberGuests, + roomId: roomId, + }, + { + onSuccess: () => { + console.log("장바구니 담기 성공"); + console.log("체크인 날짜:", formattedCheckInAt); + console.log("체크아웃 날짜:", formattedCheckOutAt); + console.log("숙박 인원:", numberGuests); + navigate("/cart"); + }, + onError: (error) => { + console.error("장바구니 추가 실패:", error); + alert("장바구니 추가에 실패했습니다."); + }, + }, + ); + }; + + const handleBuyNow = () => { + const formattedCheckInAt = formatDate(new Date(checkInAt)); + const formattedCheckOutAt = formatDate(new Date(checkOutAt)); + + postOrder.mutate( + { + checkInAt: formattedCheckInAt, + checkOutAt: formattedCheckOutAt, + numberOfGuests: numberGuests, + roomId: roomId, + }, + { + onSuccess: (response) => { + setPurchase((prev) => ({ + ...prev, + order_id: response.data.data, + })), + navigate("/payment"); + }, + onError: (error) => { + console.error("주문 실패:", error); + alert("주문 처리에 실패했습니다. 다시 시도해주세요."); + }, + }, + ); + }; + + return ( + + + 장바구니 담기 + + + 바로 구매하기 + + + ); +}; export default ActionButtonGroup; diff --git a/src/components/Detail/Calendar/Calendar.css b/src/components/Detail/Calendar/Calendar.css index f516b0f..adc41bd 100644 --- a/src/components/Detail/Calendar/Calendar.css +++ b/src/components/Detail/Calendar/Calendar.css @@ -1,14 +1,14 @@ -.react-datepicker { - display: flex; /* flexbox 레이아웃을 사용 */ - justify-content: center; /* 중앙 정렬 */ - align-items: center; /* 수직 중앙 정렬 */ +.react-datepicker-first { + display: flex; + justify-content: center; + align-items: center; position: relative; - width: 40vw; /* 필요에 따라 가로 길이 조정 */ + width: 40vw; border: transparent; border-radius: 15px; } -.react-datepicker__input-container > input { +.react-datepicker-first__input-container > input { position: absolute; text-align: center; color: transparent; @@ -22,18 +22,18 @@ border-radius: 35px; background-color: transparent; } -.react-datepicker__input-container > input:focus { +.react-datepicker-first__input-container > input:focus { outline: white; } -.react-datepicker__header { +.react-datepicker-first__header { margin-top: 20px; margin-left: 20px; background-color: transparent; border: transparent; } -.react-datepicker__close-icon::after { +.react-datepicker-first__close-icon::after { position: fixed; right: 670px; top: 110px; @@ -42,23 +42,23 @@ font-size: 10px; background-color: #a3a2a2; } -.react-datepicker__month-container { +.react-datepicker-first__month-container { min-height: 300px; margin-right: 2vw; } -.react-datepicker__month-container:last-child { +.react-datepicker-first__month-container:last-child { margin-right: 0; } -.react-datepicker__navigation { +.react-datepicker-first__navigation { top: 10px; } -.react-datepicker__navigation--previous { +.react-datepicker-first__navigation--previous { left: 20px; } -.react-datepicker__navigation--next { +.react-datepicker-first__navigation--next { right: 20px; } diff --git a/src/components/Detail/Calendar/Calendar.types.ts b/src/components/Detail/Calendar/Calendar.types.ts index 00b0595..8d256ab 100644 --- a/src/components/Detail/Calendar/Calendar.types.ts +++ b/src/components/Detail/Calendar/Calendar.types.ts @@ -1,3 +1,4 @@ export interface CalendarProps { price: number; + onDateChange: (checkInDate: Date | null, checkOutDate: Date | null) => void; } diff --git a/src/components/Detail/Calendar/index.tsx b/src/components/Detail/Calendar/index.tsx index ac62e0a..f677dde 100644 --- a/src/components/Detail/Calendar/index.tsx +++ b/src/components/Detail/Calendar/index.tsx @@ -1,33 +1,52 @@ -import React, { useState } from "react"; +import { ko } from "date-fns/locale"; +import React from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; -import * as Styled from "./Calendar.styles.ts"; +import { useRecoilState } from "recoil"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom.ts"; +import formatNumber from "../../../utils/formatNumber"; import "./Calendar.css"; -import { ko } from "date-fns/locale"; +import * as Styled from "./Calendar.styles.ts"; import { CalendarProps } from "./Calendar.types.ts"; -const Calendar: React.FC = ({ price }) => { - const [startDate, setStartDate] = useState(new Date()); - const [endDate, setEndDate] = useState(null); +const Calendar: React.FC = ({ price, onDateChange }) => { + const [checkInDate, setCheckInDate] = useRecoilState(checkInDateState); + const [checkOutDate, setCheckOutDate] = useRecoilState(checkOutDateState); - // 날짜 변경 핸들러 - const handleChange = (dates: Date[]) => { + const isValidDate = (date: Date | null) => { + return date instanceof Date && !isNaN(date.getTime()); + }; + + const validCheckInDate = isValidDate(checkInDate) ? checkInDate : null; + const validCheckOutDate = isValidDate(checkOutDate) ? checkOutDate : null; + + const handleChange = (dates: [Date, Date]) => { const [start, end] = dates; - setStartDate(start); - setEndDate(end); + setCheckInDate(start); + setCheckOutDate(end); + + onDateChange(start, end); + + // console.log("체크인 날짜:", formatDate(start)); + // console.log("체크아웃 날짜:", formatDate(end)); }; return ( <> 날짜 선택 - 1박 가격 ₩ {price} + + 1박 가격 ₩ {price !== undefined ? formatNumber(price) : "정보 없음"} + = ({ price }) => { dateFormat="yyyy/MM/dd" isClearable={true} showPopperArrow={false} + className="react-datepicker-first" /> ); diff --git a/src/components/Detail/PriceDisplay/index.test.tsx b/src/components/Detail/PriceDisplay/index.test.tsx new file mode 100644 index 0000000..4108fb1 --- /dev/null +++ b/src/components/Detail/PriceDisplay/index.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import PriceDisplay from "./"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom"; +import calculateNightCount from "../../../utils/calculateNightCount"; +import formatNumber from "../../../utils/formatNumber"; + +describe("PriceDisplay Component", () => { + const mockPricePerNight = 100000; + const mockCheckInDate = new Date("2023-01-01"); + const mockCheckOutDate = new Date("2023-01-03"); + + const setup = () => { + render( + { + set(checkInDateState, mockCheckInDate); + set(checkOutDateState, mockCheckOutDate); + }} + > + + , + ); + }; + + test("총합 가격이 올바르게 계산되어 표시되는지 테스트", () => { + setup(); + + const nightCount = calculateNightCount( + mockCheckInDate.toISOString(), + mockCheckOutDate.toISOString(), + ); + const expectedTotalPrice = nightCount * mockPricePerNight; + + expect( + screen.getByText(`₩ ${formatNumber(expectedTotalPrice)}`), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/Detail/PriceDisplay/index.tsx b/src/components/Detail/PriceDisplay/index.tsx index aa8ddc3..48774e1 100644 --- a/src/components/Detail/PriceDisplay/index.tsx +++ b/src/components/Detail/PriceDisplay/index.tsx @@ -1,11 +1,50 @@ import * as Styled from "./PriceDisplay.styles"; -import { PriceDisplayProps } from "./PriceDisplay.types"; - -const PriceDisplay: React.FC = ({ price }) => ( - - 합계 - ₩ {price} - -); +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom"; +import calculateNightCount from "../../../utils/calculateNightCount"; +import formatNumber from "../../../utils/formatNumber"; +import { purchaseState } from "../../../store/purchaseAtom"; +// import formatDate from "../../../utils/formatDate"; + +const PriceDisplay: React.FC<{ pricePerNight: number }> = ({ + pricePerNight = 0, +}) => { + const checkInDate = useRecoilValue(checkInDateState); + const checkOutDate = useRecoilValue(checkOutDateState); + const setPurchase = useSetRecoilState(purchaseState); + + let totalPrice = pricePerNight; + + if (checkInDate instanceof Date && checkOutDate instanceof Date) { + const formattedCheckInDate = checkInDate.toISOString(); + const formattedCheckOutDate = checkOutDate.toISOString(); + const nightCount = calculateNightCount( + formattedCheckInDate, + formattedCheckOutDate, + ); + totalPrice = nightCount * pricePerNight; + + setPurchase((prev) => ({ + ...prev, + totalPrice: totalPrice, + })); + } + + console.log("체크인 날짜:", checkInDate); + console.log("체크아웃 날짜:", checkOutDate); + // console.log("숙박 일수:", nightCount); + console.log("총 가격:", totalPrice); + console.log("1박당 가격:", pricePerNight); + + return ( + + 합계 + ₩ {formatNumber(totalPrice)} + + ); +}; export default PriceDisplay; diff --git a/src/components/Detail/ProductDetails/ProductDetail.types.ts b/src/components/Detail/ProductDetails/ProductDetail.types.ts index 6637b30..3ad08d6 100644 --- a/src/components/Detail/ProductDetails/ProductDetail.types.ts +++ b/src/components/Detail/ProductDetails/ProductDetail.types.ts @@ -1,4 +1,5 @@ export interface ProductDetailsProps { roomName: string; name: string; + price: number; } diff --git a/src/components/Detail/ProductGallery/ProductGallery.styles.ts b/src/components/Detail/ProductGallery/ProductGallery.styles.ts index 7c932e6..6258051 100644 --- a/src/components/Detail/ProductGallery/ProductGallery.styles.ts +++ b/src/components/Detail/ProductGallery/ProductGallery.styles.ts @@ -20,7 +20,7 @@ export const SlideContainer = styled.div` text-align: center; `; -export const ProductImage = styled("img")` +export const ProductImage = styled.img` margin-left: auto; margin-right: auto; max-width: 75vw; diff --git a/src/components/Detail/ProductGallery/ProductGallery.types.ts b/src/components/Detail/ProductGallery/ProductGallery.types.ts index 860ee97..715041f 100644 --- a/src/components/Detail/ProductGallery/ProductGallery.types.ts +++ b/src/components/Detail/ProductGallery/ProductGallery.types.ts @@ -1,3 +1,3 @@ export interface ProductGalleryProps { - images: string[]; + images: { id: number; imageUrl: string }[]; } diff --git a/src/components/Detail/ProductGallery/index.tsx b/src/components/Detail/ProductGallery/index.tsx index fb98133..18bd17d 100644 --- a/src/components/Detail/ProductGallery/index.tsx +++ b/src/components/Detail/ProductGallery/index.tsx @@ -2,6 +2,7 @@ import * as Styled from "./ProductGallery.styles"; import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import { ProductGalleryProps } from "./ProductGallery.types"; +import Empty from "../../../assets/image/empty_large.png"; const settings = { dots: true, @@ -11,18 +12,30 @@ const settings = { slidesToScroll: 1, }; +// img empty set +const handleError = (e: React.SyntheticEvent) => { + e.currentTarget.src = Empty; +}; + const ProductGallery: React.FC = ({ images }) => { return ( - {images.map((image, index) => ( - - + {images.length > 0 ? ( + images.map(({ id, imageUrl }) => ( + + + + )) + ) : ( + + - ))} + )} ); diff --git a/src/components/Detail/QuantitySelector/QuantitySelector.styles.ts b/src/components/Detail/QuantitySelector/QuantitySelector.styles.ts index 971fca5..fe79424 100644 --- a/src/components/Detail/QuantitySelector/QuantitySelector.styles.ts +++ b/src/components/Detail/QuantitySelector/QuantitySelector.styles.ts @@ -10,8 +10,8 @@ export const SelectorContainer = styled.div` export const InfoContainer = styled.div` display: flex; - flex-direction: row; - align-items: center; + flex-direction: column; + align-items: start; gap: 8px; `; @@ -88,3 +88,18 @@ export const PlusButton = styled(Button)` 40% 2px, 2px 40%; `; +export const PriceLabelContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 40px; +`; +export const InfoText = styled.div` + font-size: 0.8rem; + align-items: center; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.04375rem; + color: #646464; +`; diff --git a/src/components/Detail/QuantitySelector/index.tsx b/src/components/Detail/QuantitySelector/index.tsx index 15db1ef..f0de32a 100644 --- a/src/components/Detail/QuantitySelector/index.tsx +++ b/src/components/Detail/QuantitySelector/index.tsx @@ -5,7 +5,7 @@ import { QuantitySelectorProps } from "./QuantitySelector.types"; const QuantitySelector: React.FC = ({ initialQuantity, onQuantityChange, - price, + capacity, }) => { const [quantity, setQuantity] = useState(initialQuantity); @@ -16,27 +16,35 @@ const QuantitySelector: React.FC = ({ }; const handleIncrease = () => { - const newQuantity = quantity + 1; - setQuantity(newQuantity); - onQuantityChange(newQuantity); + if (quantity < capacity) { + const newQuantity = quantity + 1; + setQuantity(newQuantity); + onQuantityChange(newQuantity); + } }; return ( - {" "} - {/* 새로운 컨테이너 추가 */} - 숙박 인원 선택 - 1인당 가격 ₩{price} + + 숙박 인원 선택 + 최대 {capacity}명 + + + * 숙박 인원은 금액에 반영되지 않습니다 + - {" "} - {/* 새로운 컨테이너 추가 */} - {quantity} - + + = capacity} + > + + + ); diff --git a/src/components/Detail/StockStatusBaner/StockStatusBanner.styles.ts b/src/components/Detail/StockStatusBaner/StockStatusBanner.styles.ts index b70fa5c..63cc172 100644 --- a/src/components/Detail/StockStatusBaner/StockStatusBanner.styles.ts +++ b/src/components/Detail/StockStatusBaner/StockStatusBanner.styles.ts @@ -1,25 +1,25 @@ import styled from "@emotion/styled"; -// interface BannerProps { -// lowStock?: boolean; -// outOfStock?: boolean; -// } +interface BannerProps { + lowStock?: boolean; + outOfStock?: boolean; +} -export const Banner = styled.div` +export const Banner = styled.div` display: inline-block; margin-bottom: 16px; margin-right: 20px; - padding: 4px 8px; + padding: 5px 9px; border-radius: 20px; - font-size: 0.75rem; + font-size: 0.8rem; font-weight: bold; text-align: center; color: white; background-color: #5645d6; cursor: default; - ${(props) => props.lowStock && `background-color: #191554l;`} + ${(props) => props.lowStock && `background-color: #191554;`} ${(props) => props.outOfStock && `background-color: red;`} `; diff --git a/src/components/Detail/StockStatusBaner/StockStatusBanner.types.ts b/src/components/Detail/StockStatusBaner/StockStatusBanner.types.ts index e69de29..0026a7d 100644 --- a/src/components/Detail/StockStatusBaner/StockStatusBanner.types.ts +++ b/src/components/Detail/StockStatusBaner/StockStatusBanner.types.ts @@ -0,0 +1,4 @@ +export interface BannerProps { + outOfStock?: boolean; + lowStock?: boolean; +} diff --git a/src/components/Detail/StockStatusBaner/index.tsx b/src/components/Detail/StockStatusBaner/index.tsx index 15048cd..44a144d 100644 --- a/src/components/Detail/StockStatusBaner/index.tsx +++ b/src/components/Detail/StockStatusBaner/index.tsx @@ -1,17 +1,19 @@ -import React from "react"; import { Banner } from "./StockStatusBanner.styles"; -const StockStatusBanner: React.FC = () => { - const quantity = 3; // 임시 값 +interface StockStatusBannerProps { + inventory: number; +} - const lowStockThreshold = 5; // 임시 값 +const StockStatusBanner: React.FC = ({ inventory }) => { + const lowStockThreshold = 5; - if (quantity <= 0) { + if (inventory <= 0) { return 품절; - } else if (quantity > 0 && quantity <= lowStockThreshold) { + } else if (inventory <= lowStockThreshold) { return 품절 임박; } else { return null; } }; + export default StockStatusBanner; diff --git a/src/components/DetailList/BookingCalendar/BookingCalendar.styles.ts b/src/components/DetailList/BookingCalendar/BookingCalendar.styles.ts new file mode 100644 index 0000000..3c0fe66 --- /dev/null +++ b/src/components/DetailList/BookingCalendar/BookingCalendar.styles.ts @@ -0,0 +1,9 @@ +import styled from "@emotion/styled"; +import "react-datepicker/dist/react-datepicker.css"; + +export const DatePickerWrapper = styled.div` + .react-datepicker { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border: 1px solid #d3d3d3; + } +`; diff --git a/src/components/DetailList/BookingCalendar/BookingCalendar.types.ts b/src/components/DetailList/BookingCalendar/BookingCalendar.types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/DetailList/BookingCalendar/Calendar.css b/src/components/DetailList/BookingCalendar/Calendar.css new file mode 100644 index 0000000..012a0bb --- /dev/null +++ b/src/components/DetailList/BookingCalendar/Calendar.css @@ -0,0 +1,80 @@ +.react-datepicker { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: 40vw; + border: transparent; + border-radius: 10px; + /* box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); */ + /* border: 1px solid #d3d3d3; */ +} + +.react-datepicker-second { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: 40vw; + border: transparent; + border-radius: 15px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border: 1px solid #d3d3d3; +} + +.react-datepicker__input-container > input { + position: absolute; + text-align: center; + color: transparent; + font-size: 13px; + left: 50%; + top: 70px; + transform: translateX(-50%); + width: 200px; + height: 57px; + border: transparent; + border-radius: 35px; + background-color: transparent; +} + +.react-datepicker__input-container > input:focus { + outline: white; +} + +.react-datepicker__header { + margin-top: 20px; + margin-left: 20px; + background-color: transparent; + border: transparent; +} + +.react-datepicker__close-icon::after { + position: fixed; + right: 670px; + top: 110px; + width: 10px; + height: 10px; + font-size: 10px; + background-color: #a3a2a2; + display: none; +} +.react-datepicker__month-container { + min-height: 300px; + margin-right: 2vw; +} + +.react-datepicker__month-container:last-child { + margin-right: 0; +} + +.react-datepicker__navigation { + top: 10px; +} + +.react-datepicker__navigation--previous { + left: 20px; +} + +.react-datepicker__navigation--next { + right: 20px; +} diff --git a/src/components/DetailList/BookingCalendar/index.test.tsx b/src/components/DetailList/BookingCalendar/index.test.tsx new file mode 100644 index 0000000..86f23ca --- /dev/null +++ b/src/components/DetailList/BookingCalendar/index.test.tsx @@ -0,0 +1,20 @@ +// import "@testing-library/jest-dom"; +// import { fireEvent, render, screen } from "@testing-library/react"; +// import { RecoilRoot } from "recoil"; +// import Calendar from "./"; + +// describe("Calendar", () => { +// test("캘린더가 랜더잉 되고 날짜 선택이 되는지 테스트", () => { +// render( +// +// +// , +// ); + +// expect(screen.getByRole("textbox")).toBeInTheDocument(); + +// const dateSelectorButton = screen.getByRole("button"); +// fireEvent.click(dateSelectorButton); +// }); + +// }); diff --git a/src/components/DetailList/BookingCalendar/index.tsx b/src/components/DetailList/BookingCalendar/index.tsx new file mode 100644 index 0000000..490fdea --- /dev/null +++ b/src/components/DetailList/BookingCalendar/index.tsx @@ -0,0 +1,61 @@ +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { ko } from "date-fns/locale"; +import "./Calendar.css"; +import { useRecoilState } from "recoil"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom.ts"; +import { useRef } from "react"; +import DateSelector from "../DateSelector/index.tsx"; +import * as Styled from "./BookingCalendar.styles.ts"; + +const Calendar = () => { + const [checkInDate, setCheckInDate] = useRecoilState(checkInDateState); + const [checkOutDate, setCheckOutDate] = useRecoilState(checkOutDateState); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const datePickerRef = useRef(null); + + const isValidDate = (date: Date | null) => { + return date instanceof Date && !isNaN(date.getTime()); + }; + + const validCheckInDate = isValidDate(checkInDate) ? checkInDate : null; + const validCheckOutDate = isValidDate(checkOutDate) ? checkOutDate : null; + + const handleChange = (dates: [Date | null, Date | null]) => { + const [start, end] = dates; + setCheckInDate(start); + setCheckOutDate(end); + }; + + const handleDateSelectorClick = () => { + if (datePickerRef.current) { + datePickerRef.current.setOpen(true); + } + }; + + return ( + + } + className="react-datepicker-second" + /> + + ); +}; + +export default Calendar; diff --git a/src/components/DetailList/Card/Card.styles.ts b/src/components/DetailList/Card/Card.styles.ts new file mode 100644 index 0000000..00416a1 --- /dev/null +++ b/src/components/DetailList/Card/Card.styles.ts @@ -0,0 +1,44 @@ +import styled from "@emotion/styled"; +import { Box } from "@mui/material"; + +export const Card = styled(Box)<{ isActive: boolean }>` + display: flex; + align-items: center; + justify-content: center; + + width: 20vw; + height: 12vh; + + border-radius: 4px; + padding: 0 20px; + border: ${({ isActive }) => + isActive ? "1px solid #D3D3D3" : "1px solid #E6E6E6"}; +`; + +export const CardContentContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +export const CardIcon = styled.div` + display: flex; + justify-content: center; + + font-size: 4rem; + svg { + width: 480px; + height: 48px; + } +`; + +export const CardTitle = styled.div` + font-size: 1.5rem; + text-align: center; + font-weight: bold; +`; + +export const CardDescription = styled.div<{ isActive: boolean }>` + text-align: center; + color: ${({ isActive }) => (isActive ? "black" : "#E6E6E6")}; +`; diff --git a/src/components/DetailList/Card/Card.types.ts b/src/components/DetailList/Card/Card.types.ts new file mode 100644 index 0000000..cd6f560 --- /dev/null +++ b/src/components/DetailList/Card/Card.types.ts @@ -0,0 +1,5 @@ +export interface CardData { + icon: React.ReactNode; + description: string; + isActive: boolean; +} diff --git a/src/components/DetailList/Card/index.test.tsx b/src/components/DetailList/Card/index.test.tsx new file mode 100644 index 0000000..92fe14c --- /dev/null +++ b/src/components/DetailList/Card/index.test.tsx @@ -0,0 +1,26 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import Card from "./"; + +describe("Card", () => { + const testIcon =
Test Icon
; + test("active 상태가 true일때 카드 스타일 테스트", () => { + const testDescription = "Active Description"; + render( + , + ); + + const descriptionElement = screen.getByText(testDescription); + expect(descriptionElement).toHaveStyle("color: black"); + }); + + test("active 상태가 false일때 카드 스타일 테스트", () => { + const testDescription = "Inactive Description"; + render( + , + ); + + const descriptionElement = screen.getByText(testDescription); + expect(descriptionElement).toHaveStyle("color: #E6E6E6"); + }); +}); diff --git a/src/components/DetailList/Card/index.tsx b/src/components/DetailList/Card/index.tsx new file mode 100644 index 0000000..74b61f1 --- /dev/null +++ b/src/components/DetailList/Card/index.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import * as Styled from "./Card.styles.ts"; +import { CardData } from "./Card.types.ts"; + +const Card: React.FC = ({ icon, description, isActive }) => { + return ( + + + {icon} + {/* */} + + {description} + + + + ); +}; + +export default Card; diff --git a/src/components/DetailList/CardList/CardList.styles.ts b/src/components/DetailList/CardList/CardList.styles.ts new file mode 100644 index 0000000..5ce4ed5 --- /dev/null +++ b/src/components/DetailList/CardList/CardList.styles.ts @@ -0,0 +1,35 @@ +import styled from "@emotion/styled"; + +export const CardTitleText = styled.div` + margin-right: 16px; + // margin-top: 10px; + margin-bottom: 20px; + + min-width: 100px; + + color: #222; + font-size: 1.2rem; + font-style: normal; + font-weight: 600; + line-height: normal; + letter-spacing: -0.15rem; +`; + +export const CardListContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + align-items: stretch; + } +`; + +export const IconImage = styled.img<{ isActive: boolean }>` + width: 8vw; + height: 8vh; + + filter: ${({ isActive }) => (isActive ? "none" : "grayscale(100%)")}; +`; diff --git a/src/components/DetailList/CardList/CardList.types.ts b/src/components/DetailList/CardList/CardList.types.ts new file mode 100644 index 0000000..9951c8f --- /dev/null +++ b/src/components/DetailList/CardList/CardList.types.ts @@ -0,0 +1,5 @@ +export interface CardListProps { + parking: boolean; + cooking: boolean; + pickup: boolean; +} diff --git a/src/components/DetailList/CardList/index.test.tsx b/src/components/DetailList/CardList/index.test.tsx new file mode 100644 index 0000000..3263771 --- /dev/null +++ b/src/components/DetailList/CardList/index.test.tsx @@ -0,0 +1,22 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import CardList from "./"; + +describe("CardList", () => { + test("카드가 텍스트를 랜더링하고 상태에 따라 다른 스타일을 적용한다.", () => { + render(); + + expect(screen.getByText("주차 가능해요")).toBeInTheDocument(); + expect(screen.getByText("취사가 가능해요")).toBeInTheDocument(); + expect(screen.getByText("픽업 서비스가 있어요")).toBeInTheDocument(); + + const parkingIcon = screen.getByAltText("parking-icon"); + expect(parkingIcon).toHaveStyle("filter: none"); + + const cookingIcon = screen.getByAltText("cooking-icon"); + expect(cookingIcon).toHaveStyle("filter: grayscale(100%)"); + + const pickupIcon = screen.getByAltText("pickup-icon"); + expect(pickupIcon).toHaveStyle("filter: none"); + }); +}); diff --git a/src/components/DetailList/CardList/index.tsx b/src/components/DetailList/CardList/index.tsx new file mode 100644 index 0000000..c1e9396 --- /dev/null +++ b/src/components/DetailList/CardList/index.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import Card from "../Card"; +import * as Styled from "./CardList.styles.ts"; +import pickupIcon from "../../../assets/svg/pickup-icon.svg"; +import parkingIcon from "../../../assets/svg/automobile-icon.svg"; +import cookingIcon from "../../../assets/svg/cooking-icon.svg"; +import { CardListProps } from "./CardList.types.ts"; + +const CardList: React.FC = ({ parking, cooking, pickup }) => { + const cardData = [ + { + icon: ( + + ), + description: "주차 가능해요", + isActive: parking, + }, + { + icon: ( + + ), + description: "취사가 가능해요", + isActive: cooking, + }, + { + icon: ( + + ), + description: "픽업 서비스가 있어요", + isActive: pickup, + }, + ]; + + return ( + <> + 우리 숙소는요,{" "} + + {cardData.map((card, index) => ( + + ))} + + + ); +}; + +export default CardList; diff --git a/src/components/DetailList/DateSelector/DateSelector.styles.ts b/src/components/DetailList/DateSelector/DateSelector.styles.ts new file mode 100644 index 0000000..d3a7be9 --- /dev/null +++ b/src/components/DetailList/DateSelector/DateSelector.styles.ts @@ -0,0 +1,33 @@ +import styled from "@emotion/styled"; + +export const DateButton = styled.button` + display: flex; + align-items: center; + gap: 8px; + + padding: 12px 10px; + margin-top: 30px; + + background-color: #ffffff; + border: 1px solid #d3d3d3; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +`; + +export const CalendarIconImage = styled.img` + width: 4vw; + height: 4vh; +`; + +export const ArrowIconImage = styled.img` + margin-left: 10px; + + width: 4vw; + height: 4vh; +`; + +export const Container = styled.span` + display: flex; + align-items: center; +`; diff --git a/src/components/DetailList/DateSelector/DateSelector.types.ts b/src/components/DetailList/DateSelector/DateSelector.types.ts new file mode 100644 index 0000000..a9a478c --- /dev/null +++ b/src/components/DetailList/DateSelector/DateSelector.types.ts @@ -0,0 +1,3 @@ +export interface DateSelectorProps { + onClick: () => void; +} diff --git a/src/components/DetailList/DateSelector/index.test.tsx b/src/components/DetailList/DateSelector/index.test.tsx new file mode 100644 index 0000000..3466fed --- /dev/null +++ b/src/components/DetailList/DateSelector/index.test.tsx @@ -0,0 +1,44 @@ +// import "@testing-library/jest-dom"; +// import { render, screen } from "@testing-library/react"; +// import { RecoilRoot } from "recoil"; +// import { +// checkInDateState, +// checkOutDateState, +// } from "../../../store/checkinCheckOutAtom.ts"; +// import DateSelector from "./"; + +// jest.mock("recoil", () => ({ +// ...jest.requireActual("recoil"), +// useRecoilValue: jest.fn().mockImplementation((state) => { +// if (state === checkInDateState) { +// return "2023-01-01"; +// } else if (state === checkOutDateState) { +// return "2023-01-02"; +// } +// return null; +// }), +// })); + +// describe("DateSelector", () => { +// const mockOnClick = jest.fn(); + +// it("renders the date selector with the correct date range and responds to click events", () => { +// render( +// +// +// , +// ); + +// // 날짜 범위가 정상적으로 표시되는지 확인 +// expect(screen.getByText("2023-01-01~2023-01-02")).toBeInTheDocument(); + +// // 아이콘이 정상적으로 렌더링되는지 확인 +// expect(screen.getByAltText("calendar 아이콘")).toBeInTheDocument(); +// expect(screen.getByAltText("화살표 아이콘")).toBeInTheDocument(); + +// // onClick 이벤트 핸들러가 호출되는지 확인 +// const button = screen.getByRole("button"); +// button.click(); +// expect(mockOnClick).toHaveBeenCalled(); +// }); +// }); diff --git a/src/components/DetailList/DateSelector/index.tsx b/src/components/DetailList/DateSelector/index.tsx new file mode 100644 index 0000000..3161fbf --- /dev/null +++ b/src/components/DetailList/DateSelector/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { useRecoilValue } from "recoil"; +import arrowDownIcon from "../../../assets/svg/arrow-down-icon.svg"; +import calendarIcon from "../../../assets/svg/calendar-icon.svg"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom.ts"; +import { formatDate } from "../../../utils/formatDate.ts"; +import * as Styled from "./DateSelector.styles.ts"; +import { DateSelectorProps } from "./DateSelector.types.ts"; + +const DateSelector = React.forwardRef( + ({ onClick }, ref) => { + const storedCheckInDate = useRecoilValue(checkInDateState); + const storedCheckOutDate = useRecoilValue(checkOutDateState); + + const checkInDate = storedCheckInDate + ? new Date(storedCheckInDate) + : new Date(); + const checkOutDate = storedCheckOutDate + ? new Date(storedCheckOutDate) + : new Date(checkInDate.getTime() + 24 * 60 * 60 * 1000); + + const formattedCheckInDate = formatDate(checkInDate).slice(5); + const formattedCheckOutDate = formatDate(checkOutDate).slice(5); + + const dateRange = `${formattedCheckInDate}~${formattedCheckOutDate}`; + + return ( + + + + {dateRange || "날짜 선택"} + + + + ); + }, +); + +export default DateSelector; diff --git a/src/components/DetailList/ProductImage/ProductImage.styles.ts b/src/components/DetailList/ProductImage/ProductImage.styles.ts new file mode 100644 index 0000000..0f809f5 --- /dev/null +++ b/src/components/DetailList/ProductImage/ProductImage.styles.ts @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; +import { Box } from "@mui/material"; + +export const ImageContainer = styled(Box)` + height: 60vh; + margin-right: 10px; +`; + +export const ProductImage = styled("img")` + // margin-left: auto; + margin-right: 10px; + max-width: 60vw; + max-height: 60vh; +`; diff --git a/src/components/DetailList/ProductImage/ProductImage.types.ts b/src/components/DetailList/ProductImage/ProductImage.types.ts new file mode 100644 index 0000000..38547bd --- /dev/null +++ b/src/components/DetailList/ProductImage/ProductImage.types.ts @@ -0,0 +1,3 @@ +export interface ProductImageProps { + image: string; +} diff --git a/src/components/DetailList/ProductImage/index.test.tsx b/src/components/DetailList/ProductImage/index.test.tsx new file mode 100644 index 0000000..43b3c31 --- /dev/null +++ b/src/components/DetailList/ProductImage/index.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from "@testing-library/react"; +import ProductImage from "./"; + +describe("ProductImage", () => { + it("이미지가 랜더링 되는지 테스트", () => { + const testImageUrl = + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/2c0134f2-6ba3-434c-8dca-6d5831bf6e24"; + render(); + + const imageElement = screen.getByRole("img", { + name: /accommodation 이미지/i, + }); + expect(imageElement).toBeInTheDocument(); + expect(imageElement).toHaveAttribute("src", testImageUrl); + expect(imageElement).toHaveAttribute("alt", "accommodation 이미지"); + }); +}); diff --git a/src/components/DetailList/ProductImage/index.tsx b/src/components/DetailList/ProductImage/index.tsx new file mode 100644 index 0000000..657d223 --- /dev/null +++ b/src/components/DetailList/ProductImage/index.tsx @@ -0,0 +1,21 @@ +import * as Styled from "./ProductImage.styles"; +import { ProductImageProps } from "./ProductImage.types"; +import Empty from "../../../assets/image/empty_large.png"; + +const ProductImage: React.FC = ({ image }) => { + // img empty set + const handleError = (e: React.SyntheticEvent) => { + e.currentTarget.src = Empty; + }; + return ( + + + + ); +}; + +export default ProductImage; diff --git a/src/components/DetailList/ProductInfo/ProductInfo.styles.ts b/src/components/DetailList/ProductInfo/ProductInfo.styles.ts new file mode 100644 index 0000000..177936b --- /dev/null +++ b/src/components/DetailList/ProductInfo/ProductInfo.styles.ts @@ -0,0 +1,61 @@ +import styled from "@emotion/styled"; +import { Box } from "@mui/material"; + +export const DetailsContainer = styled(Box)` + display: flex; + flex-direction: column; + justify-content: space-between; + + margin-top: 10px; + height: 50vh; + + overflow-y: auto; +`; + +export const AddressInfoContainer = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + justify-content: space-between; +`; +export const ContactContainer = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + justify-content: space-between; +`; +export const TimeContainer = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + justify-content: space-between; +`; + +export const InfoTitleText = styled.div` + min-width: 100px; + margin-right: 16px; + color: #222; + font-size: 1rem; + font-style: normal; + font-weight: 600; + line-height: normal; + letter-spacing: -0.15rem; +`; + +export const InfoDetailText = styled.div` + text-align: left; + color: #222; + font-size: 0.95rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.04375rem; +`; + +export const ItemContainer = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + + margin-bottom: 10px; +`; diff --git a/src/components/DetailList/ProductInfo/ProductInfo.types.ts b/src/components/DetailList/ProductInfo/ProductInfo.types.ts new file mode 100644 index 0000000..7903da3 --- /dev/null +++ b/src/components/DetailList/ProductInfo/ProductInfo.types.ts @@ -0,0 +1,8 @@ +export interface ProductInfoProps { + address: string; + infoDetail: string; + phoneNumber: string; + homepage: string; + checkIn: string; + checkOut: string; +} diff --git a/src/components/DetailList/ProductInfo/index.test.tsx b/src/components/DetailList/ProductInfo/index.test.tsx new file mode 100644 index 0000000..e0718ed --- /dev/null +++ b/src/components/DetailList/ProductInfo/index.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from "@testing-library/react"; +import ProductInfo from "./"; + +describe("ProductInfo", () => { + it("텍스트가 잘 랜더링 되는지 테스트", () => { + const address = "경상남도 하동군 화개로 13"; + const infoDetail = "This is a detail for Accommodation1"; + const phoneNumber = "신라스테이"; + const homepage = "123-456-7890"; + const checkIn = "14:00:00"; + const checkOut = "11:00:00"; + + render( + , + ); + + expect(screen.getByText(address)).toBeInTheDocument(); + expect(screen.getByText(infoDetail)).toBeInTheDocument(); + expect(screen.getByText(phoneNumber)).toBeInTheDocument(); + expect(screen.getByText(homepage)).toBeInTheDocument(); + expect(screen.getByText(checkIn)).toBeInTheDocument(); + expect(screen.getByText(checkOut)).toBeInTheDocument(); + }); +}); diff --git a/src/components/DetailList/ProductInfo/index.tsx b/src/components/DetailList/ProductInfo/index.tsx new file mode 100644 index 0000000..dba53a3 --- /dev/null +++ b/src/components/DetailList/ProductInfo/index.tsx @@ -0,0 +1,48 @@ +import * as Styled from "./ProductInfo.styles"; +import { ProductInfoProps } from "./ProductInfo.types"; + +const ProductInfo: React.FC = ({ + address, + infoDetail, + phoneNumber, + homepage, + checkIn, + checkOut, +}) => ( + + + + 주소 + {address} + + + 설명 + {infoDetail} + + + + + + 전화번호 + {phoneNumber} + + + 홈페이지 + {homepage} + + + + + + 체크인 + {checkIn} + + + 체크아웃 + {checkOut} + + + +); + +export default ProductInfo; diff --git a/src/components/DetailList/ProductTitle/ProductTitle.styles.ts b/src/components/DetailList/ProductTitle/ProductTitle.styles.ts new file mode 100644 index 0000000..55838c5 --- /dev/null +++ b/src/components/DetailList/ProductTitle/ProductTitle.styles.ts @@ -0,0 +1,28 @@ +import styled from "@emotion/styled"; +import { Box } from "@mui/material"; + +export const ProductType = styled.div` + margin-bottom: 6px; + + color: #222; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.05rem; +`; + +export const ProductName = styled.div` + margin-bottom: 20px; + + color: #222; + font-size: 1.625rem; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.08125rem; +`; + +export const DetailsContainer = styled(Box)` + padding: 0; +`; diff --git a/src/components/DetailList/ProductTitle/ProductTitle.types.ts b/src/components/DetailList/ProductTitle/ProductTitle.types.ts new file mode 100644 index 0000000..43a260f --- /dev/null +++ b/src/components/DetailList/ProductTitle/ProductTitle.types.ts @@ -0,0 +1,4 @@ +export interface ProductTitleProps { + type: number; + name: string; +} diff --git a/src/components/DetailList/ProductTitle/index.test.tsx b/src/components/DetailList/ProductTitle/index.test.tsx new file mode 100644 index 0000000..6733bda --- /dev/null +++ b/src/components/DetailList/ProductTitle/index.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from "@testing-library/react"; +import ProductTitle from "./"; + +describe("ProductTitled", () => { + it("텍스트가 잘 랜더링 되는지 테스트", () => { + const type = "호텔"; + const name = "신라스테이"; + render(); + + expect(screen.getByText(type)).toBeInTheDocument(); + expect(screen.getByText(name)).toBeInTheDocument(); + }); +}); diff --git a/src/components/DetailList/ProductTitle/index.tsx b/src/components/DetailList/ProductTitle/index.tsx new file mode 100644 index 0000000..4556088 --- /dev/null +++ b/src/components/DetailList/ProductTitle/index.tsx @@ -0,0 +1,24 @@ +import * as Styled from "./ProductTitle.styles"; +import { ProductTitleProps } from "./ProductTitle.types"; + +const typeTexts: { [key: number]: string } = { + 0: "관광호텔", + 1: "콘도미니엄", + 2: "유스호스텔", + 3: "펜션", + 4: "모텔", + 5: "민박", + 6: "게스트하우스", + 7: "홈스테이", + 8: "서비스드레지던스", + 9: "한옥", +}; + +const ProductTitle: React.FC = ({ type, name }) => ( + + {typeTexts[type]} + {name} + +); + +export default ProductTitle; diff --git a/src/components/DetailList/RoomItem/RoomItem.styles.ts b/src/components/DetailList/RoomItem/RoomItem.styles.ts new file mode 100644 index 0000000..04bd4f0 --- /dev/null +++ b/src/components/DetailList/RoomItem/RoomItem.styles.ts @@ -0,0 +1,117 @@ +import styled from "@emotion/styled"; + +export const ItemWrapper = styled.ul` + display: flex; + flex-direction: row; + padding: 1.19rem 0.81rem 1.19rem 1.81rem; + + position: relative; + border-radius: 0.625rem; + border: 1px solid #e6e6e6; + background: #fff; + + cursor: pointer; + &:hover { + border: 1px solid #ff5100; + box-shadow: 0px 0px 20px 0px rgba(255, 81, 0, 0.05); + } +`; + +export const ImageContainer = styled.div` + flex-shrink: 0; + width: 300px; + height: 200px; + + background-color: #f0f0f0; + margin-right: 20px; + border-radius: 0.625rem; + overflow: hidden; +`; + +export const ItemImage = styled.img` + width: 100%; + height: auto; + object-fit: contain; +`; + +export const ItemDetails = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-between; + + padding: 0 20px; +`; + +export const NameCapacityContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + + height: 100%; +`; + +export const ItemName = styled.h3` + color: #333; + font-size: 1.6rem; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.08125rem; +`; + +export const ItemCapacity = styled.span` + color: #646464; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.05rem; +`; + +export const InventoryPriceContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + + margin-top: auto; + + gap: 4px; +`; + +export const ItemInventory = styled.span` + color: #646464; + + font-size: 0.9rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.04688rem; +`; + +export const ItemPrice = styled.span` + font-size: 1.2rem; + color: #333; + font-weight: bold; +`; + +export const CapacityPriceContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; +`; + +export const CapacityContainer = styled.span` + display: flex; + align-items: center; + + margin-top: 20px; +`; + +export const IconImage = styled.img` + width: 2vw; + height: 2vh; +`; diff --git a/src/components/DetailList/RoomItem/RoomItem.types.ts b/src/components/DetailList/RoomItem/RoomItem.types.ts new file mode 100644 index 0000000..ba92b73 --- /dev/null +++ b/src/components/DetailList/RoomItem/RoomItem.types.ts @@ -0,0 +1,11 @@ +export interface RoomItemProps { + id: number; + name: string; + price: number; + capacity: number; + roomImageUrl?: string | undefined; + RoomInventory?: { date: string; inventory: number }[]; + checkInDate: Date | null; + checkOutDate: Date | null; + roomImages: string; +} diff --git a/src/components/DetailList/RoomItem/index.test.tsx b/src/components/DetailList/RoomItem/index.test.tsx new file mode 100644 index 0000000..6cf08cb --- /dev/null +++ b/src/components/DetailList/RoomItem/index.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { createWrapper } from "../../../test/test.utils"; +import RoomItem from "./"; + +const testData = { + id: 1, + name: "그랜드하얏트", + price: 100000, + capacity: 2, + roomImageUrl: "test-room-image.jpg", + RoomInventory: [{ date: "2023-05-01", inventory: 5 }], + checkInDate: new Date("2023-05-01"), + checkOutDate: new Date("2023-05-02"), + roomImages: "test-room-image.jpg", +}; + +describe("RoomItem Test", () => { + test("객실 아이템이 렌더링되는지 테스트", () => { + if ("IntersectionObserver" in window) { + render( + , + ); + } + }); + + test("객실 리스트 아이템을 누르면 해당 객실 상세페이지로 이동.", () => { + if ("IntersectionObserver" in window) { + const router = jest.fn(); + const user = userEvent.setup(); + const wrapper = createWrapper(); + + render( + , + { wrapper }, + ); + + const item = screen.getByTestId("individual-item"); + user.click(item); + + expect(router).toHaveBeenCalledWith(testData.id); + } + }); +}); diff --git a/src/components/DetailList/RoomItem/index.tsx b/src/components/DetailList/RoomItem/index.tsx new file mode 100644 index 0000000..f1954ec --- /dev/null +++ b/src/components/DetailList/RoomItem/index.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import * as Styled from "./RoomItem.styles"; +import formatNumber from "../../../utils/formatNumber"; +import { RoomItemProps } from "./RoomItem.types"; +import personIcon from "../../../assets/svg/person-icon.svg"; +// import { formatDateToYYMMDD } from "../../../utils/formatDate"; +import { formatDate } from "../../../utils/formatDate"; +import { useLocation, useNavigate } from "react-router-dom"; +import Empty from "../../../assets/image/empty_medium.png"; + +const RoomItem: React.FC< + RoomItemProps & { checkInDate: Date; checkOutDate: Date } +> = ({ id, name, price, capacity, roomImages, RoomInventory, checkInDate }) => { + const location = useLocation(); + const navigate = useNavigate(); + const queryParams = new URLSearchParams(location.search); + const accommodationId = queryParams.get("accommodation-id"); + + let remainingInventory = "데이터 없음"; + + if (checkInDate && RoomInventory) { + const checkInDateString = formatDate(checkInDate); + + const inventoryData = RoomInventory.find( + (inv) => inv.date === checkInDateString, + ); + + remainingInventory = inventoryData + ? `${inventoryData.inventory}개` + : "확인 불가"; + } + + const handleRoomClick = () => { + navigate(`/detail?accommodation-id=${accommodationId}&room-id=${id}`); + }; + + //img empty set + const handleError = (e: React.SyntheticEvent) => { + e.currentTarget.src = Empty; + }; + + return ( + + + + + + {name} + + + + 최대 인원: {capacity}명 + + + + 남은 객실: {remainingInventory} + + ₩{formatNumber(price)} + + + + + ); +}; + +export default RoomItem; diff --git a/src/components/DetailList/RoomList/RoomList.styles.ts b/src/components/DetailList/RoomList/RoomList.styles.ts new file mode 100644 index 0000000..fdd5484 --- /dev/null +++ b/src/components/DetailList/RoomList/RoomList.styles.ts @@ -0,0 +1,10 @@ +import styled from "@emotion/styled"; + +export const RoomList = styled.li` + list-style: none; + margin-top: 2rem; + + display: flex; + flex-direction: column; + gap: 1.5rem; +`; diff --git a/src/components/DetailList/RoomList/RoomList.types.ts b/src/components/DetailList/RoomList/RoomList.types.ts new file mode 100644 index 0000000..0fa9b9c --- /dev/null +++ b/src/components/DetailList/RoomList/RoomList.types.ts @@ -0,0 +1,9 @@ +export interface RoomType { + id: number; + name: string; + price: number; + capacity: number; + roomImageUrl?: string | undefined; + roomInventories?: { date: string; inventory: number }[]; + roomImages: { id: number; imageUrl: string }[]; +} diff --git a/src/components/DetailList/RoomList/RoomList.utils.ts b/src/components/DetailList/RoomList/RoomList.utils.ts new file mode 100644 index 0000000..c781463 --- /dev/null +++ b/src/components/DetailList/RoomList/RoomList.utils.ts @@ -0,0 +1,29 @@ +import { RoomType } from "./RoomList.types"; + +const isInventoryAvailable = ( + room: RoomType, + checkInDate: Date, + checkOutDate: Date, +) => { + if (!checkInDate || !checkOutDate) return true; + + const startDate = new Date(checkInDate); + const endDate = new Date(checkOutDate); + + if (room.roomInventories) { + for (const inventoryInfo of room.roomInventories) { + const inventoryDate = new Date(inventoryInfo.date + "T00:00:00.000Z"); + + if ( + inventoryDate >= startDate && + inventoryDate < endDate && + inventoryInfo.inventory <= 0 + ) { + return false; + } + } + } + return true; +}; + +export default isInventoryAvailable; diff --git a/src/components/DetailList/RoomList/index.test.tsx b/src/components/DetailList/RoomList/index.test.tsx new file mode 100644 index 0000000..d8c5e6f --- /dev/null +++ b/src/components/DetailList/RoomList/index.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import RoomList from "./"; +import isInventoryAvailable from "./RoomList.utils"; +import { BrowserRouter } from "react-router-dom"; + +const accommodationDetail = { + rooms: [ + { + id: 1, + name: "스탠다드 룸", + price: 150000, + capacity: 2, + roomImages: [ + { id: 101, imageUrl: "image1.jpg" }, + { id: 102, imageUrl: "image2.jpg" }, + ], + RoomInventory: [ + { date: "2023-01-01", inventory: 5 }, + { date: "2023-01-02", inventory: 4 }, + ], + roomImageUrl: "standard-room.jpg", + }, + ], +}; +describe("RoomList Test", () => { + test("필터링된 룸 리스트가 렌더링되는지 테스트", () => { + const testCheckInDate = new Date("2023-01-01"); + const testCheckOutDate = new Date("2023-01-02"); + + render( + + + + + , + ); + + accommodationDetail.rooms.forEach((room) => { + if (isInventoryAvailable(room, testCheckInDate, testCheckOutDate)) { + expect(screen.getByText(room.name)).toBeInTheDocument(); + } else { + expect(screen.queryByText(room.name)).toBeNull(); + } + }); + }); +}); diff --git a/src/components/DetailList/RoomList/index.tsx b/src/components/DetailList/RoomList/index.tsx new file mode 100644 index 0000000..1de4d03 --- /dev/null +++ b/src/components/DetailList/RoomList/index.tsx @@ -0,0 +1,42 @@ +import { useRecoilValue } from "recoil"; +import { + checkInDateState, + checkOutDateState, +} from "../../../store/checkinCheckOutAtom"; +import RoomItem from "../RoomItem"; +import * as Styled from "./RoomList.styles"; +import isInventoryAvailable from "./RoomList.utils"; +import { RoomType } from "./RoomList.types"; + +const RoomList = ({ rooms }: { rooms: RoomType[] }) => { + const checkInDate = useRecoilValue(checkInDateState) || new Date(); + const checkOutDate = useRecoilValue(checkOutDateState) || new Date(); + + const filteredRooms = rooms.filter((room) => + isInventoryAvailable(room, checkInDate, checkOutDate), + ); + + return ( + + {filteredRooms.map((room) => + checkInDate && checkOutDate ? ( + 0 ? room.roomImages[0].imageUrl : "" + } + checkInDate={checkInDate} + checkOutDate={checkOutDate} + /> + ) : null, + )} + + ); +}; + +export default RoomList; diff --git a/src/components/Main/AccommodationList/AccommodationList.styles.ts b/src/components/Main/AccommodationList/AccommodationList.styles.ts new file mode 100644 index 0000000..2f55b5d --- /dev/null +++ b/src/components/Main/AccommodationList/AccommodationList.styles.ts @@ -0,0 +1,149 @@ +import styled from "@emotion/styled"; +import { motion } from "framer-motion"; +import { Link } from "react-router-dom"; + +interface RandomAccomDataProps { + isRandomAccomData?: boolean; +} + +export const Title = styled.h3` + margin: 1.5rem; + + font-size: 1.3rem; + font-weight: 700; + text-align: center; +`; + +export const AccomList = styled(motion.div)` + display: grid; + grid-template-columns: ${({ isRandomAccomData }) => + isRandomAccomData ? "repeat(5, auto)" : "repeat(3, auto)"}; + justify-content: center; + align-items: center; + + gap: 1.2rem; + margin-bottom: 3rem; + + @media (max-width: 1200px) { + grid-template-columns: ${({ isRandomAccomData }) => + isRandomAccomData ? "repeat(5, auto)" : "repeat(3, auto)"}; + } + + @media (max-width: 768px) { + grid-template-columns: repeat(2, auto); + } + + @media (max-width: 480px) { + grid-template-columns: repeat(1, auto); + } +`; + +export const ItemContainer = styled.div` + padding: 0.8rem; + + width: 13rem; + height: 16rem; + border: 1px solid #e6e6e6; + border-radius: 0.625rem; + + transition: all 0.1s ease; + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08); + + &:hover { + border: 1px solid #ff5100; + color: #ff5100; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transform: scale(1.05); + } + + @media (max-width: 768px) { + width: 20rem; + height: 18rem; + } + + @media (max-width: 480px) { + width: 25rem; + height: 22rem; + } +`; + +export const ItemLink = styled(Link)``; + +export const ItemPicture = styled.div` + position: relative; + + background-color: white; + + overflow: hidden; + + img { + width: 13rem; + height: 13rem; + } + + @media (max-width: 768px) { + img { + width: 100%; + height: 15rem; + } + } + + @media (max-width: 480px) { + img { + width: 100%; + height: 19rem; + } + } +`; + +export const ItemInfo = styled.div` + display: flex; + flex-direction: row; + align-items: center; + + margin-top: 0.5rem; + width: 100%; + + h3 { + cursor: pointer; + } + + .item-name { + font-size: 0.9rem; + } + .item-price { + font-size: 0.88rem; + font-weight: 700; + } + + img { + width: 2rem; + height: 2rem; + cursor: pointer; + } +`; + +export const ItemInfoFirstColumn = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + justify-content: center; + align-items: flex-start; + + gap: 0.4rem; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const ItemInfoSecondColumn = styled.div` + display: flex; + flex-shrink: 0; + flex-direction: column; + align-items: center; + + gap: 0.2rem; + + font-size: 0.8rem; +`; diff --git a/src/components/Main/AccommodationList/AccommodationList.types.ts b/src/components/Main/AccommodationList/AccommodationList.types.ts new file mode 100644 index 0000000..9e5241c --- /dev/null +++ b/src/components/Main/AccommodationList/AccommodationList.types.ts @@ -0,0 +1,29 @@ +import { ReactNode } from "react"; + +export type AccommodationListProps = { + accommodations?: AccommodationData[]; + title: ReactNode; + isRandomAccomData?: boolean; +}; + +export type AccommodationData = { + id: number; + name: string; + type?: number; + address?: string; + phoneNumber?: string; + homepage?: string; + infoDetail?: string; + thumbnailUrl: string; + categoryParking: boolean; + categoryCooking: boolean; + categoryPickup: boolean; + categoryAmenities?: string; + categoryDiningArea?: string; + checkIn?: string; + checkOut?: string; + wishCount: number; + isWish: boolean; + lowest_price: number; + viewCount: number; +}; diff --git a/src/components/Main/AccommodationList/index.test.tsx b/src/components/Main/AccommodationList/index.test.tsx new file mode 100644 index 0000000..775dfed --- /dev/null +++ b/src/components/Main/AccommodationList/index.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; + +jest.mock(".", () => ({ + AccommodationList: ({ title }: { title: string }) => ( +
{title} - Mock Component
+ ), +})); + +describe("메인페이지 숙소 목록 렌더링 테스트", () => { + test("AccommodationList 컴포넌트가 정상적으로 렌더링되어야 한다", () => { + const { AccommodationList } = require("."); + + render( + + + , + ); + + expect(screen.getByText("Test Title - Mock Component")).toBeInTheDocument(); + }); +}); diff --git a/src/components/Main/AccommodationList/index.tsx b/src/components/Main/AccommodationList/index.tsx new file mode 100644 index 0000000..e751ea8 --- /dev/null +++ b/src/components/Main/AccommodationList/index.tsx @@ -0,0 +1,73 @@ +import * as Styled from "./AccommodationList.styles"; +import HeartClick from "../../Category/HeartClick"; +import { + AccommodationData, + AccommodationListProps, +} from "./AccommodationList.types"; +import { useRef } from "react"; +import { useInView } from "framer-motion"; +import { random } from "lodash"; +import formatNumber from "../../../utils/formatNumber"; +import Empty from "../../../assets/image/empty.png"; + +export const AccommodationList = ({ + accommodations, + title, + isRandomAccomData, +}: AccommodationListProps) => { + const ref = useRef(null); + const isInView = useInView(ref, { once: true }); + + //img empty set + const handleError = (e: React.SyntheticEvent) => { + e.currentTarget.src = Empty; + }; + + return ( + <> + {title} + + {accommodations?.map((item: AccommodationData) => ( + + + + {item.name} + + + + +

{item.name}

+

+ ₩{item.lowest_price && formatNumber(item.lowest_price)}원 + 부터 +

+
+ + + +
+
+
+ ))} +
+ + ); +}; diff --git a/src/components/MyPage/Header/index.tsx b/src/components/MyPage/Header/index.tsx index 3d38b01..fe2b663 100644 --- a/src/components/MyPage/Header/index.tsx +++ b/src/components/MyPage/Header/index.tsx @@ -1,15 +1,17 @@ import * as Styled from "./Header.styles"; +import { userDataAtom } from "../../../store/userDataAtom"; +import { useRecoilState } from "recoil"; -const index = () => { +const Header = () => { + const userData = useRecoilState(userDataAtom); return ( - test1234 + {userData[0].nickName} 님 안녕하세요! - 탈퇴하기 ); }; -export default index; +export default Header; diff --git a/src/components/MyPage/MyList/index.tsx b/src/components/MyPage/MyList/index.tsx index 5e9494d..e99b0f3 100644 --- a/src/components/MyPage/MyList/index.tsx +++ b/src/components/MyPage/MyList/index.tsx @@ -9,29 +9,26 @@ const index = () => { 💵 결제 내역 - 3 - + 화살표 🛒 장바구니 - 3 - + 화살표 + + + + + + ❤️ 찜한 목록 + + 화살표 - {/* */} - - - ❤️ 찜한 목록 - 3 - - - - {/* */} ); }; diff --git a/src/components/MyPage/MyOrder/Header/index.tsx b/src/components/MyPage/MyOrder/Header/index.tsx index a13dcb4..3dbab71 100644 --- a/src/components/MyPage/MyOrder/Header/index.tsx +++ b/src/components/MyPage/MyOrder/Header/index.tsx @@ -1,11 +1,16 @@ import { Link } from "react-router-dom"; import * as Styled from "./Header.styles"; -import { bookedList } from "../../../../mock/myPageData"; +import { useGetMyOrder } from "../../../../hooks/usePayment"; -const index = () => { +const Header = () => { + const { data } = useGetMyOrder(); // 결제 이력 데이터 요청 return ( - 결제내역 {bookedList.length} + {data?.data.data ? ( + 결제내역 {data?.data.data.length} + ) : ( +
+ )} 돌아가기 @@ -13,4 +18,4 @@ const index = () => { ); }; -export default index; +export default Header; diff --git a/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.styles.ts b/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.styles.ts index 254abb9..6a796a2 100644 --- a/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.styles.ts +++ b/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.styles.ts @@ -8,12 +8,7 @@ export const ItemWrapper = styled.ul` display: flex; flex-direction: column; padding: 1.19rem 0.81rem 2.19rem 1.81rem; - - cursor: pointer; - &:hover { - border: 1px solid #ff5100; - box-shadow: 0px 0px 20px 0px rgba(255, 81, 0, 0.05); - } + margin-bottom: 1.5rem; `; export const ItemTitle = styled.p` @@ -46,10 +41,20 @@ export const Title = styled.span` `; export const ItemImg = styled.div` - width: 8.25rem; - height: 8.5625rem; - background: #d9d9d9; - margin-right: 1.81rem; + position: relative; + + background-color: white; + + background-image: url("/src/assets/image/empty.png"); + background-repeat: no-repeat; + background-size: cover; + object-fit: cover; + margin-right: 2rem; + + img { + width: 10rem; + height: 10rem; + } `; export const Content = styled.span``; diff --git a/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.types.ts b/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.types.ts index ec69522..6a412ba 100644 --- a/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.types.ts +++ b/src/components/MyPage/MyOrder/MyOrderItem/MyOrder.types.ts @@ -4,7 +4,7 @@ export interface MyOrderItemProps { type: string; checkIn: string; checkOut: string; - guests: number; + numberOfGuests: number; price: number; - paymentAt: string; + roomUrl: string; } diff --git a/src/components/MyPage/MyOrder/MyOrderItem/index.tsx b/src/components/MyPage/MyOrder/MyOrderItem/index.tsx index f58228c..9e0ae5f 100644 --- a/src/components/MyPage/MyOrder/MyOrderItem/index.tsx +++ b/src/components/MyPage/MyOrder/MyOrderItem/index.tsx @@ -4,27 +4,22 @@ import calculateNightCount from "../../../../utils/calculateNightCount"; import formatNumber from "../../../../utils/formatNumber"; const index = ({ - id, name, type, checkIn, checkOut, - guests, + numberOfGuests, price, - paymentAt, + roomUrl, }: MyOrderItemProps) => { return ( {name} - + + + - - 주문정보: - - {id}, {paymentAt} - - 방 타입: {type} @@ -41,13 +36,15 @@ const index = ({ 숙박인원: - {guests} + {numberOfGuests} - {formatNumber(price)} + + {formatNumber(price * calculateNightCount(checkIn, checkOut))} + ); diff --git a/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.styles.ts b/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.styles.ts new file mode 100644 index 0000000..fa8dad7 --- /dev/null +++ b/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.styles.ts @@ -0,0 +1,41 @@ +import styled from "@emotion/styled"; + +export const MyOrderItemWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 0.3rem; + border: 1px solid #e6e6e6; + border-radius: 1rem; + padding: 2rem; + margin-top: 2rem; + transition: 0.5s; + + &:hover { + border: 1px solid #ff5100; + box-shadow: 0px 0px 20px 0px rgba(255, 81, 0, 0.05); + } +`; + +export const ItemWrapper = styled.div` + height: 19rem; + overflow: auto; + padding-top: 1rem; + border-bottom: 1px solid #e6e6e6; + padding: 1.5rem; +`; + +export const Header = styled.div` + border-bottom: 1px solid #e6e6e6; + padding: 0rem 0 0.94rem 0; + + display: flex; + justify-content: space-between; + + color: #ff5100; + font-weight: 700; + letter-spacing: -0.05rem; +`; + +export const OrderInfo = styled.span``; + +export const OrderTotalPrice = styled.span``; diff --git a/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.types.ts b/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.types.ts new file mode 100644 index 0000000..5b814f3 --- /dev/null +++ b/src/components/MyPage/MyOrder/MyOrderItemWrapper/MyOrderItemWrapper.types.ts @@ -0,0 +1,10 @@ +export interface MyOrderItemWrapperProps { + id: number; + accommodationName: string; + roomName: string; + price: number; + numberOfGuests: number; + checkInAt: string; + checkOutAt: string; + roomUrl: string; +} diff --git a/src/components/MyPage/MyOrder/MyOrderItemWrapper/index.tsx b/src/components/MyPage/MyOrder/MyOrderItemWrapper/index.tsx new file mode 100644 index 0000000..85befcf --- /dev/null +++ b/src/components/MyPage/MyOrder/MyOrderItemWrapper/index.tsx @@ -0,0 +1,43 @@ +import * as Styled from "./MyOrderItemWrapper.styles"; +import MyOrderItem from "../MyOrderItem/index"; +import { MyOrderListProps } from "../MyOrderList/MyOrderList.type"; +import { MyOrderItemWrapperProps } from "./MyOrderItemWrapper.types"; +import formatNumber from "../../../../utils/formatNumber"; + +const MyOrderItemWrapper = ({ + id, + totalPrice, + totalCount, + paymentAt, + rooms, +}: MyOrderListProps) => { + return ( + + + + 결제번호: {id}, 결제일시: {paymentAt} + + + 결제개수: {totalCount}, 결제가격: ₩ {formatNumber(totalPrice)} + + + + {rooms.map((item: MyOrderItemWrapperProps) => ( + + ))} + + + ); +}; + +export default MyOrderItemWrapper; diff --git a/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.styles.ts b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.styles.ts index 9d3ca34..f548b44 100644 --- a/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.styles.ts +++ b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.styles.ts @@ -2,9 +2,7 @@ import styled from "@emotion/styled"; export const MyOrderList = styled.li` list-style: none; - margin-top: 2rem; display: flex; flex-direction: column; - gap: 1.5rem; `; diff --git a/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.test.tsx b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.test.tsx new file mode 100644 index 0000000..d7f3d44 --- /dev/null +++ b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.test.tsx @@ -0,0 +1,74 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import { RecoilRoot } from "recoil"; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import MyOrderList from "."; + +// useGetMyOrder 훅을 모킹하는 코드 +jest.mock("../../../../hooks/usePayment", () => ({ + useGetMyOrder: jest.fn(() => ({ data: { data: { data: mockOrderData } } })), +})); + +const mockOrderData = [ + { + paymentId: 1, + totalPrice: 30000, + totalCount: 2, + paymentAt: "2023-11-21T12:00:01", + rooms: [ + { + id: 1, + accommodationName: "부산 앙코르 호텔", + roomName: "스위트룸", + price: 300000, + number_guests: 2, + checkInAt: "2023-12-25", + checkOutAt: "2023-12-26", + roomUrl: "www.accommodation1.com/thumbnail.jpg", + }, + ], + }, +]; + +const createWrapper = () => { + const queryClient = new QueryClient(); + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); +}; + +describe("MyOrderList 컴포넌트", () => { + test("결제 내역이 있는 경우 정상적으로 렌더링되어야 함", () => { + render(, { wrapper: createWrapper() }); + + // 결제 완료한 숙소 정보가 포함된 요소를 찾습니다. + const orderItem = screen.getByText("부산 앙코르 호텔"); + + // 검증 + expect(orderItem).toBeInTheDocument(); + }); + + test('결제 내역이 없는 경우 "로딩중..." 메시지가 나타나야 함', () => { + // useGetMyOrder 훅을 모킹하여 빈 데이터를 반환하도록 설정 + // eslint-disable-next-line @typescript-eslint/no-var-requires + jest + // eslint-disable-next-line @typescript-eslint/no-var-requires + .spyOn(require("../../../../hooks/usePayment"), "useGetMyOrder") + .mockReturnValueOnce({ + data: { data: [] }, + }); + + render(, { wrapper: createWrapper() }); + + // "로딩중..." 메시지가 있는지 확인 + const noOrderMessage = screen.queryByText("로딩중..."); + + // 검증 + expect(noOrderMessage).toBeInTheDocument(); + }); +}); diff --git a/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.type.ts b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.type.ts new file mode 100644 index 0000000..866542d --- /dev/null +++ b/src/components/MyPage/MyOrder/MyOrderList/MyOrderList.type.ts @@ -0,0 +1,7 @@ +export interface MyOrderListProps { + id: string; + totalPrice: number; + totalCount: number; + paymentAt: string; + rooms: []; +} diff --git a/src/components/MyPage/MyOrder/MyOrderList/index.tsx b/src/components/MyPage/MyOrder/MyOrderList/index.tsx index 1a783ab..2078748 100644 --- a/src/components/MyPage/MyOrder/MyOrderList/index.tsx +++ b/src/components/MyPage/MyOrder/MyOrderList/index.tsx @@ -1,25 +1,35 @@ -import MyOrderItem from "../MyOrderItem/index"; import * as Styled from "./MyOrderList.styles"; -import { bookedList } from "../../../../mock/myPageData"; - +import { useGetMyOrder } from "../../../../hooks/usePayment"; +import MyOrderItemWrapper from "../MyOrderItemWrapper"; +import { MyOrderListProps } from "./MyOrderList.type"; const MyOrderList = () => { + const { data } = useGetMyOrder(); // 결제 이력 데이터 요청 + console.log(data?.data.data); + return ( - {bookedList.map((item) => ( - - ))} + {data?.data.data ? ( + data?.data.data + .sort((a: MyOrderListProps, b: MyOrderListProps) => { + const dateA = new Date(a.paymentAt).getTime(); + const dateB = new Date(b.paymentAt).getTime(); + + return dateB - dateA; + }) + .map((item: MyOrderListProps) => ( + + )) + ) : ( +
로딩중...
+ )}
); }; - export default MyOrderList; diff --git a/src/components/MyPage/MyWish/MyWishHeader/MyWIshHeader.styles.ts b/src/components/MyPage/MyWish/MyWishHeader/MyWIshHeader.styles.ts new file mode 100644 index 0000000..5204247 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishHeader/MyWIshHeader.styles.ts @@ -0,0 +1,47 @@ +import styled from "@emotion/styled"; + +export const MyBox = styled.div` + display: flex; + text-align: center; + align-items: center; + justify-content: space-between; + + width: 60%; + border-radius: 0.625rem; + border: 1px solid #e6e6e6; + background: #fff; + padding: 3rem 3.12rem; + box-sizing: border-box; + + margin-top: 2rem; + + cursor: pointer; + &:hover { + border: 1px solid #ff5100; + box-shadow: 0px 0px 20px 0px rgba(255, 81, 0, 0.05); + } + + @media (max-width: 443px) { + width: 90%; + } +`; + +export const Content = styled.div` + display: flex; + text-align: center; + align-items: center; +`; + +export const Title = styled.div` + color: #222; + font-size: 1.75rem; + font-weight: 400; + margin-right: 3.5rem; +`; + +export const Count = styled.div` + color: #222; + font-size: 1.875rem; + font-weight: 600; + line-height: 2.625rem; +`; diff --git a/src/components/MyPage/MyWish/MyWishHeader/index.test.tsx b/src/components/MyPage/MyWish/MyWishHeader/index.test.tsx new file mode 100644 index 0000000..4bb08b2 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishHeader/index.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from "@testing-library/react"; +import MyWishHeader from "."; + +describe("MyWish Header 테스트", () => { + test("MyWish Header 렌더링 테스트", () => { + render(); + expect(screen.getByText("❤️ 찜한 목록")).toBeInTheDocument(); + }); + + test("MyWish Header props로 받아온 숫자 테스트", () => { + render(); + expect(screen.getByText("5")).toBeInTheDocument(); + }); +}); diff --git a/src/components/MyPage/MyWish/MyWishHeader/index.tsx b/src/components/MyPage/MyWish/MyWishHeader/index.tsx new file mode 100644 index 0000000..12a4e25 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishHeader/index.tsx @@ -0,0 +1,18 @@ +import * as Styled from "./MyWIshHeader.styles"; + +type MyWishHeaderProps = { + wishCount: number; +}; + +const MyWishHeader = ({ wishCount }: MyWishHeaderProps) => { + return ( + + + ❤️ 찜한 목록 + {wishCount} + + + ); +}; + +export default MyWishHeader; diff --git a/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishItem.styles.ts b/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishItem.styles.ts new file mode 100644 index 0000000..26df8df --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishItem.styles.ts @@ -0,0 +1,97 @@ +import styled from "@emotion/styled"; +import { motion } from "framer-motion"; +import { flexCenter } from "../../../../../pages/Signin/Signin.styles"; + +export const CategoryItemContainer = styled.div` + ${flexCenter}; + + width: 100%; + height: auto; + + padding: 1rem; + position: relative; + + border: 1px solid #e0e0e0; + border-radius: 10px; + + font-size: 0.9rem; + + transition: all 0.3s ease-in-out; + + &:hover { + cursor: pointer; + border: 1px solid #ff5100; + } +`; + +export const CategoryItemWrapper = styled(motion.div)` + display: flex; + flex-direction: row; + align-items: center; + + width: 100%; + height: 100%; + + gap: 0.5rem; +`; + +export const CategoryImage = styled.img` + width: 10vw; + height: 10vw; + + padding: 1rem; + + object-fit: cover; + object-position: center; + border-radius: 30px; + + @media (max-width: 768px) { + width: 20vw; + height: 20vw; + } +`; + +export const CategoryTextWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: start; + + width: 90%; + + gap: 1rem; +`; + +export const CategoryName = styled.strong` + font-size: 1.1rem; + font-weight: 700; + + color: #222222; +`; + +export const CategoryTopWrapper = styled.div` + display: flex; + flex-direction: column; + + gap: 0.3rem; +`; + +export const CategoryDescription = styled.span` + color: #222222; +`; + +export const CategoryView = styled.span` + color: #222222; +`; + +export const CategoryDownWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + width: 90%; +`; + +export const CategoryPrice = styled.span` + color: #222222; + font-weight: 600; +`; diff --git a/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishList.types.ts b/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishList.types.ts new file mode 100644 index 0000000..f2b2e6c --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/MyWishItem/MyWishList.types.ts @@ -0,0 +1,5 @@ +import { myWishProps } from "../../../../../pages/Category/Category.types"; + +export type myWishItemProps = { + data: myWishProps; +}; diff --git a/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.test.tsx b/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.test.tsx new file mode 100644 index 0000000..b178f39 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import MyWishItem from "."; +import { createWrapper } from "../../../../../test/test.utils"; + +const testData = { + id: String(53096), + name: "비진도 바다이야기 펜션", + address: "경상남도 통영시 한산면 외항길 78", + phoneNumber: "055-642-6171", + type: 4, + wishCount: 644, + thumbnailUrl: + "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 707, + checkIn: "18:00", + checkOut: "12:00", + infoDetail: "통영 바다를 한눈에 담은 펜션", +}; + +describe("MyWish Test", () => { + test("MyWish", () => { + if ("IntersectionObserver" in window) { + const wrapper = createWrapper(); + render(, { wrapper }); + } + }); + + test("카테고리 아이템을 누르면 해당 숙소의 상세페이지로 이동합니다.", () => { + if ("IntersectionObserver" in window) { + const router = jest.fn(); + const user = userEvent; + const wrapper = createWrapper(); + + render(, { wrapper }); + + const button = screen.getByTestId("individual-item"); + user.click(button); + + expect(router).toHaveBeenCalledWith(testData.id); + } + }); +}); diff --git a/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.tsx b/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.tsx new file mode 100644 index 0000000..09cbf65 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/MyWishItem/index.tsx @@ -0,0 +1,99 @@ +import { useInView } from "framer-motion"; +import * as Styled from "./MyWishItem.styles"; +import Empty from "../../../../../assets/image/empty.png"; + +import * as _ from "lodash"; +import { useEffect, useRef, useState } from "react"; +import { random } from "lodash"; +import { useNavigate } from "react-router-dom"; +import HeartClick from "../../../../Category/HeartClick"; +import { myWishItemProps } from "./MyWishList.types"; + +const checkWindowInnerWidth = () => { + if (window.innerWidth < 443) { + return 10; + } else if (window.innerWidth < 600) { + return 20; + } else if (window.innerWidth < 768) { + return 40; + } else if (window.innerWidth < 1024) { + return 60; + } else { + return 120; + } +}; + +const MyWishItem = ({ data }: myWishItemProps) => { + const ref = useRef(null); + const firstWindowSize = checkWindowInnerWidth(); + const [truncateLength, setTruncateLength] = useState(firstWindowSize); + const isInView = useInView(ref, { once: true }); + const router = useNavigate(); + + const handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + router(`/detailList?accommodation-id=${data.id}`); + }; + + const handleError = (e: React.SyntheticEvent) => { + e.currentTarget.src = Empty; + }; + + useEffect(() => { + const handleResize = () => { + const newTruncateLength = checkWindowInnerWidth(); + setTruncateLength(newTruncateLength); + }; + + window.addEventListener("resize", handleResize); + + return () => window.removeEventListener("resize", handleResize); + }, []); + + return ( + + +
+ +
+ + {data.name} + + + {_.truncate(data.infoDetail, { + length: truncateLength, + })} + + + + 조회수 : {data.viewCount} + + + +
+
+ ); +}; + +export default MyWishItem; diff --git a/src/components/MyPage/MyWish/MyWishList/MyWishList.styles.ts b/src/components/MyPage/MyWish/MyWishList/MyWishList.styles.ts new file mode 100644 index 0000000..66d421e --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/MyWishList.styles.ts @@ -0,0 +1,26 @@ +import styled from "@emotion/styled"; + +export const ListGridItemWrapper = styled.div` + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + + width: 60%; + height: object-fit; + + margin: 2rem 0; + + box-sizing: border-box; + + gap: 1rem; + overflow: auto; + + @media (max-width: 443px) { + width: 90%; + } + + ::-webkit-scrollbar { + display: none; + } +`; diff --git a/src/components/MyPage/MyWish/MyWishList/index.test.tsx b/src/components/MyPage/MyWish/MyWishList/index.test.tsx new file mode 100644 index 0000000..4a79c67 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/index.test.tsx @@ -0,0 +1,58 @@ +import { render, renderHook, screen, waitFor } from "@testing-library/react"; +import MyWishList from "./index"; +import { createWrapper } from "../../../../test/test.utils"; +import { useGetMyWishList } from "../../../../api/MyPage/query"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockedUseGetMyWishList = useGetMyWishList as jest.Mock; + +jest.mock("../../../../api/MyPage/query"); + +describe("MyWish 쿼리 받아오기 테스트", () => { + beforeEach(() => { + mockedUseGetMyWishList.mockImplementation(() => ({ + isLoading: true, + error: false, + })); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("로딩중일 때 로딩중이라는 문구가 보여야 한다.", () => { + mockedUseGetMyWishList.mockImplementation(() => ({ + isLoading: true, + error: false, + })); + + const wrapper = createWrapper(); + + render(, { wrapper }); + + const { result } = renderHook(() => useGetMyWishList(), { wrapper }); + + waitFor(() => { + expect(result.current.isLoading).toBe(true); + expect(screen.getByText("로딩중")).toBeInTheDocument(); + }); + }); + + test("로딩중이 아닐 때 로딩중이라는 문구가 보여서는 안된다.", () => { + mockedUseGetMyWishList.mockImplementation(() => ({ + isLoading: false, + error: false, + })); + + const wrapper = createWrapper(); + + render(, { wrapper }); + + const { result } = renderHook(() => useGetMyWishList(), { wrapper }); + + waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(screen.queryByText("로딩중")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/MyPage/MyWish/MyWishList/index.tsx b/src/components/MyPage/MyWish/MyWishList/index.tsx new file mode 100644 index 0000000..2ffbcd2 --- /dev/null +++ b/src/components/MyPage/MyWish/MyWishList/index.tsx @@ -0,0 +1,31 @@ +import { useGetMyWishList } from "../../../../api/MyPage/query"; +import * as Styled from "./MyWishList.styles"; +import { myWishDataProps } from "../../../../pages/Category/Category.types"; +import MyWishHeader from "../MyWishHeader"; +import MyWishItem from "./MyWishItem"; + +const MyWishList = () => { + const { data, isLoading, error } = useGetMyWishList(); + + if (error) return
에러발생
; + + return ( + <> + + + {data && + data.data.map((item: myWishDataProps) => { + return ( + + ); + })} + {isLoading &&
} +
+ + ); +}; + +export default MyWishList; diff --git a/src/components/Payment/Btn/Btn.test.tsx b/src/components/Payment/Btn/Btn.test.tsx new file mode 100644 index 0000000..7462d4e --- /dev/null +++ b/src/components/Payment/Btn/Btn.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import Btn from '.'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; + + +const createWrapper = () => { + const queryClient = new QueryClient(); + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + }; + + +describe('Btn 컴포넌트', () => { + test('최초 진입 후 필수 약관에 동의하기 전까지 결제하기 버튼이 비활성화되어야 함', () => { + render(, { wrapper: createWrapper() } ); + + // 결제하기 버튼이 비활성화되어 있는지 확인 + const goToPayButton = screen.getByText('결제하기'); + expect(goToPayButton).toBeDisabled(); + }); +}); diff --git a/src/components/Payment/Btn/index.tsx b/src/components/Payment/Btn/index.tsx index e367b57..fd0e989 100644 --- a/src/components/Payment/Btn/index.tsx +++ b/src/components/Payment/Btn/index.tsx @@ -1,25 +1,58 @@ import { purchaseState, termsState } from "../../../store/purchaseAtom"; import * as Styled from "./Btn.styles"; import { useRecoilState } from "recoil"; -import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import formatNumber from "../../../utils/formatNumber"; +import { useDeleteOrder, usePayment } from "../../../hooks/usePayment"; +import { useNavigate } from "react-router-dom"; +import Swal from "sweetalert2"; const Btn = () => { const navigate = useNavigate(); const [isDisabeld, setIsDisabled] = useState(true); const termsAllChecked = useRecoilState(termsState); const [purchaseList] = useRecoilState(purchaseState); + const orderId = purchaseList.order_id; + const PaymentMutation = usePayment(); + const deleteOrderMutation = useDeleteOrder(); + + // 결제 전송 + const paymentFetch = () => { + Swal.fire({ + title: "결제 하시겠습니까?", + padding: "50px", + confirmButtonColor: "#FF5100", + confirmButtonText: "결제하기", + showDenyButton: true, + denyButtonText: "취소하기", + denyButtonColor: "#001650", + }).then((result) => { + if (result.isConfirmed) { + PaymentMutation.mutate(orderId as number, { + onSuccess: async (data) => { + console.log("결제 성공 데이터:", data); + navigate(`/Complete/${data.data.data}`); + }, + }); + } else if (result.isDenied) { + return; + } else if (result.isDismissed) { + return; + } + }); + }; + + const deleteOrderfetch = () => { + deleteOrderMutation.mutate(orderId as number); + }; const handleBackToCart = () => { + deleteOrderfetch(); history.back(); }; + const handleGoToPay = () => { - const answer = confirm("결제하시겠습니까?"); - if (answer) { - sessionStorage.clear(); - navigate("/Complete"); - } + paymentFetch(); }; useEffect(() => { diff --git a/src/components/Payment/Item/Item.styles.ts b/src/components/Payment/Item/Item.styles.ts index 165dd11..5f092e8 100644 --- a/src/components/Payment/Item/Item.styles.ts +++ b/src/components/Payment/Item/Item.styles.ts @@ -8,7 +8,7 @@ export const ItemWrapper = styled.ul` display: flex; flex-direction: column; padding: 1.19rem 0.81rem 2.19rem 1.81rem; - margin-bottom: 1.5rem; + margin-bottom: 1.3rem; `; export const ItemTitle = styled.p` @@ -28,6 +28,7 @@ export const ItemInfo = styled.div` display: flex; flex-direction: column; gap: 0.7rem; + margin-top: 0.5rem; `; export const ItemValueWrapper = styled.div``; @@ -41,10 +42,22 @@ export const Title = styled.span` `; export const ItemImg = styled.div` - width: 8.25rem; - height: 8.5625rem; - background: #d9d9d9; - margin-right: 1.81rem; + position: relative; + + background-color: white; + + overflow: hidden; + + background-image: url("/src/assets/image/empty.png"); + background-repeat: no-repeat; + background-size: cover; + object-fit: cover; + margin-right: 2rem; + + img { + width: 10rem; + height: 10rem; + } `; export const Content = styled.span``; diff --git a/src/components/Payment/Item/Item.types.ts b/src/components/Payment/Item/Item.types.ts index 4124fed..59b47af 100644 --- a/src/components/Payment/Item/Item.types.ts +++ b/src/components/Payment/Item/Item.types.ts @@ -5,4 +5,5 @@ export interface PaymentItemProps { checkOut: string; guests: number; price: number; + roomUrl: string; } diff --git a/src/components/Payment/Item/index.tsx b/src/components/Payment/Item/index.tsx index bcd5681..06ace59 100644 --- a/src/components/Payment/Item/index.tsx +++ b/src/components/Payment/Item/index.tsx @@ -10,12 +10,16 @@ const index = ({ checkOut, guests, price, + roomUrl, }: PaymentItemProps) => { return ( {name} - + + + + 방 타입: @@ -40,7 +44,9 @@ const index = ({ - {formatNumber(price)} + + {formatNumber(price * calculateNightCount(checkIn, checkOut))} + ); diff --git a/src/components/Payment/ItemList/ItemList.styles.ts b/src/components/Payment/ItemList/ItemList.styles.ts index 946397b..e871b29 100644 --- a/src/components/Payment/ItemList/ItemList.styles.ts +++ b/src/components/Payment/ItemList/ItemList.styles.ts @@ -14,7 +14,7 @@ export const Title = styled.p` export const ItemList = styled.li` width: 100%; - height: 18rem; + height: 20rem; border-bottom: 1px solid #d3d3d3; overflow: auto; `; diff --git a/src/components/Payment/ItemList/ItemList.types.ts b/src/components/Payment/ItemList/ItemList.types.ts index cab5a27..4ad65f2 100644 --- a/src/components/Payment/ItemList/ItemList.types.ts +++ b/src/components/Payment/ItemList/ItemList.types.ts @@ -1,5 +1,10 @@ -import { CartItemType } from "../../../types"; - -export interface EstimateProps { - estimatedPrice: CartItemType[]; +export interface OrderItem { + roomUrl: string; + id: number; + accommodationName: string; + roomName: string; + checkInAt: string; + checkOutAt: string; + numberOfGuests: number; + price: number; } diff --git a/src/components/Payment/ItemList/index.tsx b/src/components/Payment/ItemList/index.tsx index c84f1ac..66c6adc 100644 --- a/src/components/Payment/ItemList/index.tsx +++ b/src/components/Payment/ItemList/index.tsx @@ -3,22 +3,39 @@ import * as Styled from "./ItemList.styles"; import { useRecoilState } from "recoil"; import { purchaseState } from "../../../store/purchaseAtom"; import formatNumber from "../../../utils/formatNumber"; +import { useGetOrderList } from "../../../hooks/useGetOrderList"; +import { OrderItem } from "./ItemList.types"; const ItemList = () => { const [purchaseList] = useRecoilState(purchaseState); + const orderId = purchaseList.order_id; + const { data } = useGetOrderList(orderId as number); + console.log(data); + const orderList: OrderItem[] = data?.data.data.rooms; + + // authInstance + // .get("/orders/9") + // .then((response) => { + // console.log(response); + // }) + // .catch((error) => { + // console.error(error); + // }); + return ( - 결제 항목 {purchaseList.data.length} + 결제 항목 {orderList?.length} - {purchaseList.data.map((item) => ( + {orderList?.map((item) => ( ))} diff --git a/src/components/Payment/TermsAndConditions/TermsAndConditions.test.tsx b/src/components/Payment/TermsAndConditions/TermsAndConditions.test.tsx new file mode 100644 index 0000000..a695649 --- /dev/null +++ b/src/components/Payment/TermsAndConditions/TermsAndConditions.test.tsx @@ -0,0 +1,54 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import TermsAndConditions from '.'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import { RecoilRoot } from 'recoil'; + +const createWrapper = () => { + const queryClient = new QueryClient(); + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + }; + +describe('TermsAndConditions 컴포넌트', () => { + test('필수 약관 전체 동의 체크 시 모든 약관이 체크되어야 함', () => { + render(, { wrapper: createWrapper() }); + + const termsAllCheckbox = screen.getByText('필수 약관 전체 동의'); + fireEvent.click(termsAllCheckbox); + + const individualTermsCheckboxes = screen.getAllByTestId('individual-terms-checkbox'); + individualTermsCheckboxes.forEach((checkbox) => { + expect(checkbox).toBeChecked(); + }); + }); + + test('각 약관을 개별적으로 체크할 수 있어야 함', () => { + render(,{ wrapper: createWrapper() }); + + const individualTermsCheckboxes = screen.getAllByTestId('individual-terms-checkbox'); + + fireEvent.click(individualTermsCheckboxes[0]); + expect(individualTermsCheckboxes[0]).toBeChecked(); + + fireEvent.click(individualTermsCheckboxes[1]); + expect(individualTermsCheckboxes[1]).toBeChecked(); + }); + + test('전체 약관 동의 체크 시 개별 약관 체크를 해제하면 필수 약관 전체 동의도 해제되어야 함', () => { + render(,{ wrapper: createWrapper() }); + + const termsAllCheckbox = screen.getByText('필수 약관 전체 동의'); + const individualTermsCheckboxes = screen.getAllByTestId('individual-terms-checkbox'); + + fireEvent.click(termsAllCheckbox); + fireEvent.click(individualTermsCheckboxes[0]); + + expect(termsAllCheckbox).not.toBeChecked(); + }); +}); diff --git a/src/components/Payment/TermsAndConditions/index.tsx b/src/components/Payment/TermsAndConditions/index.tsx index 260804f..1ceb009 100644 --- a/src/components/Payment/TermsAndConditions/index.tsx +++ b/src/components/Payment/TermsAndConditions/index.tsx @@ -15,7 +15,7 @@ const TermsAndConditions = () => { const setTermsAllChecked = useSetRecoilState(termsState); //체크박스 단일 선택 - const handleSingleCheck = (e) => { + const handleSingleCheck = (e: React.ChangeEvent) => { if (e.target.checked) { setCheckItems([...checkItems, Number(e.target.id)]); } else { @@ -24,7 +24,7 @@ const TermsAndConditions = () => { }; //체크박스 전체 선택 - const handleAllCheck = (e) => { + const handleAllCheck = (e: React.ChangeEvent) => { if (e.target.checked) { const items: number[] = []; Object.entries(data).map((item, i) => { @@ -62,6 +62,7 @@ const TermsAndConditions = () => { handleSingleCheck(e)} // 체크된 아이템 배열에 해당 아이템이 있을 경우 선택 활성화, 아닐 시 해제 diff --git a/src/components/Search/Button/Button.styles.ts b/src/components/Search/Button/Button.styles.ts index 9fc08c7..0859f20 100644 --- a/src/components/Search/Button/Button.styles.ts +++ b/src/components/Search/Button/Button.styles.ts @@ -1,20 +1,20 @@ import styled from "@emotion/styled"; -export const SearchButton = styled.div` +export const SearchButton = styled.button` display: flex; flex-direction: column; justify-content: center; align-items: center; - width: 5em; + width: 10rem; height: 100%; - gap: 0.5em; + gap: 0.5rem; background-color: #e6e6e6; border: 1px solid #e6e6e6; - border-radius: 0.5em; + border-radius: 0.5rem; - font-size: 2em; + font-size: 2rem; transition: background-color 0.3s, color 0.1s, @@ -31,12 +31,48 @@ export const SearchButton = styled.div` } img { - width: 2em; - height: 2em; + width: 5rem; + height: 4rem; } &:active { - transform: translateX(0.05em) translateY(0.05em); + transform: translateX(0.15rem) translateY(0.15rem); + } + + @media (max-width: 768px) { + flex-direction: row; + justify-content: center; + align-items: center; + + width: 100%; + height: 5rem; + + &:hover { + background-color: #191554; + color: white; + + img { + filter: brightness(0) invert(1); + } + } + } + + @media (max-width: 480px) { + flex-direction: row; + justify-content: center; + align-items: center; + + width: 100%; + height: 5rem; + + &:hover { + background-color: #191554; + color: white; + + img { + filter: brightness(0) invert(1); + } + } } `; @@ -46,10 +82,10 @@ export const SearchResetButton = styled.button` justify-content: center; align-items: center; - width: 7em; - height: 5em; + width: 7rem; + height: 5rem; padding: 0; - margin-left: 1em; + margin-left: 1rem; background-color: transparent; appearance: none; @@ -59,12 +95,12 @@ export const SearchResetButton = styled.button` overflow: hidden; img { - padding: 0.2em; + padding: 0.2rem; border-radius: 2rem; - width: 3em; - height: 3em; + width: 3rem; + height: 2.5rem; transition: transform 0.3s, scale 0.3s, @@ -81,4 +117,34 @@ export const SearchResetButton = styled.button` &:active img { transform: rotate(-180deg) scale(0.95); } + + @media (max-width: 768px) { + flex-direction: column; + justify-content: center; + align-items: center; + + padding: 0; + margin: 0; + + height: 100%; + + &:active img { + transform: rotate(-180deg) scale(0.95); + } + } + + @media (max-width: 480px) { + flex-direction: column; + justify-content: center; + align-items: center; + + padding: 0; + margin: 0; + + height: 100%; + + &:active img { + transform: rotate(-180deg) scale(0.95); + } + } `; diff --git a/src/components/Search/Button/index.test.tsx b/src/components/Search/Button/index.test.tsx new file mode 100644 index 0000000..16255e8 --- /dev/null +++ b/src/components/Search/Button/index.test.tsx @@ -0,0 +1,26 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { SearchButton, SearchResetButton } from "./index"; + +describe("검색페이지 검색버튼", () => { + it("버튼 클릭시 선택된 카테고리를 종합하여 검색 쿼리를 보낸다", () => { + const sendSearchQuery = jest.fn(); + render(); + + const button = screen.getByRole("button", { name: /검색/i }); + + fireEvent.click(button); + expect(sendSearchQuery).toHaveBeenCalledTimes(1); + }); +}); + +describe("검색페이지 리셋버튼", () => { + it("버튼 클릭시 검색 카테고리가 리셋된다", () => { + const handleResetSearch = jest.fn(); + render(); + + const button = screen.getByRole("button", { name: /다시 검색하기/i }); + + fireEvent.click(button); + expect(handleResetSearch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/Search/Button/index.tsx b/src/components/Search/Button/index.tsx index 7571c26..260ff7e 100644 --- a/src/components/Search/Button/index.tsx +++ b/src/components/Search/Button/index.tsx @@ -3,7 +3,7 @@ import ResetIcon from "../../../assets/svg/reset-btn.svg"; import SearchIcon from "../../../assets/svg/search-icon.svg"; import { SearchButtonProps, SearchResetButtonProps } from "./Button.types"; -export const SearchButton: React.FC = ({ onClick }) => { +export const SearchButton = ({ onClick }: SearchButtonProps) => { return ( search @@ -12,9 +12,9 @@ export const SearchButton: React.FC = ({ onClick }) => { ); }; -export const SearchResetButton: React.FC = ({ - onClick, -}) => { +export const SearchResetButton = ({ + onClick +}: SearchResetButtonProps) => { return ( 다시 검색하기 diff --git a/src/components/Search/SearchParamsDisplay/SearchParamsDisplay.styles.ts b/src/components/Search/SearchParamsDisplay/SearchParamsDisplay.styles.ts new file mode 100644 index 0000000..cd6323c --- /dev/null +++ b/src/components/Search/SearchParamsDisplay/SearchParamsDisplay.styles.ts @@ -0,0 +1,122 @@ +import styled from "@emotion/styled"; + +export const SearchHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + gap: 1rem; + + h2 { + margin-right: 1rem; + } + + @media (max-width: 768px) { + flex-direction: column; + justify-content: center; + align-items: center; + + gap: 0; + } + + @media (max-width: 480px) { + flex-direction: column; + justify-content: center; + align-items: center; + + gap: 0; + } +`; + +export const SearchTitle = styled.div` + width: 20rem; +`; + +export const SearchParamsWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + height: 100%; + + @media (max-width: 768px) { + flex-direction: row; + justify-content: center; + align-items: center; + + padding: 0; + margin: 0; + + width: 100%; + } + + @media (max-width: 480px) { + flex-direction: row; + justify-content: center; + align-items: center; + + padding: 0; + margin: 0; + + width: 100%; + } +`; + +export const SearchParams = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex: 1 0 40%; + + gap: 0.5rem; + + font-size: 1.1rem; + font-weight: 500; + + .region, + .type { + padding: 0.3rem; + background-color: #e6e6e6; + } + + .option { + display: flex; + flex-direction: row; + + gap: 0.5rem; + } + + .option-each { + padding: 0.3rem; + background-color: #e6e6e6; + } + + @media (max-width: 768px) { + flex-direction: row; + justify-content: center; + align-items: center; + flex-grow: 1; + + width: 100%; + height: 100%; + padding: 0; + + font-size: 1rem; + } + + @media (max-width: 480px) { + flex-direction: row; + justify-content: center; + align-items: center; + flex-grow: 1; + + width: 22rem; + height: 100%; + padding: 0; + + font-size: 1rem; + } +`; diff --git a/src/components/Search/SearchParamsDisplay/index.test.tsx b/src/components/Search/SearchParamsDisplay/index.test.tsx new file mode 100644 index 0000000..84f5a2c --- /dev/null +++ b/src/components/Search/SearchParamsDisplay/index.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import * as recoilHooks from "recoil"; +import { SearchParamsDisplay } from "../SearchParamsDisplay"; +import * as searchHandlers from "../../../hooks/useSearchHandler"; + +jest.mock("recoil", () => ({ + ...jest.requireActual("recoil"), + useRecoilValue: jest.fn(), +})); + +jest.mock("../../../hooks/useSearchHandler", () => ({ + useSearchHandlers: jest.fn(), +})); + +const mockSearchState = { + selectedRegion: 0, + selectedType: 1, + selectedOptions: [99], +}; + +const mockHandleResetSearch = jest.fn(); + +beforeEach(() => { + (recoilHooks.useRecoilValue as jest.Mock).mockReturnValue(mockSearchState); + + (searchHandlers.useSearchHandlers as jest.Mock).mockReturnValue({ + handleResetSearch: mockHandleResetSearch, + }); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("선택한 검색 카테고리 상단 출력 테스트", () => { + test("현재 선택된 항목에 맞는 해당 카테고리가 상단에 출력되어야 함", () => { + render( + + + , + ); + + const regionDisplay = screen.getByText("서울시"); + const typeDisplay = screen.getByText("콘도"); + const optionsDisplay = screen.getAllByText("상관 없음"); + + expect(regionDisplay).toBeInTheDocument(); + expect(typeDisplay).toBeInTheDocument(); + expect(optionsDisplay).toHaveLength(1); + }); + + test("'다시 검색하기' 버튼을 누를 경우 기존 선택된 카테고리가 모두 리셋되어야 함", () => { + render( + + + , + ); + + const resetButton = screen.getByRole("button"); + resetButton.click(); + + expect(mockHandleResetSearch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/Search/SearchParamsDisplay/index.tsx b/src/components/Search/SearchParamsDisplay/index.tsx new file mode 100644 index 0000000..f1afb26 --- /dev/null +++ b/src/components/Search/SearchParamsDisplay/index.tsx @@ -0,0 +1,42 @@ +import * as Styled from "./SearchParamsDisplay.styles"; +import { options, regions, types } from "../../../store/searchSelectorData"; +import { useRecoilValue } from "recoil"; +import { searchStateAtom } from "../../../store/searchSelectorAtom"; +import { SearchResetButton } from "../Button"; +import { useSearchHandlers } from "../../../hooks/useSearchHandler"; + +export const SearchParamsDisplay = () => { + const searchState = useRecoilValue(searchStateAtom); + const { handleResetSearch } = useSearchHandlers(); + + return ( + + +

원하시는 숙소를 찾아드릴게요 👀 ❤️

+
+ + +
+ {`${regions.find( + (region) => region.value === searchState.selectedRegion, + )?.name} `} +
+ +
+ {`${types.find((type) => type.value === searchState.selectedType) + ?.name} `} +
+ +
+ {searchState.selectedOptions.map((optionValue, index) => ( +
+ {options.find((option) => option.value === optionValue)?.name} +
+ ))} +
+
+ +
+
+ ); +}; diff --git a/src/components/Search/SearchSelector/SearchOptions/index.test.tsx b/src/components/Search/SearchSelector/SearchOptions/index.test.tsx new file mode 100644 index 0000000..85ce6c0 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchOptions/index.test.tsx @@ -0,0 +1,73 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import { OptionsWrapper } from "../SearchOptions"; + +const mockHandleOptionClick = jest.fn(); +jest.mock("../../../../hooks/useSearchHandler", () => ({ + useSearchHandlers: () => ({ + handleOptionClick: mockHandleOptionClick, + }), +})); + +const options = [ + { name: "상관 없음", value: 99 }, + { name: "주차 가능", value: 0 }, + { name: "조리 가능", value: 1 }, + { name: "픽업 가능", value: 2 }, +]; + +describe("검색바 테스트 - 추가 옵션", () => { + beforeEach(() => { + mockHandleOptionClick.mockReset(); + }); + + test("마우스 hover시 모든 옵션이 출력되어야 한다", () => { + const { getByText } = render( + + + , + ); + + fireEvent.mouseEnter(getByText("추가 옵션")); + options.forEach((option) => { + expect(getByText(option.name)).toBeInTheDocument(); + }); + }); + + const simulateHover = () => { + render( + + + , + ); + fireEvent.mouseEnter(screen.getByText("추가 옵션")); + }; + + test('"상관 없음"이 선택된 경우, 나머지 세 옵션은 동시에 선택 불가함', async () => { + simulateHover(); + + const noConcernOption = screen.getByText("상관 없음"); + fireEvent.click(noConcernOption); + expect(mockHandleOptionClick).toHaveBeenCalledWith(99); + + const parkingOption = screen.getByText("주차 가능"); + fireEvent.click(parkingOption); + expect(mockHandleOptionClick).toHaveBeenCalledWith(0); + }); + + test('"상관 없음"을 제외한 나머지 세 옵션은 중복 선택 가능', async () => { + simulateHover(); + + fireEvent.click(screen.getByText("주차 가능")); + expect(mockHandleOptionClick).toHaveBeenCalledWith(0); + + fireEvent.click(screen.getByText("조리 가능")); + expect(mockHandleOptionClick).toHaveBeenCalledWith(1); + + fireEvent.click(screen.getByText("픽업 가능")); + expect(mockHandleOptionClick).toHaveBeenCalledWith(2); + + fireEvent.click(screen.getByText("상관 없음")); + expect(mockHandleOptionClick).toHaveBeenCalledWith(99); + }); +}); diff --git a/src/components/Search/SearchSelector/SearchOptions/index.tsx b/src/components/Search/SearchSelector/SearchOptions/index.tsx new file mode 100644 index 0000000..ff349a1 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchOptions/index.tsx @@ -0,0 +1,74 @@ +import * as Styled from "../SearchSelector.styles"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { useSearchHandlers } from "../../../../hooks/useSearchHandler"; +import { searchStateAtom } from "../../../../store/searchSelectorAtom"; +import OptionIcon from "../../../../assets/svg/option-icon.svg"; +import Option0 from "../../../../assets/svg/option0.svg"; +import Option1 from "../../../../assets/svg/option1.svg"; +import Option2 from "../../../../assets/svg/option2.svg"; +import { OptionImages } from "../SearchSelector.types"; + +const optionImages: OptionImages = { + 0: Option0, + 1: Option1, + 2: Option2, +}; + +type Option = { + name: string; + value: number; +}; + +type OptionsWrapperProps = { + options: Option[]; +}; + +export const OptionsWrapper = ({ options }: OptionsWrapperProps) => { + const searchState = useRecoilValue(searchStateAtom); + const setSearchState = useSetRecoilState(searchStateAtom); + const { handleOptionClick } = useSearchHandlers(); + + return ( + + setSearchState((current) => ({ ...current, isOptionsHovered: true })) + } + onMouseLeave={() => + setSearchState((current) => ({ ...current, isOptionsHovered: false })) + } + > + + Check 3

추가 옵션

+ {!searchState.isOptionsHovered && ( + option-selector + )} +
+ {searchState.isOptionsHovered && ( + + {options.map((option) => ( + handleOptionClick(option.value)} + > + {option.name} + {option.name !== "상관 없음" && ( + {option.name} + )} + + ))} + + )} + {!searchState.isOptionsHovered && searchState.isOptionsSelected && ( + + {searchState.selectedOptions.map((selectedValue) => { + const optionName = options.find( + (option) => option.value === selectedValue, + )?.name; + return
{optionName} ✔️
; + })} +
+ )} +
+ ); +}; diff --git a/src/components/Search/SearchSelector/SearchRegion/index.test.tsx b/src/components/Search/SearchSelector/SearchRegion/index.test.tsx new file mode 100644 index 0000000..2225903 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchRegion/index.test.tsx @@ -0,0 +1,61 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import { RegionWrapper } from "../SearchRegion"; + +const mockHandleRegionClick = jest.fn(); +jest.mock("../../../../hooks/useSearchHandler", () => ({ + useSearchHandlers: () => ({ + handleRegionClick: mockHandleRegionClick, + }), +})); + +const regions = [ + { name: "전국", value: 99 }, + { name: "서울시", value: 0 }, + { name: "경기도", value: 1 }, + { name: "강원도", value: 2 }, + { name: "충청도", value: 3 }, + { name: "전라도", value: 4 }, + { name: "경상도", value: 5 }, + { name: "제주도", value: 6 }, +]; + +describe("검색바 테스트 - 지역 선택", () => { + beforeEach(() => { + mockHandleRegionClick.mockReset(); + }); + + test("마우스 hover시 모든 옵션이 출력되어야 한다", () => { + const { getByText } = render( + + + , + ); + + fireEvent.mouseEnter(getByText("지역 선택")); + regions.forEach((region) => { + expect(getByText(region.name)).toBeInTheDocument(); + }); + }); + + const simulateHover = () => { + render( + + + , + ); + fireEvent.mouseEnter(screen.getByText("지역 선택")); + }; + + test("지역은 하나만 선택할 수 있다", async () => { + simulateHover(); + + const allRegion = screen.getByText("전국"); + fireEvent.click(allRegion); + expect(mockHandleRegionClick).toHaveBeenCalledWith(99); + + const regionSeoul = screen.getByText("서울시"); + fireEvent.click(regionSeoul); + expect(mockHandleRegionClick).toHaveBeenCalledWith(0); + }); +}); diff --git a/src/components/Search/SearchSelector/SearchRegion/index.tsx b/src/components/Search/SearchSelector/SearchRegion/index.tsx new file mode 100644 index 0000000..64419b5 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchRegion/index.tsx @@ -0,0 +1,60 @@ +import * as Styled from "../SearchSelector.styles"; +import MapIcon from "../../../../assets/svg/map-icon.svg"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { useSearchHandlers } from "../../../../hooks/useSearchHandler"; +import { searchStateAtom } from "../../../../store/searchSelectorAtom"; + +type Region = { + name: string; + value: number; +}; + +type RegionWrapperProps = { + regions: Region[]; +}; + +export const RegionWrapper = ({ regions }: RegionWrapperProps) => { + const searchState = useRecoilValue(searchStateAtom); + const setSearchState = useSetRecoilState(searchStateAtom); + const { handleRegionClick } = useSearchHandlers(); + + return ( + + setSearchState((current) => ({ ...current, isRegionHovered: true })) + } + onMouseLeave={() => + setSearchState((current) => ({ ...current, isRegionHovered: false })) + } + > + + Check 1

지역 선택

+ {!searchState.isRegionHovered && ( + region-selector + )} +
+ {searchState.isRegionHovered && ( + + {regions.map((region) => ( + handleRegionClick(region.value)} + > + {region.name} + + ))} + + )} + {!searchState.isRegionHovered && searchState.isRegionSelected && ( + + {!searchState.isRegionHovered && + regions.find( + (region) => region.value === searchState.selectedRegion, + )?.name}{" "} + ✔️ + + )} +
+ ); +}; diff --git a/src/components/Search/SearchSelector/SearchSelector.styles.ts b/src/components/Search/SearchSelector/SearchSelector.styles.ts new file mode 100644 index 0000000..6e82104 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchSelector.styles.ts @@ -0,0 +1,242 @@ +import styled from "@emotion/styled"; +import { + OptionItemProps, + RegionItemProps, + TypeItemProps, + TypeWrapperProps, +} from "./SearchSelector.types"; + +export const SearchCardWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 1; + position: relative; + + height: 100%; + + background-color: #ff6c27; + border: 1px solid #e6e6e6; + border-radius: 1rem; + + overflow: hidden; + transition: all 0.5s; + cursor: pointer; + &:hover { + flex: ${({ isType }) => (isType ? "4.5" : "2.5")}; + + background-color: white; + border: 1px solid #ff5100; + + span { + color: #ff5100; + } + } + + span { + min-width: 14rem; + + transition: all 0.5s; + text-align: center; + text-transform: uppercase; + color: white; + font-weight: 700; + letter-spacing: 0.1rem; + } + + img { + width: 3.4rem; + height: 3.4rem; + } + + @media (max-width: 768px) { + flex-direction: row; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + padding: 1rem 0; + + &:hover { + flex-direction: column; + } + } + + @media (max-width: 480px) { + flex-direction: row; + + width: 100%; + height: 100%; + + padding: 1rem 0; + overflow-x: hidden; + + &:hover { + flex-direction: column; + } + } +`; + +export const SelectedItemDisplay = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 30% 0 0; + + width: 100%; + gap: 0.3rem; + + background-color: white; + border-top: 1px solid #e6e6e6; + font-size: 1.3rem; + + @media (max-width: 768px) { + justify-content: center; + align-items: center; + flex: 50% 0 0; + + width: 100%; + height: 100%; + padding: 1rem; + } + + @media (max-width: 480px) { + justify-content: center; + align-items: center; + flex: 50% 0 0; + + width: 100%; + height: 100%; + padding: 1rem; + } +`; + +export const RegionList = styled.div` + display: block; + flex-direction: column; + + width: 60%; +`; + +export const RegionItem = styled.div` + padding: 0.5rem 1rem; + margin-bottom: 0.5rem; + + background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; + border: ${({ selected }) => + selected ? "1px solid #ff5100" : "1px solid #e6e6e6"}; + border-radius: 0.5rem; + + color: ${({ selected }) => (selected ? "white" : "black")}; + + transition: + background-color 0.3s, + color 0.3s; + cursor: pointer; + &:hover { + background-color: #ff5100; + color: white; + } +`; + +export const TypeList = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(6, 1fr); + + gap: 0.2rem; + width: 80%; +`; + +export const TypeItem = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + grid-column: ${({ isFullWidth }) => (isFullWidth ? "1 / -1" : "auto")}; + + padding-left: 1rem; + padding-top: 0.6rem; + padding-bottom: 0.6rem; + + width: auto; + height: 2.2rem; + border-radius: 0.625rem; + border: 1px solid #e6e6e6; + + background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; + color: ${({ selected }) => (selected ? "white" : "black")}; + transition: + background-color 0.3s, + color 0.3s; + cursor: pointer; + &:hover { + background-color: #ff5100; + color: white; + } + + img { + width: 3.4rem; + height: 3.4rem; + border-radius: 0 0.625rem 0.625rem 0; + filter: ${({ selected }) => (selected ? "none" : "grayscale(100%)")}; + transition: filter 0.3s; + } + + &:hover img { + filter: none; + } +`; + +export const OptionList = styled.div` + display: grid; + grid-template-rows: repeat(4, auto); + justify-content: center; + align-items: center; + + gap: 0.4rem; + width: 90%; +`; + +export const OptionItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + padding: 1rem; + + width: 15rem; + height: 3rem; + border-radius: 0.5rem; + border: ${({ selected }) => + selected ? "1px solid #ff5100" : "1px solid #e6e6e6"}; + + background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; + color: ${({ selected }) => (selected ? "white" : "black")}; + + transition: + background-color 0.3s, + color 0.3s; + + cursor: pointer; + &:hover { + background-color: #ff5100; + color: white; + } + + img { + width: 3.5rem; + height: 3.5rem; + + object-fit: contain; + filter: ${({ selected }) => (selected ? "none" : "grayscale(100%)")}; + transition: filter 0.3s; + } + + &:hover img { + filter: none; + } +`; diff --git a/src/components/Search/SearchSelector/SearchSelector.types.ts b/src/components/Search/SearchSelector/SearchSelector.types.ts new file mode 100644 index 0000000..12ea846 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchSelector.types.ts @@ -0,0 +1,49 @@ +export interface SearchType { + region: number | null; + type: number | null; + categoryParking: boolean; + categoryCooking: boolean; + categoryPickup: boolean; +} + +export interface TypeImages { + [key: number]: string; +} + +export interface OptionImages { + [key: number]: string; +} + +export interface RegionItemProps { + selected: boolean; +} + +export interface TypeItemProps { + selected: boolean; + isFullWidth?: boolean; +} + +export interface TypeWrapperProps { + isType?: boolean; + isRegionHovered?: boolean; +} + +export interface OptionItemProps { + selected: boolean; +} + +export interface QueryParams { + [key: string]: string; +} + +export type SearchStateUpdates = Partial<{ + selectedRegion: number; + selectedType: number; + selectedOptions: number[]; + isRegionSelected: boolean; + isRegionHovered: boolean; + isTypeSelected: boolean; + isTypeHovered: boolean; + isOptionsSelected: boolean; + isOptionsHovered: boolean; +}>; diff --git a/src/components/Search/SearchSelector/SearchType/index.test.tsx b/src/components/Search/SearchSelector/SearchType/index.test.tsx new file mode 100644 index 0000000..e87417c --- /dev/null +++ b/src/components/Search/SearchSelector/SearchType/index.test.tsx @@ -0,0 +1,62 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { RecoilRoot } from "recoil"; +import { TypeWrapper } from "../SearchType"; + +const mockHandleTypeClick = jest.fn(); +jest.mock("../../../../hooks/useSearchHandler", () => ({ + useSearchHandlers: () => ({ + handleTypeClick: mockHandleTypeClick, + }), +})); + +const types = [ + { name: "전체 타입", value: 99 }, + { name: "호텔", value: 0 }, + { name: "콘도", value: 1 }, + { name: "호스텔", value: 2 }, + { name: "펜션", value: 3 }, + { name: "모텔", value: 4 }, + { name: "민박", value: 5 }, + { name: "게스트하우스", value: 6 }, + { name: "홈스테이", value: 7 }, + { name: "레지던스", value: 8 }, + { name: "한옥", value: 9 }, +]; + +describe("검색바 테스트 - 숙소 타입", () => { + beforeEach(() => { + mockHandleTypeClick.mockReset(); + }); + + test("마우스 hover시 모든 옵션이 출력되어야 한다", () => { + const { getByText } = render( + + + , + ); + + fireEvent.mouseEnter(getByText("숙소 타입")); + types.forEach((type) => { + expect(getByText(type.name)).toBeInTheDocument(); + }); + }); + + const simulateHover = () => { + render( + + + , + ); + fireEvent.mouseEnter(screen.getByText("숙소 타입")); + }; + + test("숙소 타입은 하나만 선택할 수 있다", async () => { + simulateHover(); + + fireEvent.click(screen.getByText("전체 타입")); + expect(mockHandleTypeClick).toHaveBeenCalledWith(99); + + fireEvent.click(screen.getByText("펜션")); + expect(mockHandleTypeClick).toHaveBeenCalledWith(3); + }); +}); diff --git a/src/components/Search/SearchSelector/SearchType/index.tsx b/src/components/Search/SearchSelector/SearchType/index.tsx new file mode 100644 index 0000000..7f901d2 --- /dev/null +++ b/src/components/Search/SearchSelector/SearchType/index.tsx @@ -0,0 +1,91 @@ +import * as Styled from "../SearchSelector.styles"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { useSearchHandlers } from "../../../../hooks/useSearchHandler"; +import { searchStateAtom } from "../../../../store/searchSelectorAtom"; +import AccomIcon from "../../../../assets/svg/accom-icon.svg"; +import Type0 from "../../../../assets/image/type0.png"; +import Type1 from "../../../../assets/image/type1.jpeg"; +import Type2 from "../../../../assets/image/type2.jpg"; +import Type3 from "../../../../assets/image/type3.jpg"; +import Type4 from "../../../../assets/image/type4.jpg"; +import Type5 from "../../../../assets/image/type5.jpg"; +import Type6 from "../../../../assets/image/type6.png"; +import Type7 from "../../../../assets/image/type7.jpg"; +import Type8 from "../../../../assets/image/type8.png"; +import Type9 from "../../../../assets/image/type9.png"; +import { TypeImages } from "../SearchSelector.types"; + +const typeImages: TypeImages = { + 0: Type0, + 1: Type1, + 2: Type2, + 3: Type3, + 4: Type4, + 5: Type5, + 6: Type6, + 7: Type7, + 8: Type8, + 9: Type9, +}; + +type Type = { + name: string; + value: number; +}; + +type TypeWrapperProps = { + types: Type[]; +}; + +export const TypeWrapper = ({ types }: TypeWrapperProps) => { + const searchState = useRecoilValue(searchStateAtom); + const setSearchState = useSetRecoilState(searchStateAtom); + const { handleTypeClick } = useSearchHandlers(); + + return ( + <> + + setSearchState((current) => ({ ...current, isTypeHovered: true })) + } + onMouseLeave={() => + setSearchState((current) => ({ ...current, isTypeHovered: false })) + } + isType={true} + > + + Check 2

숙소 타입

+ {!searchState.isTypeHovered && ( + type-selector + )} +
+ {searchState.isTypeHovered && ( + + {types.map((type) => ( + handleTypeClick(type.value)} + isFullWidth={type.name === "전체 타입"} + > + {type.name} + {type.name !== "전체 타입" && ( + {type.name} + )} + + ))} + + )} + {!searchState.isTypeHovered && searchState.isTypeSelected && ( + + { + types.find((type) => type.value === searchState.selectedType) + ?.name + }{" "} + ✔️ + + )} +
+ + ); +}; diff --git a/src/components/SearchListBanner/SearchList.styles.ts b/src/components/SearchListBanner/SearchList.styles.ts index 91f78c4..aeeef72 100644 --- a/src/components/SearchListBanner/SearchList.styles.ts +++ b/src/components/SearchListBanner/SearchList.styles.ts @@ -1,32 +1,37 @@ import styled from "@emotion/styled"; import { motion } from "framer-motion"; +import { CategoryViewButtonWrapper } from "../Category/CategoryFilter/CategoryFilter.styles"; export const SearchListContainer = styled.div` - position: absolute; - top: 0; - display: flex; justify-content: center; width: 100%; - padding: 0.5rem; + padding: 1rem; margin: 0 auto; color: #222; - background-color: white; box-sizing: border-box; - border-radius: 5px; filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.25)); - - transform: translateY(-50%); `; - export const SearchListWrapper = styled.div` display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; `; +export const SearchListQueryText = styled.h1` + font-size: 1.5rem; + font-weight: 700; + color: #222; +`; + +export const SearchListButtonWrapper = + CategoryViewButtonWrapper.withComponent("div"); + export const SearchListButton = styled(motion.div)` padding: 0.5rem 1rem; diff --git a/src/components/SearchListBanner/index.test.tsx b/src/components/SearchListBanner/index.test.tsx new file mode 100644 index 0000000..e8a9999 --- /dev/null +++ b/src/components/SearchListBanner/index.test.tsx @@ -0,0 +1,74 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import SearchListBanner from "."; +import userEvent from "@testing-library/user-event"; +import { createWrapper } from "../../test/test.utils"; + +describe("SearchListBanner", () => { + it("should render the component", () => { + const wrapper = createWrapper(); + + render( + , + + { wrapper }, + ); + expect(screen.getByText("픽업가능")).toBeInTheDocument(); + }); + + it("검색결과가 없을 때 모든 숙소의 검색 결과입니다. 라는 문구가 보여야 합니다.", () => { + const wrapper = createWrapper(); + + render( + , + + { wrapper }, + ); + expect( + screen.getByText("모든 숙소의 검색 결과입니다."), + ).toBeInTheDocument(); + }); + + it("region이 0일때(서울), 숙소를 클릭하면 /results?page=0®ion=0 로 이동해야 합니다.", () => { + const wrapper = createWrapper(); + const router = jest.fn(); + const user = userEvent; + + render( + , + + { wrapper }, + ); + + const button = screen.getByText("서울시"); + user.click(button); + + waitFor(() => { + expect(button).toBeInTheDocument(); + expect(router).toHaveBeenCalledWith("/results?page=0®ion=0"); + }); + }); +}); diff --git a/src/components/SearchListBanner/index.tsx b/src/components/SearchListBanner/index.tsx index 70be591..1556343 100644 --- a/src/components/SearchListBanner/index.tsx +++ b/src/components/SearchListBanner/index.tsx @@ -4,11 +4,21 @@ import { filterTextDecoder, } from "../../utils/filterTextDecoder"; import * as Styled from "./SearchList.styles"; +import { useRecoilState } from "recoil"; +import { categoryViewAtom } from "../../store/categoryViewAtom"; +import CategoryFilterViewButton from "../Category/CategoryFilter/CategoryFilterViewButton"; const SearchListBanner = ({ validParams }: SearchListBannerProps) => { const validArray = filterTextDecoder(validParams); const router = useNavigate(); + const [categoryViewState, setCategoryViewState] = + useRecoilState(categoryViewAtom); + + const handleClick = () => { + setCategoryViewState((prev) => !prev); + }; + const handleClickSearch = (url: string) => { router(`/results?page=0${url}`); }; @@ -16,13 +26,28 @@ const SearchListBanner = ({ validParams }: SearchListBannerProps) => { return ( + 보기 방식 + + + + + 검색 조건 {validArray.length !== 0 ? ( validArray.map((item) => { return ( handleClickSearch(item.url)} - whileHover={{ scale: 1.1 }} + whileHover={{ scale: 1.02 }} transition={{ duration: 0.1, type: "spring", diff --git a/src/hooks/useAlert.ts b/src/hooks/useAlert.ts new file mode 100644 index 0000000..5b12763 --- /dev/null +++ b/src/hooks/useAlert.ts @@ -0,0 +1,22 @@ +import Swal from "sweetalert2"; + +export const useAuthAlert = () => { + Swal.fire({ + title: "인증이 만료되었습니다.", + text: "다시 로그인하시겠습니까?", + padding: "50px", + confirmButtonColor: "#FF5100", + confirmButtonText: "로그인", + showDenyButton: true, + denyButtonText: "회원가입", + denyButtonColor: "#001650", + }).then((result) => { + if (result.isConfirmed) { + location.href = "/signin"; + } else if (result.isDenied) { + location.href = "/signup"; + } else if (result.isDismissed) { + location.href = "/signin"; + } + }); +}; diff --git a/src/hooks/useAxios.ts b/src/hooks/useAxios.ts new file mode 100644 index 0000000..ae7a58c --- /dev/null +++ b/src/hooks/useAxios.ts @@ -0,0 +1,91 @@ +import axios, { + AxiosError, + AxiosInstance, + InternalAxiosRequestConfig, +} from "axios"; +import { getTokenRefresh } from "../utils/getTokenRefresh"; +import { useAuthAlert as swal } from "./useAlert"; + +// 토큰 추가 함수 +const addTokenToHeader = async (config: InternalAxiosRequestConfig) => { + const token = await getTokenRefresh(); + + if (!token) { + swal(); + } + config.headers.access_token = token; + + return config; +}; + +// 에러 핸들링 함수 +const logErrorInterceptor = (error: AxiosError) => { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response) { + console.log("response가 있는 경우", error); + return Promise.reject(axiosError.response); + } else if (axiosError.request) { + return Promise.reject(axiosError.request); + } else { + return Promise.reject(axiosError.message); + } + } + return Promise.reject(error); +}; + +// 인증 필요 에러 핸들링 함수 +const logAuthErrorInterceptor = (error: AxiosError) => { + return Promise.reject(error); +}; + +// 인증 필요 없는 경우 +const baseInterceptors = (instance: AxiosInstance) => { + instance.interceptors.request.use( + (config) => config, + (error) => logErrorInterceptor(error), + ); + instance.interceptors.response.use( + (response) => response, + (error) => logErrorInterceptor(error), + ); +}; + +// 인증 필요한 경우 +const authInterceptors = (instance: AxiosInstance) => { + instance.interceptors.request.use( + (config) => addTokenToHeader(config), + (error) => logAuthErrorInterceptor(error), + ); + instance.interceptors.response.use( + (response) => response, + (error) => logAuthErrorInterceptor(error), + ); +}; + +// axios인스턴스 생성 (인증x) +const base = () => { + const instance = axios.create({ + baseURL: "/api", + timeout: 1000, + }); + + baseInterceptors(instance); + + return instance; +}; + +// axios인스턴스 생성 (인증o) +const auth = () => { + const instance = axios.create({ + baseURL: "/api", + timeout: 1000, + }); + + authInterceptors(instance); + + return instance; +}; + +export const baseInstance = base(); +export const authInstance = auth(); diff --git a/src/hooks/useCartFetch.ts b/src/hooks/useCartFetch.ts index 8c178ff..6205847 100644 --- a/src/hooks/useCartFetch.ts +++ b/src/hooks/useCartFetch.ts @@ -1,10 +1,10 @@ import { useMutation, useQuery } from "@tanstack/react-query"; -import axios from "axios"; +import { authInstance } from "./useAxios"; // 카트 목록 가져오기 export const useGetMyCart = () => { return useQuery({ - queryFn: () => axios.get("/api/v1/baskets"), + queryFn: () => authInstance.get("/baskets"), queryKey: ["cart"], }); }; @@ -12,9 +12,8 @@ export const useGetMyCart = () => { // 카트 선택 상품 주문요청 export const usePostOrders = () => { return useMutation({ - mutationFn: (data: { id: number }) => { - console.log(data); - return axios.post("/api/v1/baskets/orders"); + mutationFn: (data: { ids: number[] }) => { + return authInstance.post("/baskets/orders", data); }, }); }; @@ -22,8 +21,9 @@ export const usePostOrders = () => { // 카트 아이템 삭제 export const useDeleteCartItem = () => { return useMutation({ - mutationFn: (data: { room_basket_id: number }) => { - return axios.delete("/api/v1/baskets", { data }); + mutationFn: (data: { ids: number[] }) => { + console.log(data); + return authInstance.put("/baskets", { data }); }, }); }; diff --git a/src/hooks/useCategoryInfiniteQuery.ts b/src/hooks/useCategoryInfiniteQuery.ts index 725e1ce..8a3c871 100644 --- a/src/hooks/useCategoryInfiniteQuery.ts +++ b/src/hooks/useCategoryInfiniteQuery.ts @@ -6,18 +6,18 @@ import { checkCategoryQueryUrl } from "../utils/filterDecoder"; export const useCategoryInfiniteQuery = ({ region, type, - category_parking, - category_cooking, - category_pickup, + categoryParking, + categoryCooking, + categoryPickup, }: CategoryFilterParams) => { const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({ queryKey: [ "category", region, type, - category_parking, - category_cooking, - category_pickup, + categoryParking, + categoryCooking, + categoryPickup, ], queryFn: ({ pageParam }) => fetchCatgory({ @@ -25,13 +25,14 @@ export const useCategoryInfiniteQuery = ({ ...checkCategoryQueryUrl({ region, type, - category_parking, - category_cooking, - category_pickup, + categoryParking, + categoryCooking, + categoryPickup, }), }), initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (allPages.length === 0) return undefined; if (lastPage.data.length === 0) return undefined; return lastPageParam + 1; }, diff --git a/src/hooks/useDetailFetch.ts b/src/hooks/useDetailFetch.ts new file mode 100644 index 0000000..de44495 --- /dev/null +++ b/src/hooks/useDetailFetch.ts @@ -0,0 +1,58 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { authInstance, baseInstance } from "./useAxios"; + +// 숙소 상세 정보 조회 +export const useGetAccommodationDetail = (accommodationId: number) => { + return useQuery({ + queryKey: ["accommodationDetail", accommodationId], + // queryFn: () => baseInstance.get("/accommodations/${accommodationId}"), + queryFn: () => baseInstance.get(`/accommodations/${accommodationId}`), + }); +}; + +// 객실 상세 정보 조회 +export const useGetRoomDetail = (roomId: number) => { + return useQuery({ + queryKey: ["roomDetail", roomId], + // queryFn: () => baseInstance.get("rooms/${roomId}"), + queryFn: () => baseInstance.get(`/rooms/${roomId}`), + }); +}; + +// 상품 장바구니에 담기 +export const usePostRoomToCart = () => { + return useMutation({ + mutationFn: (data: { + checkInAt: string; + checkOutAt: string; + numberOfGuests: number; + roomId: number; + }) => { + // return authInstance.post("/rooms/${data.roomId}/baskets", { + return authInstance.post(`/rooms/${data.roomId}/baskets`, { + checkInAt: data.checkInAt, + checkOutAt: data.checkOutAt, + numberOfGuests: data.numberOfGuests, + }); + }, + }); +}; + +// 단일 상품 주문 +export const usePostOrder = () => { + return useMutation({ + mutationFn: (data: { + checkInAt: string; + checkOutAt: string; + numberOfGuests: number; + roomId: number; + }) => { + return authInstance.post(`/rooms/${data.roomId}/orders`, { + // return authInstance.post("/rooms/${roomId}/orders", { + checkInAt: data.checkInAt, + checkOutAt: data.checkOutAt, + numberOfGuests: data.numberOfGuests, + }); + }, + }); +}; diff --git a/src/hooks/useGeolocation.ts b/src/hooks/useGeolocation.ts new file mode 100644 index 0000000..268107f --- /dev/null +++ b/src/hooks/useGeolocation.ts @@ -0,0 +1,123 @@ +import axios from "axios"; +import { useEffect, useState } from "react"; + +interface Location { + isLoading: boolean; + coordinate?: { lat: number; lng: number }; + error?: { code: number; message: string }; + cityCode: number; +} + +// reverseGeocoded 에서 요청받은 데이터의 타입 입니다. 참고 하셔도 될 거 같아요!! +// type ReverseGeocodedData = { +// latitude: number; +// longitude: number; +// continent: string; +// lookupSource: string; +// continentCode: string; +// localityLanguageRequested: string; +// city: string; +// countryName: string; +// countryCode: string; +// postcode: string; +// principalSubdivision: string; +// principalSubdivisionCode: string; +// plusCode: string; +// locality: string; +// localityInfo: object; +// }; + +// 좌표값 받아서 해당 도시의 검색 코드 리턴하는 함수 +const Geocoded = async ( + latitude: string, + longitude: string, +): Promise => { + const reverseGeocoded = await axios.get( + `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}`, + ); + + if ( + reverseGeocoded.data.localityInfo.administrative[1].name === "Gyeonggi-do" + ) + return 1; + if (reverseGeocoded.data.localityInfo.administrative[1].name === "Gangwon") + return 2; + if ( + reverseGeocoded.data.localityInfo.administrative[1].name === + "Chungcheongnam-do" || + reverseGeocoded.data.localityInfo.administrative[1].name === + "Chungcheongbuk-do" + ) + return 3; + if ( + reverseGeocoded.data.localityInfo.administrative[1].name === + "Jeollanam-do" || + reverseGeocoded.data.localityInfo.administrative[1].name === "Jeollabuk-do" + ) + return 4; + if ( + reverseGeocoded.data.localityInfo.administrative[1].name === + "Gyeongsangnam-do" && + reverseGeocoded.data.localityInfo.administrative[1].name === + "Gyeongsangbuk-do" + ) + return 5; + if (reverseGeocoded.data.localityInfo.administrative[1].name === "Jeju") + return 6; + + return reverseGeocoded.data.localityInfo.administrative[1].name; +}; + +// 위치정보 저장하는 훅 +const useGeolocation = () => { + // 위치 정보 상태 + const [location, setLocation] = useState({ + isLoading: false, + coordinate: { lat: 0, lng: 0 }, //좌표값 + cityCode: 0, // 지역 검색코드 + }); + + // 성공하면 위치 정보 상태 저장 + const onSuccess = async (location: { + coords: { latitude: number; longitude: number }; + }) => { + await Geocoded( + location.coords.latitude.toString(), + location.coords.longitude.toString(), + ).then((res) => { + setLocation({ + isLoading: true, + coordinate: { + lat: location.coords.latitude, + lng: location.coords.longitude, + }, + cityCode: res, + }); + }); + }; + + // 에러(위치정보 동의 X -> 위치정보 없음)시 상태 저장 + const onError = (error: { code: number; message: string }) => { + setLocation({ + isLoading: true, + error, + cityCode: 0, + }); + }; + + useEffect(() => { + // 위치 정보 없으면 + if (!("geolocation" in navigator)) { + onError({ + code: 0, + message: "위치정보 없음", + }); + } + // 위치 정보 있으면 + navigator.geolocation.getCurrentPosition(onSuccess, onError); + }, []); + + return location; +}; + +export default useGeolocation; diff --git a/src/hooks/useGetCompleteData.ts b/src/hooks/useGetCompleteData.ts new file mode 100644 index 0000000..987445b --- /dev/null +++ b/src/hooks/useGetCompleteData.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { authInstance } from "./useAxios"; + +export const useGetCompleteData = (id: string) => { + return useQuery({ + queryFn: () => authInstance.get(`/payment/${id}`), + queryKey: ["complete"], + }); +}; diff --git a/src/hooks/useGetOrderList.ts b/src/hooks/useGetOrderList.ts new file mode 100644 index 0000000..b6bddf3 --- /dev/null +++ b/src/hooks/useGetOrderList.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { authInstance } from "./useAxios"; + +// 주문 목록 가져오기 +export const useGetOrderList = (orderId: number) => { + return useQuery({ + queryFn: () => authInstance.get(`/orders/${orderId}`), + queryKey: ["orderList"], + }); +}; diff --git a/src/hooks/useGetValidParams.ts b/src/hooks/useGetValidParams.ts index 79c2f3c..2f9cbe4 100644 --- a/src/hooks/useGetValidParams.ts +++ b/src/hooks/useGetValidParams.ts @@ -12,9 +12,9 @@ const useGetValidParams = () => { const validParams = { region: getNumberParam("region", 99), type: getNumberParam("type", 99), - category_cooking: getNumberParam("category_cooking", 2), - category_parking: getNumberParam("category_parking", 2), - category_pickup: getNumberParam("category_pickup", 2), + categoryCooking: getNumberParam("categoryCooking", 2), + categoryParking: getNumberParam("categoryParking", 2), + categoryPickup: getNumberParam("categoryPickup", 2), }; return validParams; diff --git a/src/hooks/useMainListFetch.ts b/src/hooks/useMainListFetch.ts new file mode 100644 index 0000000..8e70d98 --- /dev/null +++ b/src/hooks/useMainListFetch.ts @@ -0,0 +1,77 @@ +import { useQuery } from "@tanstack/react-query"; +import { baseInstance } from "./useAxios"; +import { AccommodationData } from "../components/Main/AccommodationList/AccommodationList.types"; + +interface ListDataResponse { + data: AccommodationData[]; +} + +// 받아온 숙소 데이터를 무작위로 셔플 +function shuffleList(array: AccommodationData[]): AccommodationData[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +// 내 위치 기반 주변 숙소 리스트 출력 +export const useFetchAccomByRegion = (cityCode: number) => { + return useQuery({ + queryKey: ["accommodations", "byRegion", cityCode], + queryFn: async () => { + const data = await baseInstance.get( + `accommodations?region01=${cityCode}`, + ); + + const shuffledData = shuffleList(data.data.data); + const selectedData = shuffledData.slice(0, 3); + + console.log("내 지역코드", cityCode); + return selectedData; + }, + }); +}; + +// 가장 많은 찜을 받은 숙소 리스트 출력 +export const useFetchTopLikedAccom = () => { + return useQuery({ + queryKey: ["accommodations", "topLiked"], + queryFn: async () => { + const data = await baseInstance.get("accommodations"); + const listData = data.data.data; + const filteredData = listData.filter((item) => item.wishCount > 1); + const topLikedData = filteredData.sort( + (a, b) => b.wishCount - a.wishCount, + ); + console.log("가장 많은 좋아요 숙소", topLikedData); + return topLikedData; + }, + }); +}; + +// 주차 가능 숙소 리스트 출력 +export const useFetchAccomWithParking = () => { + return useQuery({ + queryKey: ["accommodations", "withParking"], + queryFn: async () => { + const data = await baseInstance.get( + "accommodations?category-parking=1", + ); + const shuffledData = shuffleList(data.data.data); + const selectedData = shuffledData.slice(0, 3); + return selectedData; + }, + }); +}; + +// 모든 숙소 리스트 출력 +export const useFetchAllAccommodations = () => { + return useQuery({ + queryKey: ["accommodations", "all"], + queryFn: async () => { + const data = await baseInstance.get("accommodations"); + return data.data.data; + }, + }); +}; diff --git a/src/hooks/usePayment.ts b/src/hooks/usePayment.ts new file mode 100644 index 0000000..4407f45 --- /dev/null +++ b/src/hooks/usePayment.ts @@ -0,0 +1,42 @@ +import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; +import { authInstance } from "./useAxios"; +// 결제 생성 +export const usePayment = () => { + return useMutation({ + mutationFn: (orderId: number) => { + return authInstance.post(`/orders/${orderId}/payments`); + }, + }); +}; + +//결제 목록 불러오기 +export const useGetMyOrder = () => { + return useQuery({ + queryFn: () => authInstance.get("/payment"), + queryKey: ["payment"], + }); +}; + +// 결제 목록 불러오기(무한스크롤) +export const useGetMyOrderInfinite = () => { + return useInfiniteQuery({ + queryKey: ["payment"], + queryFn: ({ pageParam = 1 }) => + authInstance.get(`/payment?page=${pageParam}&pageSize=20`), + getNextPageParam: (lastPage, allPages) => { + // 만약 더 불러올 페이지가 있다면 페이지 번호를 반환함 + return lastPage.data.length === 20 ? allPages.length + 1 : undefined; + }, + // initialPageParam 속성 추가 + initialPageParam: 1, + }); +}; + +// 주문 취소 +export const useDeleteOrder = () => { + return useMutation({ + mutationFn: (orderId: number) => { + return authInstance.delete(`/orders/${orderId}`); + }, + }); +}; diff --git a/src/hooks/useSearchHandler.ts b/src/hooks/useSearchHandler.ts new file mode 100644 index 0000000..9387ba0 --- /dev/null +++ b/src/hooks/useSearchHandler.ts @@ -0,0 +1,118 @@ +import { useRecoilState } from "recoil"; +import { searchStateAtom } from "../store/searchSelectorAtom"; + +export const useSearchHandlers = () => { + const [searchState, setSearchState] = useRecoilState(searchStateAtom); + + const handleRegionClick = (value: number) => { + setSearchState({ + ...searchState, + selectedRegion: value, + isRegionSelected: value !== 99, + }); + }; + + const handleTypeClick = (value: number) => { + setSearchState({ + ...searchState, + selectedType: value, + isTypeSelected: value !== 99, + }); + }; + + const handleOptionClick = (value: number) => { + setSearchState((current) => { + if (value === 99) { + return { + ...current, + selectedOptions: [99], + isOptionsSelected: true, + }; + } else { + const newOptions = current.selectedOptions.includes(value) + ? current.selectedOptions.filter( + (option) => option !== value && option !== 99, + ) + : [ + ...current.selectedOptions.filter((option) => option !== 99), + value, + ]; + + return { + ...current, + selectedOptions: newOptions, + isOptionsSelected: newOptions.length > 0, + }; + } + }); + }; + + const handleRegionMouseEnter = () => { + setSearchState({ + ...searchState, + isRegionHovered: true, + }); + }; + + const handleRegionMouseLeave = () => { + setSearchState({ + ...searchState, + isRegionHovered: false, + }); + }; + + const handleTypeMouseEnter = () => { + setSearchState({ + ...searchState, + isTypeHovered: true, + }); + }; + + const handleTypeMouseLeave = () => { + setSearchState({ + ...searchState, + isTypeHovered: false, + }); + }; + + const handleOptionMouseEnter = () => { + setSearchState({ + ...searchState, + isOptionsHovered: true, + }); + }; + + const handleOptionMouseLeave = () => { + setSearchState({ + ...searchState, + isOptionsHovered: false, + }); + }; + + const handleResetSearch = () => { + setSearchState({ + selectedRegion: 99, + selectedType: 99, + selectedOptions: [99], + isRegionSelected: false, + isTypeSelected: false, + isOptionsSelected: false, + isOptionsHovered: false, + isRegionHovered: false, + isTypeHovered: false, + }); + }; + + return { + handleRegionClick, + handleTypeClick, + handleOptionClick, + handleRegionMouseEnter, + handleRegionMouseLeave, + handleTypeMouseEnter, + handleTypeMouseLeave, + handleOptionMouseEnter, + handleOptionMouseLeave, + handleResetSearch, + }; +}; diff --git a/src/hooks/useSendSearchQuery.ts b/src/hooks/useSendSearchQuery.ts new file mode 100644 index 0000000..5bd25fe --- /dev/null +++ b/src/hooks/useSendSearchQuery.ts @@ -0,0 +1,41 @@ +import { useRecoilValue } from "recoil"; +import { searchStateAtom } from "../store/searchSelectorAtom"; +import { useNavigate } from "react-router-dom"; +import { QueryParams } from "../components/Search/SearchSelector/SearchSelector.types"; + +export const useSendSearchQuery = () => { + const searchState = useRecoilValue(searchStateAtom); + const navigate = useNavigate(); + + const sendSearchQuery = () => { + const optionsMap: Record = { + 0: "categoryParking", + 1: "categoryCooking", + 2: "categoryPickup", + }; + + const queryParams: Partial = {}; + + searchState.selectedOptions.forEach((optionValue) => { + if (optionValue !== 99) { + const key = optionsMap[optionValue]; + queryParams[key] = "1"; + } + }); + + if (searchState.selectedRegion !== 99) { + queryParams["region"] = searchState.selectedRegion.toString(); + } + if (searchState.selectedType !== 99) { + queryParams["type"] = searchState.selectedType.toString(); + } + + const queryString = new URLSearchParams( + queryParams as Record, + ).toString(); + + navigate(`/results?${queryString}`); + }; + + return sendSearchQuery; +}; diff --git a/src/hooks/useWishControl.ts b/src/hooks/useWishControl.ts index 902e541..e41fcf2 100644 --- a/src/hooks/useWishControl.ts +++ b/src/hooks/useWishControl.ts @@ -23,7 +23,9 @@ export const useWishControl = ({ queryFnType }: WishControlParams) => { console.log(data); }, onError: (error) => { - console.log(error); + if (error.message === "Request failed with status code 400") { + alert("이미 즐겨찾기에 추가된 숙소입니다."); + } }, }); diff --git a/src/main.tsx b/src/main.tsx index 42a8fc9..0fcb0cb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,22 +6,24 @@ import { RecoilRoot } from "recoil"; import App from "./App.tsx"; import "./index.css"; -async function deferRender() { - const { worker } = await import("./mocks/browser/browser.ts"); - return worker.start(); -} +// async function deferRender() { +// const { worker } = await import("./mocks/browser/browser.ts"); +// return worker.start(); +// } -deferRender().then(() => { - const queryClient = new QueryClient(); - - ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - - - - , - ); +// deferRender().then(() => { +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, }); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + , +); +// }); diff --git a/src/mock/detailListPageData.ts b/src/mock/detailListPageData.ts new file mode 100644 index 0000000..af303b1 --- /dev/null +++ b/src/mock/detailListPageData.ts @@ -0,0 +1,202 @@ +export const accommodationDetail = { + id: 1, + name: "AccommodationName", + type: "Hotel", + address: "경상남도 하동군 화개로 13", + phoneNumber: "123-456-7890", + homepage: "www.accommodation1.com", + infoDetail: "This is a detail for Accommodation1", + thumbnailUrl: + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/2c0134f2-6ba3-434c-8dca-6d5831bf6e24", + categoryParking: true, + categoryCooking: false, + categoryPickup: false, + categoryAmenities: false, + categoryDiningArea: true, + checkIn: "14:00:00", + checkOut: "11:00:00", + wishCount: 10, + viewCount: 101, + lowest_price: 100, + rooms: [ + { + id: 1, + name: "디럭스룸", + price: 390000, + capacity: 2, + // inventory: 0, + categoryTv: true, + categoryPc: false, + categoryInternet: true, + categoryRefrigerator: true, + categoryBathingFacilities: false, + categoryDryer: true, + room_image_url: [ + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/2c0134f2-6ba3-434c-8dca-6d5831bf6e24", + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/fd904255-0d68-46df-a091-18d6efc6427f", + ], + RoomInventory: [ + { date: "23-11-28", inventory: 0 }, + { date: "23-11-29", inventory: 7 }, + { date: "23-11-30", inventory: 3 }, + { date: "23-12-01", inventory: 0 }, + { date: "23-12-02", inventory: 5 }, + { date: "23-12-03", inventory: 5 }, + { date: "23-12-04", inventory: 5 }, + { date: "23-12-05", inventory: 5 }, + { date: "23-12-06", inventory: 5 }, + { date: "23-12-07", inventory: 5 }, + { date: "23-12-08", inventory: 5 }, + { date: "23-12-09", inventory: 5 }, + { date: "23-12-10", inventory: 5 }, + { date: "23-11-27", inventory: 2 }, + ], + }, + { + id: 2, + name: "Room2", + price: 200, + capacity: 3, + // inventory: 5, + categoryTv: false, + categoryPc: true, + categoryInternet: false, + categoryRefrigerator: true, + categoryBathingFacilities: true, + categoryDryer: false, + RoomInventory: [ + { date: "23-11-28", inventory: 1 }, + { date: "23-11-29", inventory: 0 }, + { date: "23-11-30", inventory: 8 }, + { date: "23-12-01", inventory: 9 }, + { date: "23-12-02", inventory: 4 }, + { date: "23-12-03", inventory: 3 }, + { date: "23-12-04", inventory: 1 }, + { date: "23-12-05", inventory: 2 }, + { date: "23-12-06", inventory: 5 }, + { date: "23-12-07", inventory: 5 }, + { date: "23-12-08", inventory: 5 }, + { date: "23-12-09", inventory: 5 }, + { date: "23-12-10", inventory: 5 }, + { date: "23-11-27", inventory: 2 }, + ], + }, + + { + id: 3, + name: "Room3", + price: 200, + capacity: 3, + // inventory: 5, + categoryTv: false, + categoryPc: true, + categoryInternet: false, + categoryRefrigerator: true, + categoryBathingFacilities: true, + categoryDryer: false, + RoomInventory: [ + { date: "23-11-28", inventory: 3 }, + { date: "23-11-29", inventory: 1 }, + { date: "23-11-30", inventory: 5 }, + { date: "23-12-01", inventory: 5 }, + { date: "23-12-02", inventory: 5 }, + { date: "23-12-03", inventory: 5 }, + { date: "23-12-04", inventory: 5 }, + { date: "23-12-05", inventory: 5 }, + { date: "23-12-06", inventory: 5 }, + { date: "23-12-07", inventory: 5 }, + { date: "23-12-08", inventory: 5 }, + { date: "23-12-09", inventory: 5 }, + { date: "23-12-10", inventory: 5 }, + { date: "23-11-27", inventory: 2 }, + ], + }, + // { + // id: 4, + // name: "Room4", + // price: 200, + // capacity: 3, + // inventory: 5, + // categoryTv: false, + // categoryPc: true, + // categoryInternet: false, + // categoryRefrigerator: true, + // categoryBathingFacilities: true, + // categoryDryer: false, + // RoomInventory: [ + // { date: "23-11-28", inventory: 1 }, + // { date: "23-11-29", inventory: 1 }, + // { date: "23-11-30", inventory: 5 }, + // { date: "23-12-01", inventory: 5 }, + // { date: "23-12-02", inventory: 5 }, + // { date: "23-12-03", inventory: 5 }, + // { date: "23-12-04", inventory: 5 }, + // { date: "23-12-05", inventory: 5 }, + // { date: "23-12-06", inventory: 5 }, + // { date: "23-12-07", inventory: 5 }, + // { date: "23-12-08", inventory: 5 }, + // { date: "23-12-09", inventory: 5 }, + // { date: "23-12-10", inventory: 5 }, + // { date: "23-11-27", inventory: 2 }, + // ], + // }, + // { + // id: 5, + // name: "Room5", + // price: 200, + // capacity: 3, + // inventory: 5, + // categoryTv: false, + // categoryPc: true, + // categoryInternet: false, + // categoryRefrigerator: true, + // categoryBathingFacilities: true, + // categoryDryer: false, + // RoomInventory: [ + // { date: "23-11-28", inventory: 1 }, + // { date: "23-11-29", inventory: 1 }, + // { date: "23-11-30", inventory: 5 }, + // { date: "23-12-01", inventory: 5 }, + // { date: "23-12-02", inventory: 5 }, + // { date: "23-12-03", inventory: 5 }, + // { date: "23-12-04", inventory: 5 }, + // { date: "23-12-05", inventory: 5 }, + // { date: "23-12-06", inventory: 5 }, + // { date: "23-12-07", inventory: 5 }, + // { date: "23-12-08", inventory: 5 }, + // { date: "23-12-09", inventory: 5 }, + // { date: "23-12-10", inventory: 5 }, + // { date: "23-11-27", inventory: 2 }, + // ], + // }, + // { + // id: 6, + // name: "Room6", + // price: 200, + // capacity: 3, + // inventory: 5, + // categoryTv: false, + // categoryPc: true, + // categoryInternet: false, + // categoryRefrigerator: true, + // categoryBathingFacilities: true, + // categoryDryer: false, + // RoomInventory: [ + // { date: "23-11-28", inventory: 1 }, + // { date: "23-11-29", inventory: 1 }, + // { date: "23-11-30", inventory: 5 }, + // { date: "23-12-01", inventory: 5 }, + // { date: "23-12-02", inventory: 5 }, + // { date: "23-12-03", inventory: 5 }, + // { date: "23-12-04", inventory: 5 }, + // { date: "23-12-05", inventory: 5 }, + // { date: "23-12-06", inventory: 5 }, + // { date: "23-12-07", inventory: 5 }, + // { date: "23-12-08", inventory: 5 }, + // { date: "23-12-09", inventory: 5 }, + // { date: "23-12-10", inventory: 5 }, + // { date: "23-11-27", inventory: 2 }, + // ], + // }, + ], +}; diff --git a/src/mock/detailPageData.ts b/src/mock/detailPageData.ts new file mode 100644 index 0000000..a1b0ecb --- /dev/null +++ b/src/mock/detailPageData.ts @@ -0,0 +1,68 @@ +export const roomDetail = { + id: 1, + accommodation_name: "그랜드 하얏트 제주", + room_name: "오션뷰 스위트룸", + price: 390000, + stock: 5, + room_image_url: [ + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/2c0134f2-6ba3-434c-8dca-6d5831bf6e24", + "https://github.com/Yanolza-Miniproject/frontend/assets/92326949/fd904255-0d68-46df-a091-18d6efc6427f", + ], +}; + +// export const rooms = [ +// { +// accommodation_name: "호텔이름은 이거", +// id: 1, +// name: "Room1", +// price: 100, +// capacity: 2, +// inventory: 10, +// categoryTv: true, +// categoryPc: false, +// categoryInternet: true, +// categoryRefrigerator: true, +// categoryBathingFacilities: false, +// categoryDryer: true, +// room_image_url: [ +// "https://example.com/room1/image1.jpg", +// "https://example.com/room1/image2.jpg", +// ], +// }, +// { +// id: 2, +// accommodation_name: "펜션이름 어쩌구", +// name: "Room2", +// price: 120, +// capacity: 2, +// inventory: 8, +// categoryTv: true, +// categoryPc: true, +// categoryInternet: true, +// categoryRefrigerator: false, +// categoryBathingFacilities: true, +// categoryDryer: false, +// room_image_url: [ +// "https://example.com/room2/image1.jpg", +// "https://example.com/room2/image2.jpg", +// ], +// }, +// { +// id: 10, +// accommodation_name: "제주도 게스트하우스", +// name: "Room10", +// price: 200, +// capacity: 4, +// inventory: 5, +// categoryTv: true, +// categoryPc: true, +// categoryInternet: true, +// categoryRefrigerator: true, +// categoryBathingFacilities: true, +// categoryDryer: true, +// room_image_url: [ +// "https://example.com/room10/image1.jpg", +// "https://example.com/room10/image2.jpg", +// ], +// }, +// ]; diff --git a/src/mock/myPageData.ts b/src/mock/myPageData.ts index b8e353b..4fa3d7f 100644 --- a/src/mock/myPageData.ts +++ b/src/mock/myPageData.ts @@ -280,13 +280,13 @@ export const likeData = [ type: 3, name: "제주 컬리넌 호텔", address: "주소", - phone_number: "연락처", + phoneNumber: "연락처", homepage: "홈페이지", info_detail: "상세소개", thumbnail_url: "섬네일 주소(이미지 저장 url)", - category_parking: 0, - category_cooking: 1, - category_pickup: 1, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, category_amenities: 0, category_dining_area: 1, check_in: "체크인 시간", @@ -299,13 +299,13 @@ export const likeData = [ type: 3, name: "라마다 제주시티 호텔", address: "주소", - phone_number: "연락처", + phoneNumber: "연락처", homepage: "홈페이지", info_detail: "상세소개", thumbnail_url: "섬네일 주소(이미지 저장 url)", - category_parking: 0, - category_cooking: 1, - category_pickup: 1, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, category_amenities: 0, category_dining_area: 1, check_in: "체크인 시간", @@ -318,13 +318,13 @@ export const likeData = [ type: 2, name: "제주 헤이, 서귀포", address: "주소", - phone_number: "연락처", + phoneNumber: "연락처", homepage: "홈페이지", info_detail: "상세소개", thumbnail_url: "섬네일 주소(이미지 저장 url)", - category_parking: 0, - category_cooking: 1, - category_pickup: 1, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, category_amenities: 0, category_dining_area: 1, check_in: "체크인 시간", @@ -452,3 +452,118 @@ export const roomDetail = { }, ], }; + +export const paymentData = [ + { + payment_id: 1, + total_price: 30000, + total_count: 2, + payment_at: "2023-11-25T12:00:01", + rooms: [ + { + room_basket_id: 1, + accommodation_name: "부산 앙코르 호텔", + room_name: "스위트룸", + price: 300000, + number_guests: 2, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + { + room_basket_id: 2, + accommodation_name: "신라민박", + room_name: "거지존", + price: 100, + number_guests: 1, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + ], + }, + { + payment_id: 2, + total_price: 50000, + total_count: 2, + payment_at: "2023-11-21T12:00:01", + rooms: [ + { + room_basket_id: 1, + accommodation_name: "부산 앙코르 호텔", + room_name: "스위트룸", + price: 300000, + number_guests: 2, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + { + room_basket_id: 2, + accommodation_name: "신라민박", + room_name: "거지존", + price: 100, + number_guests: 1, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + ], + }, + { + payment_id: 3, + total_price: 30000, + total_count: 2, + payment_at: "2023-11-23T12:00:01", + rooms: [ + { + room_basket_id: 1, + accommodation_name: "부산 앙코르 호텔", + room_name: "스위트룸", + price: 300000, + number_guests: 2, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + { + room_basket_id: 2, + accommodation_name: "신라민박", + room_name: "거지존", + price: 100, + number_guests: 1, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + ], + }, + { + payment_id: 4, + total_price: 30000, + total_count: 2, + payment_at: "2023-11-27T12:00:01", + rooms: [ + { + room_basket_id: 1, + accommodation_name: "부산 앙코르 호텔", + room_name: "스위트룸", + price: 300000, + number_guests: 2, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + { + room_basket_id: 2, + accommodation_name: "신라민박", + room_name: "거지존", + price: 100, + number_guests: 1, + check_in_at: "2023-12-25", + check_out_at: "2023-12-26", + room_image_url: "www.accommodation1.com/thumbnail.jpg", + }, + ], + }, +]; diff --git a/src/mocks/browser/constant.ts b/src/mocks/browser/constant.ts index 76b021d..c36fec7 100644 --- a/src/mocks/browser/constant.ts +++ b/src/mocks/browser/constant.ts @@ -3,3800 +3,3800 @@ export const MockUpData = [ id: 53096, name: "비진도 바다이야기 펜션", address: "경상남도 통영시 한산면 외항길 78", - phone_number: "055-642-6171", + phoneNumber: "055-642-6171", type: 4, - like_count: 644, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 488, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 707, - check_in: "18:00", - check_out: "12:00", + wishCount: 644, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 488, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 707, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 47247, name: "토요코 인 서면", address: "부산광역시 부산진구 서전로 39", - phone_number: "051-638-1045", + phoneNumber: "051-638-1045", type: 1, - like_count: 217, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 425, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 246, - check_in: "18:00", - check_out: "12:00", + wishCount: 217, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 425, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 246, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 4709, name: "아모렉스관광호텔", address: "서울특별시 성동구 왕십리로20길 19", - phone_number: "02-2292-7634", + phoneNumber: "02-2292-7634", type: 9, - like_count: 556, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 260, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 865, - check_in: "18:00", - check_out: "12:00", + wishCount: 556, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 260, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 865, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 8297, name: "유로파라텔", address: "서울특별시 구로구 새말로18길 142", - phone_number: "02-869-7396", + phoneNumber: "02-869-7396", type: 5, - like_count: 821, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 342, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 188, - check_in: "18:00", - check_out: "12:00", + wishCount: 821, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 342, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 188, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 25033, name: "오색그린야드호텔", address: "강원특별자치도 양양군 서면 대청봉길 34", - phone_number: "033-670-1004", + phoneNumber: "033-670-1004", type: 8, - like_count: 813, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 115, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 879, - check_in: "18:00", - check_out: "12:00", + wishCount: 813, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 115, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 879, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 53079, name: "금호통영마리나리조트", address: "경상남도 통영시 큰발개1길 33(도남동)", - phone_number: "055-643-8000
055-646-7001", + phoneNumber: "055-643-8000
055-646-7001", type: 8, - like_count: 805, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 310, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 945, - check_in: "18:00", - check_out: "12:00", + wishCount: 805, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 310, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 945, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 55556, name: "무주덕유산리조트 가족호텔", address: "전라북도 무주군 설천면 만선로 185", - phone_number: "063-322-9000", + phoneNumber: "063-322-9000", type: 9, - like_count: 728, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 271, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 35, - check_in: "18:00", - check_out: "12:00", + wishCount: 728, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 271, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 35, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 59047, name: "해남유스호스텔", address: "전라남도 해남군 삼산면 대흥사길 88-88", - phone_number: "061-533-0170", + phoneNumber: "061-533-0170", type: 5, - like_count: 61, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 475, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 841, - check_in: "18:00", - check_out: "12:00", + wishCount: 61, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 475, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 841, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 25102, name: "소노벨 비발디파크", address: "강원특별자치도 홍천군 서면 한치골길 262", - phone_number: "1588-4888", + phoneNumber: "1588-4888", type: 8, - like_count: 497, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 96, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 523, - check_in: "18:00", - check_out: "12:00", + wishCount: 497, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 96, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 523, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 38116, name: "한솔모텔", address: "경상북도 경주시 보문로 465-47(신평동)", - phone_number: "054-748-3800", + phoneNumber: "054-748-3800", type: 3, - like_count: 400, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 325, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 47, - check_in: "18:00", - check_out: "12:00", + wishCount: 400, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 325, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 47, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 17390, name: "지산 메이플콘도", address: "경기도 이천시 마장면 지산로 267", - phone_number: "031-644-1261", + phoneNumber: "031-644-1261", type: 4, - like_count: 545, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 405, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 411, - check_in: "18:00", - check_out: "12:00", + wishCount: 545, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 405, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 411, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 24702, name: "금강산콘도", address: "강원특별자치도 고성군 현내면 금강산로 416", - phone_number: "033-680-7800", + phoneNumber: "033-680-7800", type: 4, - like_count: 620, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 352, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 307, - check_in: "18:00", - check_out: "12:00", + wishCount: 620, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 352, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 307, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 56452, name: "고창 선운산 유스호스텔", address: "전라북도 고창군 아산면 선운사로 158-36", - phone_number: "063-561-3333
063-561-3445
063-561-3446", + phoneNumber: "063-561-3333
063-561-3445
063-561-3446", type: 5, - like_count: 740, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 164, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 456, - check_in: "18:00", - check_out: "12:00", + wishCount: 740, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 164, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 456, + checkIn: "18:00", + checkOut: "12:00", room_counts: 14, }, { id: 52400, name: "남해비치텔", address: "경상남도 남해군 설천면 노량로 194", - phone_number: "055-863-5505", + phoneNumber: "055-863-5505", type: 3, - like_count: 911, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 415, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 219, - check_in: "18:00", - check_out: "12:00", + wishCount: 911, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 415, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 219, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 36238, name: "궁전파크모텔", address: "경상북도 봉화군 봉화읍 예봉로 2043", - phone_number: "054-674-0300", + phoneNumber: "054-674-0300", type: 1, - like_count: 628, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 310, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 882, - check_in: "18:00", - check_out: "12:00", + wishCount: 628, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 310, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 882, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 63528, name: "라운지하우스 제주다", address: "제주특별자치도 서귀포시 안덕면 사계남로50번길 60", - phone_number: "064-792-5670", + phoneNumber: "064-792-5670", type: 0, - like_count: 945, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 460, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 637, - check_in: "18:00", - check_out: "12:00", + wishCount: 945, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 460, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 637, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 26019, name: "리베라모텔(태백)", address: "강원특별자치도 태백시 고원로 87", - phone_number: "033-552-5691", + phoneNumber: "033-552-5691", type: 6, - like_count: 317, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 186, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 106, - check_in: "18:00", - check_out: "12:00", + wishCount: 317, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 186, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 106, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 26133, name: "강이흐르는마을 펜션", address: "강원특별자치도 정선군 정선읍 군언길 129", - phone_number: "033-563-7979", + phoneNumber: "033-563-7979", type: 1, - like_count: 752, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 387, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 922, - check_in: "18:00", - check_out: "12:00", + wishCount: 752, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 387, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 922, + checkIn: "18:00", + checkOut: "12:00", room_counts: 15, }, { id: 53333, name: "수미르펜션", address: "경상남도 거제시 남부면 거제남서로 750-1", - phone_number: "055-632-5745", + phoneNumber: "055-632-5745", type: 7, - like_count: 627, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 240, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 148, - check_in: "18:00", - check_out: "12:00", + wishCount: 627, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 240, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 148, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 63626, name: "소노캄 제주(구 샤인빌 리조트)", address: "제주특별자치도 서귀포시 표선면 일주동로 6347-17", - phone_number: "02-1588-4888", + phoneNumber: "02-1588-4888", type: 4, - like_count: 912, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 415, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 923, - check_in: "18:00", - check_out: "12:00", + wishCount: 912, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 415, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 923, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 32152, name: "마리나 비치펜션", address: "충청남도 태안군 남면 몽대로 494-8", - phone_number: "041-672-4097", + phoneNumber: "041-672-4097", type: 6, - like_count: 417, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 218, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 56, - check_in: "18:00", - check_out: "12:00", + wishCount: 417, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 218, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 56, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 59448, name: "보성골망태펜션", address: "전라남도 보성군 보성읍 노산길 5-56", - phone_number: "061-852-1966", + phoneNumber: "061-852-1966", type: 1, - like_count: 263, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 494, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 89, - check_in: "18:00", - check_out: "12:00", + wishCount: 263, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 494, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 89, + checkIn: "18:00", + checkOut: "12:00", room_counts: 20, }, { id: 63620, name: "금호리조트 제주", address: "제주특별자치도 서귀포시 남원읍 태위로 522-12", - phone_number: "064-766-8000", + phoneNumber: "064-766-8000", type: 4, - like_count: 49, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 393, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 461, - check_in: "18:00", - check_out: "12:00", + wishCount: 49, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 393, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 461, + checkIn: "18:00", + checkOut: "12:00", room_counts: 13, }, { id: 25009, name: "낙산 패밀리파크", address: "강원특별자치도 양양군 강현면 일출로 8", - phone_number: "033-671-6336", + phoneNumber: "033-671-6336", type: 3, - like_count: 241, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 87, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 202, - check_in: "18:00", - check_out: "12:00", + wishCount: 241, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 87, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 202, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 63637, name: "귤익는마을", address: "제주특별자치도 서귀포시 성산읍 신천서로 11", - phone_number: "064-787-0543", + phoneNumber: "064-787-0543", type: 1, - like_count: 76, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 228, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 939, - check_in: "18:00", - check_out: "12:00", + wishCount: 76, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 228, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 939, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 32157, name: "청포대하와이펜션", address: "충청남도 태안군 남면 청포대길 29", - phone_number: "041-675-5858", + phoneNumber: "041-675-5858", type: 0, - like_count: 229, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 233, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 577, - check_in: "18:00", - check_out: "12:00", + wishCount: 229, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 233, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 577, + checkIn: "18:00", + checkOut: "12:00", room_counts: 5, }, { id: 48303, name: "브릿지모텔", address: "부산광역시 수영구 광안해변로 155", - phone_number: "051-755-0583
051-755-0573", + phoneNumber: "051-755-0583
051-755-0573", type: 2, - like_count: 134, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 114, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 776, - check_in: "18:00", - check_out: "12:00", + wishCount: 134, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 114, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 776, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 56337, name: "채석강 리조트 유스호스텔", address: "전라북도 부안군 변산면 채석강길 19", - phone_number: "063-583-1234", + phoneNumber: "063-583-1234", type: 5, - like_count: 158, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 105, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 41, - check_in: "18:00", - check_out: "12:00", + wishCount: 158, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 105, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 41, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 50057, name: "백무산장 황토방 펜션", address: "경상남도 함양군 마천면 백무동로 321", - phone_number: "055-962-5277", + phoneNumber: "055-962-5277", type: 1, - like_count: 784, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 194, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 233, - check_in: "18:00", - check_out: "12:00", + wishCount: 784, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 194, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 233, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 32130, name: "연포리조트", address: "충청남도 태안군 근흥면 연포2길 57", - phone_number: "041-673-0506", + phoneNumber: "041-673-0506", type: 6, - like_count: 30, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 147, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 516, - check_in: "18:00", - check_out: "12:00", + wishCount: 30, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 147, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 516, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 53079, name: "카리브콘도텔", address: "경상남도 통영시 도남로 257-29(도남동)", - phone_number: "055-645-4070
055-644-4070", + phoneNumber: "055-645-4070
055-644-4070", type: 7, - like_count: 216, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 224, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 596, - check_in: "18:00", - check_out: "12:00", + wishCount: 216, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 224, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 596, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 34184, name: "까펠라 모텔", address: "대전광역시 유성구 한밭대로492번길 16-32", - phone_number: "0507-1303-0953", + phoneNumber: "0507-1303-0953", type: 2, - like_count: 960, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 425, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 302, - check_in: "18:00", - check_out: "12:00", + wishCount: 960, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 425, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 302, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 33603, name: "서천 휴모텔", address: "충청남도 서천군 서면 부사로 273", - phone_number: "041-952-0077", + phoneNumber: "041-952-0077", type: 8, - like_count: 799, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 187, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 324, - check_in: "18:00", - check_out: "12:00", + wishCount: 799, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 187, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 324, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 52447, name: "남해 온뷰펜션", address: "경상남도 남해군 삼동면 동부대로1122번길 128", - phone_number: "055-867-6966", + phoneNumber: "055-867-6966", type: 4, - like_count: 888, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 99, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 733, - check_in: "18:00", - check_out: "12:00", + wishCount: 888, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 99, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 733, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 25042, name: "보노펜션", address: "강원특별자치도 양양군 손양면 선사유적로 240-29", - phone_number: "010-9015-5209", + phoneNumber: "010-9015-5209", type: 5, - like_count: 559, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 410, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 685, - check_in: "18:00", - check_out: "12:00", + wishCount: 559, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 410, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 685, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 53329, name: "트로피칼드림 리조트", address: "경상남도 거제시 일운면 망양1길 16", - phone_number: "055-681-5550", + phoneNumber: "055-681-5550", type: 1, - like_count: 1000, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 135, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 10, - check_in: "18:00", - check_out: "12:00", + wishCount: 1000, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 135, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 10, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 25103, name: "신영펜션", address: "강원특별자치도 홍천군 서면 한치골길 75-12", - phone_number: "033-435-8666", + phoneNumber: "033-435-8666", type: 2, - like_count: 833, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 456, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 702, - check_in: "18:00", - check_out: "12:00", + wishCount: 833, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 456, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 702, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 18141, name: "럭셔리관광호텔(오산)", address: "경기도 오산시 경기대로 231", - phone_number: "031-378-8013~5", + phoneNumber: "031-378-8013~5", type: 8, - like_count: 226, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 101, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 323, - check_in: "18:00", - check_out: "12:00", + wishCount: 226, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 101, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 323, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 59471, name: "녹차향펜션", address: "전라남도 보성군 회천면 남부관광로 2319-8", - phone_number: "061-853-7754", + phoneNumber: "061-853-7754", type: 5, - like_count: 137, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 151, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 535, - check_in: "18:00", - check_out: "12:00", + wishCount: 137, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 151, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 535, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 33603, name: "춘장대바닷가모텔", address: "충청남도 서천군 서면 춘장대로151번길 39", - phone_number: "041-952-0737", + phoneNumber: "041-952-0737", type: 2, - like_count: 785, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 327, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 103, - check_in: "18:00", - check_out: "12:00", + wishCount: 785, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 327, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 103, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 25916, name: "라벤다모텔", address: "강원특별자치도 삼척시 대학로 49-9", - phone_number: "033-574-2543
033-574-2539", + phoneNumber: "033-574-2543
033-574-2539", type: 3, - like_count: 1, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 364, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 342, - check_in: "18:00", - check_out: "12:00", + wishCount: 1, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 364, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 342, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 63558, name: "JJ하우스", address: "제주특별자치도 서귀포시 중산간서로 241", - phone_number: "064-739-7891", + phoneNumber: "064-739-7891", type: 5, - like_count: 824, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 236, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 200, - check_in: "18:00", - check_out: "12:00", + wishCount: 824, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 236, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 200, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 6164, name: "그랜드 인터컨티넨탈 서울 파르나스", address: "서울특별시 강남구 테헤란로 521", - phone_number: "02-555-5656", + phoneNumber: "02-555-5656", type: 6, - like_count: 755, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 377, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 77, - check_in: "18:00", - check_out: "12:00", + wishCount: 755, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 377, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 77, + checkIn: "18:00", + checkOut: "12:00", room_counts: 5, }, { id: 24265, name: "춘천세종호텔", address: "강원특별자치도 춘천시 봉의산길 31", - phone_number: "033-252-1191~6", + phoneNumber: "033-252-1191~6", type: 5, - like_count: 381, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 385, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 472, - check_in: "18:00", - check_out: "12:00", + wishCount: 381, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 385, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 472, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 36307, name: "덕구온천리조트 호텔", address: "경상북도 울진군 북면 덕구온천로 924", - phone_number: "054-782-0677", + phoneNumber: "054-782-0677", type: 6, - like_count: 257, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 374, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 808, - check_in: "18:00", - check_out: "12:00", + wishCount: 257, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 374, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 808, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 63599, name: "서귀포KAL호텔", address: "제주특별자치도 서귀포시 칠십리로 242", - phone_number: "064-733-2001", + phoneNumber: "064-733-2001", type: 6, - like_count: 744, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 486, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 153, - check_in: "18:00", - check_out: "12:00", + wishCount: 744, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 486, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 153, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 63136, name: "제주 펄 관광호텔", address: "제주특별자치도 제주시 신대로20길 49(연동)", - phone_number: "064-742-8871", + phoneNumber: "064-742-8871", type: 9, - like_count: 162, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 152, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 940, - check_in: "18:00", - check_out: "12:00", + wishCount: 162, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 152, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 940, + checkIn: "18:00", + checkOut: "12:00", room_counts: 13, }, { id: 39565, name: "김천파크 관광호텔", address: "경상북도 김천시 대항면 황학동길 35-23", - phone_number: "054-437-8000", + phoneNumber: "054-437-8000", type: 7, - like_count: 661, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 269, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 920, - check_in: "18:00", - check_out: "12:00", + wishCount: 661, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 269, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 920, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 25307, name: "휘닉스 평창호텔", address: "강원특별자치도 평창군 봉평면 태기로 174", - phone_number: "033-330-6001", + phoneNumber: "033-330-6001", type: 0, - like_count: 138, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 379, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 944, - check_in: "18:00", - check_out: "12:00", + wishCount: 138, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 379, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 944, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 6164, name: "인터컨티넨탈 서울코엑스", address: "서울특별시 강남구 봉은사로 524", - phone_number: "02-3452-2500", + phoneNumber: "02-3452-2500", type: 2, - like_count: 131, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 242, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 176, - check_in: "18:00", - check_out: "12:00", + wishCount: 131, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 242, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 176, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 7616, name: "메이필드호텔", address: "서울특별시 강서구 방화대로21길 94", - phone_number: "02-2660-9000", + phoneNumber: "02-2660-9000", type: 0, - like_count: 359, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 368, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 76, - check_in: "18:00", - check_out: "12:00", + wishCount: 359, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 368, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 76, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 61949, name: "센트럴관광호텔", address: "광주광역시 서구 상무연하로 68", - phone_number: "062-383-7575", + phoneNumber: "062-383-7575", type: 7, - like_count: 492, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 288, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 846, - check_in: "18:00", - check_out: "12:00", + wishCount: 492, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 288, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 846, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 44035, name: "울산 굿모닝 관광호텔", address: "울산광역시 동구 바드래1길 15", - phone_number: "052-209-9000", + phoneNumber: "052-209-9000", type: 1, - like_count: 956, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 452, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 676, - check_in: "18:00", - check_out: "12:00", + wishCount: 956, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 452, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 676, + checkIn: "18:00", + checkOut: "12:00", room_counts: 13, }, { id: 37789, name: "베니키아 호텔 포항", address: "경상북도 포항시 남구 중앙로 128(해도동)", - phone_number: "054-282-2700", + phoneNumber: "054-282-2700", type: 2, - like_count: 273, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 62, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 509, - check_in: "18:00", - check_out: "12:00", + wishCount: 273, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 62, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 509, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 7539, name: "나이아가라호텔", address: "서울특별시 강서구 양천로 743", - phone_number: "02-3660-4000", + phoneNumber: "02-3660-4000", type: 4, - like_count: 730, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 130, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 456, - check_in: "18:00", - check_out: "12:00", + wishCount: 730, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 130, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 456, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 21959, name: "보보스모텔", address: "인천광역시 연수구 능허대로104번길 59", - phone_number: "032-831-2141", + phoneNumber: "032-831-2141", type: 6, - like_count: 971, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 378, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 159, - check_in: "18:00", - check_out: "12:00", + wishCount: 971, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 378, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 159, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 57604, name: "화야평", address: "전라남도 구례군 산동면 사포길 33-121", - phone_number: "061-783-7900", + phoneNumber: "061-783-7900", type: 9, - like_count: 696, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 270, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 360, - check_in: "18:00", - check_out: "12:00", + wishCount: 696, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 270, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 360, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 12566, name: "쉐르빌 온천 호텔", address: "경기도 양평군 개군면 신내길7번길 37", - phone_number: "031-774-4101~3", + phoneNumber: "031-774-4101~3", type: 8, - like_count: 770, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 217, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 202, - check_in: "18:00", - check_out: "12:00", + wishCount: 770, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 217, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 202, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 16491, name: "호텔리츠", address: "경기도 수원시 팔달구 권광로134번길 44-11", - phone_number: "031-224-1100", + phoneNumber: "031-224-1100", type: 6, - like_count: 449, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 351, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 378, - check_in: "18:00", - check_out: "12:00", + wishCount: 449, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 351, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 378, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 36604, name: "노송정종택(퇴계생가)[한국관광 품질인증/Korea Quality]", address: "경상북도 안동시 온혜중마길 46-5(도산면)", - phone_number: "054-856-1052", + phoneNumber: "054-856-1052", type: 1, - like_count: 393, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 222, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 538, - check_in: "18:00", - check_out: "12:00", + wishCount: 393, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 222, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 538, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 55041, name: "풍남헌[한국관광 품질인증/Korea Quality]", address: "전라북도 전주시 완산구 은행로 35 풍남헌", - phone_number: "063-286-7673", + phoneNumber: "063-286-7673", type: 0, - like_count: 807, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 251, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 862, - check_in: "18:00", - check_out: "12:00", + wishCount: 807, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 251, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 862, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 4324, name: "24게스트하우스 서울역점", address: "서울특별시 용산구 후암로57길 41", - phone_number: "02-754-8124", + phoneNumber: "02-754-8124", type: 4, - like_count: 118, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 392, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 772, - check_in: "18:00", - check_out: "12:00", + wishCount: 118, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 392, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 772, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 24902, name: "베니키아 호텔 산과 바다 대포항", address: "강원특별자치도 속초시 동해대로 3691(대포동)", - phone_number: "033-635-6644", + phoneNumber: "033-635-6644", type: 2, - like_count: 437, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 78, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 553, - check_in: "18:00", - check_out: "12:00", + wishCount: 437, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 78, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 553, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 58019, name: "순천만에코촌 유스호스텔[한국관광 품질인증/Korea Quality]", address: "전라남도 순천시 해룡면 생태배움길 123", - phone_number: "061-749-4817", + phoneNumber: "061-749-4817", type: 0, - like_count: 656, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 381, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 875, - check_in: "18:00", - check_out: "12:00", + wishCount: 656, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 381, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 875, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 26223, name: "동강시스타 리조트", address: "강원특별자치도 영월군 영월읍 사지막길 160", - phone_number: "033-905-2000", + phoneNumber: "033-905-2000", type: 7, - like_count: 842, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 450, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 17, - check_in: "18:00", - check_out: "12:00", + wishCount: 842, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 450, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 17, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 63525, name: "루체빌리조트", address: "제주특별자치도 서귀포시 안덕면 산록남로 786", - phone_number: "064-805-0114", + phoneNumber: "064-805-0114", type: 9, - like_count: 128, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 123, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 679, - check_in: "18:00", - check_out: "12:00", + wishCount: 128, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 123, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 679, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 55045, name: "한옥이야기[한국관광 품질인증/Korea Quality]", address: "전라북도 전주시 완산구 은행로 83-14", - phone_number: "010-9203-1111", + phoneNumber: "010-9203-1111", type: 6, - like_count: 127, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 401, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 206, - check_in: "18:00", - check_out: "12:00", + wishCount: 127, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 401, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 206, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 55045, name: "사랑가득[한국관광 품질인증/Korea Quality]", address: "전라북도 전주시 완산구 향교길 149-8(교동)", - phone_number: "010-7451-3355", + phoneNumber: "010-7451-3355", type: 7, - like_count: 165, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 375, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 877, - check_in: "18:00", - check_out: "12:00", + wishCount: 165, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 375, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 877, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 52433, name: "아난티 남해", address: "경상남도 남해군 남면 남서대로1179번길 40-109", - phone_number: "055-860-0100", + phoneNumber: "055-860-0100", type: 4, - like_count: 274, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 387, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 202, - check_in: "18:00", - check_out: "12:00", + wishCount: 274, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 387, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 202, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 27492, name: "충주 야생화와고택나들이[한국관광 품질인증/Korea Quality]", address: "충청북도 충주시 중원대로 2220", - phone_number: "010-5485-7744", + phoneNumber: "010-5485-7744", type: 6, - like_count: 57, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 106, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 647, - check_in: "18:00", - check_out: "12:00", + wishCount: 57, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 106, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 647, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 25300, name: "벤치하우스 펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡1길 35-6", - phone_number: "010-3414-0487", + phoneNumber: "010-3414-0487", type: 0, - like_count: 90, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 145, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 856, - check_in: "18:00", - check_out: "12:00", + wishCount: 90, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 145, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 856, + checkIn: "18:00", + checkOut: "12:00", room_counts: 15, }, { id: 23128, name: "큰나무펜션", address: "인천광역시 옹진군 자월면 대이작로159번길 35-19", - phone_number: "010-4764-6412", + phoneNumber: "010-4764-6412", type: 7, - like_count: 828, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 414, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 987, - check_in: "18:00", - check_out: "12:00", + wishCount: 828, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 414, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 987, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 48095, name: "라마다 앙코르 바이워덤 부산해운대", address: "부산광역시 해운대구 구남로 9(우동)", - phone_number: "051-610-3000", + phoneNumber: "051-610-3000", type: 2, - like_count: 74, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 57, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 743, - check_in: "18:00", - check_out: "12:00", + wishCount: 74, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 57, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 743, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 48957, name: "센트럴파크 호텔[한국관광 품질인증/Korea Quality]", address: "부산광역시 중구 해관로 20(중앙동1가)", - phone_number: "051-243-8001", + phoneNumber: "051-243-8001", type: 6, - like_count: 833, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 386, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 35, - check_in: "18:00", - check_out: "12:00", + wishCount: 833, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 386, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 35, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 25460, name: "호텔여기어때 강릉경포점", address: "강원특별자치도 강릉시 해안로406번길 13-9", - phone_number: "033-644-2776", + phoneNumber: "033-644-2776", type: 7, - like_count: 956, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 437, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 155, - check_in: "18:00", - check_out: "12:00", + wishCount: 956, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 437, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 155, + checkIn: "18:00", + checkOut: "12:00", room_counts: 7, }, { id: 31110, name: "호텔여기어때 천안성정점", address: "충청남도 천안시 서북구 성정공원4길 31", - phone_number: "070-5158-5924", + phoneNumber: "070-5158-5924", type: 9, - like_count: 265, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 140, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 679, - check_in: "18:00", - check_out: "12:00", + wishCount: 265, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 140, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 679, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 48099, name: "플레아 드 블랑 (PLEA DE BLANC)", address: "부산광역시 해운대구 해운대해변로298번길 29", - phone_number: "051-742-2277", + phoneNumber: "051-742-2277", type: 7, - like_count: 935, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 107, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 400, - check_in: "18:00", - check_out: "12:00", + wishCount: 935, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 107, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 400, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 52227, name: "산청나리꽃펜션", address: "경상남도 산청군 산청읍 웅석봉로250번길 52-9", - phone_number: "055-972-9989", + phoneNumber: "055-972-9989", type: 8, - like_count: 686, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 301, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 786, - check_in: "18:00", - check_out: "12:00", + wishCount: 686, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 301, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 786, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 53024, name: "종이섬펜션", address: "경상남도 통영시 용남면 남해안대로 79", - phone_number: "010-4593-3211", + phoneNumber: "010-4593-3211", type: 4, - like_count: 601, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 216, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 153, - check_in: "18:00", - check_out: "12:00", + wishCount: 601, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 216, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 153, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 55044, name: "사랑나무 한옥펜션", address: "전라북도 전주시 완산구 은행로 96(교동)", - phone_number: "010-5171-7970", + phoneNumber: "010-5171-7970", type: 7, - like_count: 246, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 193, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 41, - check_in: "18:00", - check_out: "12:00", + wishCount: 246, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 193, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 41, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 55517, name: "기린모텔 [한국관광 품질인증/Korea Quality]", address: "전라북도 무주군 무주읍 단천로 74", - phone_number: "010-4120-5562", + phoneNumber: "010-4120-5562", type: 7, - like_count: 390, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 110, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 938, - check_in: "18:00", - check_out: "12:00", + wishCount: 390, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 110, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 938, + checkIn: "18:00", + checkOut: "12:00", room_counts: 15, }, { id: 27027, name: "단촌서원고택 [한국관광 품질인증/Korea Quality]", address: "충청북도 단양군 단성면 북상하리길 103-10", - phone_number: "010-7230-5415", + phoneNumber: "010-7230-5415", type: 7, - like_count: 928, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 466, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 387, - check_in: "18:00", - check_out: "12:00", + wishCount: 928, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 466, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 387, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 36604, name: "온계종택 삼백당[한국관광 품질인증/Korea Quality]", address: "경상북도 안동시 온혜중마길 20", - phone_number: "010-2988-3435", + phoneNumber: "010-2988-3435", type: 3, - like_count: 999, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 313, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 933, - check_in: "18:00", - check_out: "12:00", + wishCount: 999, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 313, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 933, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 36731, name: "만초고택[한국관광 품질인증/Korea Quality]", address: "경상북도 안동시 금소중앙길 48", - phone_number: "010-5191-3697", + phoneNumber: "010-5191-3697", type: 9, - like_count: 972, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 157, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 621, - check_in: "18:00", - check_out: "12:00", + wishCount: 972, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 157, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 621, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 36759, name: "만소당 [한국관광 품질인증/Korea Quality]", address: "경상북도 안동시 한옥마을1길 6-9", - phone_number: "010-2879-7077", + phoneNumber: "010-2879-7077", type: 9, - like_count: 440, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 433, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 22, - check_in: "18:00", - check_out: "12:00", + wishCount: 440, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 433, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 22, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 8393, name: "포포인츠 바이 쉐라톤 서울 구로", address: "서울특별시 구로구 디지털로32길 72", - phone_number: "02-6905-9500", + phoneNumber: "02-6905-9500", type: 9, - like_count: 608, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 415, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 506, - check_in: "18:00", - check_out: "12:00", + wishCount: 608, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 415, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 506, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 6626, name: "강남아르누보씨티호텔", address: "서울특별시 서초구 서초대로74길 49(서초동)강남아르누보씨티호텔", - phone_number: "02-580-7300/7400", + phoneNumber: "02-580-7300/7400", type: 0, - like_count: 447, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 93, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 377, - check_in: "18:00", - check_out: "12:00", + wishCount: 447, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 93, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 377, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 48094, name: "베스트웨스턴 해운대호텔", address: "부산광역시 해운대구 구남로 42(중동)", - phone_number: "051-664-1234", + phoneNumber: "051-664-1234", type: 2, - like_count: 946, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 90, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 952, - check_in: "18:00", - check_out: "12:00", + wishCount: 946, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 90, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 952, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 25467, name: "세인트존스 호텔", address: "강원특별자치도 강릉시 창해로 307", - phone_number: "033-660-9000", + phoneNumber: "033-660-9000", type: 2, - like_count: 864, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 333, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 860, - check_in: "18:00", - check_out: "12:00", + wishCount: 864, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 333, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 860, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 55557, name: "다숲펜션[한국관광 품질인증/Korea Quality]", address: "전라북도 무주군 설천면 구천동1로 149", - phone_number: "010-3925-7346", + phoneNumber: "010-3925-7346", type: 9, - like_count: 267, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 327, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 853, - check_in: "18:00", - check_out: "12:00", + wishCount: 267, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 327, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 853, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 55116, name: "대성 정담한옥[한국관광 품질인증/Korea Quality]", address: "전라북도 전주시 완산구 고덕산1길 30(대성동)", - phone_number: "010-8753-3413", + phoneNumber: "010-8753-3413", type: 5, - like_count: 407, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 64, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 729, - check_in: "18:00", - check_out: "12:00", + wishCount: 407, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 64, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 729, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 28148, name: "초정행궁 [한국관광 품질인증/Korea Quality]", address: "충청북도 청주시 청원구 내수읍 초정약수로 851", - phone_number: "043-270-7332", + phoneNumber: "043-270-7332", type: 8, - like_count: 215, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 314, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 698, - check_in: "18:00", - check_out: "12:00", + wishCount: 215, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 314, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 698, + checkIn: "18:00", + checkOut: "12:00", room_counts: 20, }, { id: 24239, name: "케이티엔지(KT&G) 상상마당춘천스테이 [한국관광 품질인증/Korea Quality]", address: "강원특별자치도 춘천시 스포츠타운길399번길 22", - phone_number: "033-818-4200", + phoneNumber: "033-818-4200", type: 9, - like_count: 305, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 234, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 106, - check_in: "18:00", - check_out: "12:00", + wishCount: 305, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 234, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 106, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 56340, name: "일마레변산리조트", address: "전라북도 부안군 변산면 궁항로 91", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 727, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 336, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 853, - check_in: "18:00", - check_out: "12:00", + wishCount: 727, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 336, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 853, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 28742, name: "터무니", address: "충청북도 청주시 상당구 영운천로55번길 47(영운동)", - phone_number: "0507-1325-4703", + phoneNumber: "0507-1325-4703", type: 5, - like_count: 381, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 188, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 32, - check_in: "18:00", - check_out: "12:00", + wishCount: 381, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 188, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 32, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 38118, name: "블루원리조트 프라이빗콘도", address: "경상북도 경주시 보불로 391(천군동)", - phone_number: null, + phoneNumber: null, type: 9, - like_count: 433, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 126, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 233, - check_in: "18:00", - check_out: "12:00", + wishCount: 433, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 126, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 233, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 25342, name: "라마다호텔&스위트평창", address: "강원특별자치도 평창군 대관령면 오목길 107(횡계리)", - phone_number: "033-333-1000", + phoneNumber: "033-333-1000", type: 4, - like_count: 678, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 387, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 326, - check_in: "18:00", - check_out: "12:00", + wishCount: 678, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 387, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 326, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 38118, name: "블루원리조트", address: "경상북도 경주시 보불로 391(천군동)", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 704, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 349, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 238, - check_in: "18:00", - check_out: "12:00", + wishCount: 704, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 349, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 238, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 52310, name: "비바체리조트", address: "경상남도 하동군 청암면 청학로 876", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 28, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 88, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 490, - check_in: "18:00", - check_out: "12:00", + wishCount: 28, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 88, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 490, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 25343, name: "에이엠호텔", address: "강원특별자치도 평창군 대관령면 송천3길 310", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 213, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 341, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 55, - check_in: "18:00", - check_out: "12:00", + wishCount: 213, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 341, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 55, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 25307, name: "더화이트호텔", address: "강원특별자치도 평창군 봉평면 태기로 228-95(면온리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 323, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 322, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 95, - check_in: "18:00", - check_out: "12:00", + wishCount: 323, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 322, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 95, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 25366, name: "안단테 북펜션", address: "강원특별자치도 평창군 방림면 칡사리길 167운교리", - phone_number: null, + phoneNumber: null, type: 4, - like_count: 271, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 103, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 135, - check_in: "18:00", - check_out: "12:00", + wishCount: 271, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 103, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 135, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 25364, name: "작은통나무집 펜션", address: "강원특별자치도 평창군 방림면 월암길 41-7(계촌리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 156, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 126, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 402, - check_in: "18:00", - check_out: "12:00", + wishCount: 156, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 126, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 402, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 25370, name: "돈막골펜션", address: "강원특별자치도 평창군 평창읍 고길천로 200-12(이곡리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 113, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 151, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 98, - check_in: "18:00", - check_out: "12:00", + wishCount: 113, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 151, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 98, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 25369, name: "평창강가에서펜션", address: "강원특별자치도 평창군 평창읍 용항상촌길 141-46(용항리)", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 224, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 83, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 509, - check_in: "18:00", - check_out: "12:00", + wishCount: 224, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 83, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 509, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 25338, name: "산모랭이펜션", address: "강원특별자치도 평창군 진부면 장전길 277(장전리)", - phone_number: null, + phoneNumber: null, type: 1, - like_count: 976, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 279, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 936, - check_in: "18:00", - check_out: "12:00", + wishCount: 976, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 279, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 936, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 25365, name: "숲과개울펜션", address: "강원특별자치도 평창군 방림면 고원로 1505-10", - phone_number: null, + phoneNumber: null, type: 2, - like_count: 744, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 300, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 37, - check_in: "18:00", - check_out: "12:00", + wishCount: 744, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 300, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 37, + checkIn: "18:00", + checkOut: "12:00", room_counts: 7, }, { id: 25300, name: "베니스펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡길 130(흥정리)", - phone_number: null, + phoneNumber: null, type: 9, - like_count: 889, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 52, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 217, - check_in: "18:00", - check_out: "12:00", + wishCount: 889, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 52, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 217, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 25306, name: "구름아래펜션", address: "강원특별자치도 평창군 봉평면 안흥동길 6-20", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 518, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 210, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 596, - check_in: "18:00", - check_out: "12:00", + wishCount: 518, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 210, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 596, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 25364, name: "애비로드", address: "강원특별자치도 평창군 방림면 고원로 147-20(계촌리)", - phone_number: null, + phoneNumber: null, type: 1, - like_count: 945, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 441, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 343, - check_in: "18:00", - check_out: "12:00", + wishCount: 945, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 441, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 343, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 25308, name: "다차펜션", address: "강원특별자치도 평창군 봉평면 진조길 104-41(진조리)", - phone_number: null, + phoneNumber: null, type: 7, - like_count: 266, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 139, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 865, - check_in: "18:00", - check_out: "12:00", + wishCount: 266, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 139, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 865, + checkIn: "18:00", + checkOut: "12:00", room_counts: 15, }, { id: 25302, name: "쪽빛하늘펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡길 20-5(원길리)", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 534, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 143, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 48, - check_in: "18:00", - check_out: "12:00", + wishCount: 534, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 143, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 48, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 25341, name: "퀸스가든펜션", address: "강원특별자치도 평창군 대관령면 꽃밭양지길 242-63(횡계리)", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 697, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 87, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 203, - check_in: "18:00", - check_out: "12:00", + wishCount: 697, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 87, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 203, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 25340, name: "펜션그림", address: "강원특별자치도 평창군 대관령면 차항길 162-19(차항리)", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 748, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 61, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 301, - check_in: "18:00", - check_out: "12:00", + wishCount: 748, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 61, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 301, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 25300, name: "우리빌리지펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡길 401(흥정리)", - phone_number: null, + phoneNumber: null, type: 1, - like_count: 279, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 338, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 329, - check_in: "18:00", - check_out: "12:00", + wishCount: 279, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 338, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 329, + checkIn: "18:00", + checkOut: "12:00", room_counts: 13, }, { id: 25340, name: "T-Pension", address: "강원특별자치도 평창군 대관령면 차항길 162-9(차항리)", - phone_number: null, + phoneNumber: null, type: 4, - like_count: 468, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 476, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 476, - check_in: "18:00", - check_out: "12:00", + wishCount: 468, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 476, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 476, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 25305, name: "쉼펜션", address: "강원특별자치도 평창군 봉평면 팔송로 248-48(무이리)", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 453, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 253, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 533, - check_in: "18:00", - check_out: "12:00", + wishCount: 453, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 253, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 533, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 25366, name: "수가솔방", address: "강원특별자치도 평창군 방림면 여우재길 307-2(운교리)", - phone_number: null, + phoneNumber: null, type: 9, - like_count: 649, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 489, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 727, - check_in: "18:00", - check_out: "12:00", + wishCount: 649, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 489, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 727, + checkIn: "18:00", + checkOut: "12:00", room_counts: 20, }, { id: 25300, name: "하이디하우스", address: "강원특별자치도 평창군 봉평면 흥정계곡길 240(흥정리)", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 23, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 332, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 444, - check_in: "18:00", - check_out: "12:00", + wishCount: 23, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 332, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 444, + checkIn: "18:00", + checkOut: "12:00", room_counts: 20, }, { id: 25300, name: "비블로스펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡1길 9-12(흥정리)", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 385, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 370, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 623, - check_in: "18:00", - check_out: "12:00", + wishCount: 385, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 370, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 623, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 25305, name: "펜션수가성", address: "강원특별자치도 평창군 봉평면 팔송로 248-49(무이리)", - phone_number: null, + phoneNumber: null, type: 1, - like_count: 264, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 304, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 92, - check_in: "18:00", - check_out: "12:00", + wishCount: 264, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 304, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 92, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 25340, name: "하우스봉봉", address: "강원특별자치도 평창군 대관령면 차항길 192-82", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 523, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 381, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 697, - check_in: "18:00", - check_out: "12:00", + wishCount: 523, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 381, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 697, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 25306, name: "메이페어 데네브", address: "강원특별자치도 평창군 봉평면 안흥동길 70-88(무이리)", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 402, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 283, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 523, - check_in: "18:00", - check_out: "12:00", + wishCount: 402, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 283, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 523, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 25342, name: "푸른솔펜션", address: "강원특별자치도 평창군 대관령면 송전1길 20(횡계리)", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 444, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 372, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 475, - check_in: "18:00", - check_out: "12:00", + wishCount: 444, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 372, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 475, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 25306, name: "아침의 소리", address: "강원특별자치도 평창군 봉평면 무이진등길 77-70(무이리)", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 567, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 427, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 904, - check_in: "18:00", - check_out: "12:00", + wishCount: 567, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 427, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 904, + checkIn: "18:00", + checkOut: "12:00", room_counts: 14, }, { id: 25342, name: "대관령하우스", address: "강원특별자치도 평창군 대관령면 가시머리길 52-7(횡계리)", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 691, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 143, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 60, - check_in: "18:00", - check_out: "12:00", + wishCount: 691, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 143, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 60, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 25312, name: "솔숲", address: "강원특별자치도 평창군 용평면 양지길 64(속사리)", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 281, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 454, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 739, - check_in: "18:00", - check_out: "12:00", + wishCount: 281, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 454, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 739, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 25306, name: "메이페어 리겔", address: "강원특별자치도 평창군 봉평면 안흥동길 70-85(무이리)", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 906, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 60, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 817, - check_in: "18:00", - check_out: "12:00", + wishCount: 906, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 60, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 817, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 25309, name: "꼬마성", address: "강원특별자치도 평창군 봉평면 두리봉길 4-17(면온리)", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 741, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 170, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 944, - check_in: "18:00", - check_out: "12:00", + wishCount: 741, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 170, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 944, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 25352, name: "대관령로뎀나무", address: "강원특별자치도 평창군 대관령면 올림픽로 1012(용산리)", - phone_number: null, + phoneNumber: null, type: 4, - like_count: 788, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 63, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 531, - check_in: "18:00", - check_out: "12:00", + wishCount: 788, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 63, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 531, + checkIn: "18:00", + checkOut: "12:00", room_counts: 3, }, { id: 25364, name: "선영아사랑해", address: "강원특별자치도 평창군 방림면 월암길 3-6", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 205, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 449, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 43, - check_in: "18:00", - check_out: "12:00", + wishCount: 205, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 449, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 43, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 25311, name: "핑크하우스", address: "강원특별자치도 평창군 봉평면 금당계곡로 1277-9(유포리)", - phone_number: null, + phoneNumber: null, type: 2, - like_count: 827, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 280, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 308, - check_in: "18:00", - check_out: "12:00", + wishCount: 827, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 280, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 308, + checkIn: "18:00", + checkOut: "12:00", room_counts: 15, }, { id: 25337, name: "스노우힐펜션", address: "강원특별자치도 평창군 진부면 수항길 38-13(수항리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 196, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 386, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 51, - check_in: "18:00", - check_out: "12:00", + wishCount: 196, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 386, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 51, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 25300, name: "그리네펜션", address: "강원특별자치도 평창군 봉평면 흥정계곡1길 30(흥정리)", - phone_number: null, + phoneNumber: null, type: 6, - like_count: 688, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 157, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 508, - check_in: "18:00", - check_out: "12:00", + wishCount: 688, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 157, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 508, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 25306, name: "메이페어나르샤", address: "강원특별자치도 평창군 봉평면 안흥동길 70-83(무이리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 802, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 353, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 355, - check_in: "18:00", - check_out: "12:00", + wishCount: 802, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 353, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 355, + checkIn: "18:00", + checkOut: "12:00", room_counts: 18, }, { id: 25321, name: "푸른하늘펜션(펜트하우스펜션)", address: "강원특별자치도 평창군 진부면 방아다리로 264-56(두일리)", - phone_number: null, + phoneNumber: null, type: 2, - like_count: 479, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 290, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 230, - check_in: "18:00", - check_out: "12:00", + wishCount: 479, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 290, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 230, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 25306, name: "하늘닿은펜션", address: "강원특별자치도 평창군 봉평면 안흥동길 70-99(무이리)", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 636, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 78, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 680, - check_in: "18:00", - check_out: "12:00", + wishCount: 636, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 78, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 680, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 25309, name: "펜스토리", address: "강원특별자치도 평창군 봉평면 두리봉길 30(면온리)", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 470, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 188, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 178, - check_in: "18:00", - check_out: "12:00", + wishCount: 470, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 188, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 178, + checkIn: "18:00", + checkOut: "12:00", room_counts: 7, }, { id: 25306, name: "별헤는밤", address: "강원특별자치도 평창군 봉평면 무이진등길 77-15", - phone_number: null, + phoneNumber: null, type: 9, - like_count: 270, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 154, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 980, - check_in: "18:00", - check_out: "12:00", + wishCount: 270, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 154, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 980, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 25342, name: "대관령아름다운펜션", address: "강원특별자치도 평창군 대관령면 송전1길 32", - phone_number: null, + phoneNumber: null, type: 5, - like_count: 91, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 90, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 314, - check_in: "18:00", - check_out: "12:00", + wishCount: 91, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 90, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 314, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 25340, name: "대관령그린필드펜션", address: "강원특별자치도 평창군 대관령면 차항길 192-77(차항리)", - phone_number: null, + phoneNumber: null, type: 2, - like_count: 409, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 407, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 180, - check_in: "18:00", - check_out: "12:00", + wishCount: 409, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 407, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 180, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 25376, name: "로얄장모텔", address: "강원특별자치도 평창군 평창읍 백오1길 44(하리, 로얄모텔)", - phone_number: null, + phoneNumber: null, type: 4, - like_count: 554, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 95, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 820, - check_in: "18:00", - check_out: "12:00", + wishCount: 554, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 95, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 820, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 25302, name: "평창아름다운세상", address: "강원특별자치도 평창군 봉평면 봉평북로 94-6(원길리)", - phone_number: null, + phoneNumber: null, type: 9, - like_count: 562, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 271, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 30, - check_in: "18:00", - check_out: "12:00", + wishCount: 562, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 271, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 30, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 25306, name: "부성파크", address: "강원특별자치도 평창군 봉평면 안흥동1길 18-4(무이리)", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 821, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 364, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 879, - check_in: "18:00", - check_out: "12:00", + wishCount: 821, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 364, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 879, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 25309, name: "초가집황토펜션", address: "강원특별자치도 평창군 봉평면 태기로 701-7(면온리)", - phone_number: null, + phoneNumber: null, type: 1, - like_count: 59, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 496, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 38, - check_in: "18:00", - check_out: "12:00", + wishCount: 59, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 496, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 38, + checkIn: "18:00", + checkOut: "12:00", room_counts: 11, }, { id: 10841, name: "오블라디풀빌라리조트", address: "경기도 파주시 문산읍 문현말길 113-2", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 823, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 163, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 325, - check_in: "18:00", - check_out: "12:00", + wishCount: 823, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 163, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 325, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 4350, name: "린다픽[한국관광 품질인증/Korea Quality]", address: "서울특별시 용산구 이태원로23길 26-5(이태원동)", - phone_number: "0507-1326-4206", + phoneNumber: "0507-1326-4206", type: 2, - like_count: 837, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 100, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 902, - check_in: "18:00", - check_out: "12:00", + wishCount: 837, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 100, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 902, + checkIn: "18:00", + checkOut: "12:00", room_counts: 18, }, { id: 27497, name: "우제스테이[한국관광 품질인증/Korea Quality]", address: "충청북도 충주시 주정산로 50 크라운호텔", - phone_number: "043-846-9966", + phoneNumber: "043-846-9966", type: 5, - like_count: 14, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 54, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 196, - check_in: "18:00", - check_out: "12:00", + wishCount: 14, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 54, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 196, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 46027, name: "브라운도트호텔 정관점", address: "부산광역시 기장군 정관읍 산단1로 98-31브라운도트 호텔 정관점", - phone_number: null, + phoneNumber: null, type: 7, - like_count: 231, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 77, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 970, - check_in: "18:00", - check_out: "12:00", + wishCount: 231, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 77, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 970, + checkIn: "18:00", + checkOut: "12:00", room_counts: 5, }, { id: 49037, name: "베이하운드호텔", address: "부산광역시 영도구 태종로65번길 11 (대교동1가)베이하운드호텔", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 582, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 150, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 108, - check_in: "18:00", - check_out: "12:00", + wishCount: 582, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 150, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 108, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 63525, name: "디아넥스호텔", address: "제주특별자치도 서귀포시 안덕면 산록남로762번길 71", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 659, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 59, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 272, - check_in: "18:00", - check_out: "12:00", + wishCount: 659, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 59, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 272, + checkIn: "18:00", + checkOut: "12:00", room_counts: 24, }, { id: 63623, name: "카세로지", address: "제주특별자치도 서귀포시 표선면 가시로 383", - phone_number: null, + phoneNumber: null, type: 8, - like_count: 959, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 212, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 350, - check_in: "18:00", - check_out: "12:00", + wishCount: 959, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 212, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 350, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 63535, name: "중문 파르나스 제주", address: "제주특별자치도 서귀포시 색달동 3039-1", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 96, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 175, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 622, - check_in: "18:00", - check_out: "12:00", + wishCount: 96, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 175, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 622, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 32416, name: "세심천온천호텔", address: "충청남도 예산군 삽교읍 수암산로 210", - phone_number: null, + phoneNumber: null, type: 3, - like_count: 639, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 417, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 5, - check_in: "18:00", - check_out: "12:00", + wishCount: 639, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 417, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 5, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 50584, name: "배내의아침", address: "경상남도 양산시 원동면 어실로 1425", - phone_number: "055-388-4508", + phoneNumber: "055-388-4508", type: 3, - like_count: 603, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 318, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 396, - check_in: "18:00", - check_out: "12:00", + wishCount: 603, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 318, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 396, + checkIn: "18:00", + checkOut: "12:00", room_counts: 9, }, { id: 53334, name: "거제 썬트리팜리조트", address: "경상남도 거제시 남부면 거제대로 283", - phone_number: "055-632-7977", + phoneNumber: "055-632-7977", type: 0, - like_count: 344, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 362, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 855, - check_in: "18:00", - check_out: "12:00", + wishCount: 344, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 362, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 855, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 48303, name: "호텔 아쿠아펠리스", address: "부산광역시 수영구 광안해변로 225", - phone_number: "051-790-2300", + phoneNumber: "051-790-2300", type: 4, - like_count: 896, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 257, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 191, - check_in: "18:00", - check_out: "12:00", + wishCount: 896, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 257, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 191, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 32615, name: "자연속으로", address: "충청남도 공주시 계룡면 되찬길 44", - phone_number: "010-3104-3854", + phoneNumber: "010-3104-3854", type: 4, - like_count: 278, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 188, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 571, - check_in: "18:00", - check_out: "12:00", + wishCount: 278, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 188, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 571, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 24026, name: "마운틴밸리 펜션", address: "강원특별자치도 철원군 동송읍 담터길 174", - phone_number: "033-455-5596", + phoneNumber: "033-455-5596", type: 5, - like_count: 90, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 63, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 3, - check_in: "18:00", - check_out: "12:00", + wishCount: 90, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 63, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 3, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 26155, name: "메이힐스리조트", address: "강원특별자치도 정선군 고한읍 물한리길 8", - phone_number: "033-590-1000", + phoneNumber: "033-590-1000", type: 7, - like_count: 772, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 341, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 260, - check_in: "18:00", - check_out: "12:00", + wishCount: 772, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 341, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 260, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 24023, name: "한탄리버스파호텔", address: "강원특별자치도 철원군 동송읍 태봉로 1799-22", - phone_number: "033-455-1234", + phoneNumber: "033-455-1234", type: 1, - like_count: 757, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 271, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 692, - check_in: "18:00", - check_out: "12:00", + wishCount: 757, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 271, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 692, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 53079, name: "파도소리펜션", address: "경상남도 통영시 수륙길 5", - phone_number: "055-641-7755", + phoneNumber: "055-641-7755", type: 1, - like_count: 856, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 496, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 10, - check_in: "18:00", - check_out: "12:00", + wishCount: 856, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 496, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 10, + checkIn: "18:00", + checkOut: "12:00", room_counts: 7, }, { id: 32166, name: "꽃지화이트펜션", address: "충청남도 태안군 안면읍 꽃지해안로 484", - phone_number: "010-7188-0343", + phoneNumber: "010-7188-0343", type: 5, - like_count: 114, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 250, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 312, - check_in: "18:00", - check_out: "12:00", + wishCount: 114, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 250, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 312, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 32152, name: "소나무리조트", address: "충청남도 태안군 남면 몽산포길 179", - phone_number: "010-8335-4310", + phoneNumber: "010-8335-4310", type: 8, - like_count: 812, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 319, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 886, - check_in: "18:00", - check_out: "12:00", + wishCount: 812, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 319, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 886, + checkIn: "18:00", + checkOut: "12:00", room_counts: 16, }, { id: 26233, name: "동방모텔", address: "강원특별자치도 영월군 영월읍 단종로 3-1", - phone_number: "033-373-4921", + phoneNumber: "033-373-4921", type: 3, - like_count: 317, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 214, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 496, - check_in: "18:00", - check_out: "12:00", + wishCount: 317, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 214, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 496, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 26224, name: "드림힐펜션", address: "강원특별자치도 영월군 영월읍 봉래산로 339-6", - phone_number: "033-375-1234", + phoneNumber: "033-375-1234", type: 9, - like_count: 538, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 451, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 920, - check_in: "18:00", - check_out: "12:00", + wishCount: 538, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 451, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 920, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 24607, name: "숲속힐링이야기", address: "강원특별자치도 인제군 북면 설악로 3216", - phone_number: "033-462-0707", + phoneNumber: "033-462-0707", type: 0, - like_count: 799, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 305, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 95, - check_in: "18:00", - check_out: "12:00", + wishCount: 799, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 305, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 95, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 24059, name: "꿈의궁전모텔", address: "강원특별자치도 철원군 서면 와수1로 7", - phone_number: "033-458-6153", + phoneNumber: "033-458-6153", type: 7, - like_count: 195, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 363, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 818, - check_in: "18:00", - check_out: "12:00", + wishCount: 195, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 363, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 818, + checkIn: "18:00", + checkOut: "12:00", room_counts: 22, }, { id: 24059, name: "대명관광호텔", address: "강원특별자치도 철원군 서면 와수로181번길 15", - phone_number: null, + phoneNumber: null, type: 4, - like_count: 545, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 70, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 676, - check_in: "18:00", - check_out: "12:00", + wishCount: 545, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 70, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 676, + checkIn: "18:00", + checkOut: "12:00", room_counts: 21, }, { id: 24023, name: "드림파크모텔", address: "강원특별자치도 철원군 동송읍 창동로 2407", - phone_number: "033-455-9199", + phoneNumber: "033-455-9199", type: 5, - like_count: 749, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 179, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 912, - check_in: "18:00", - check_out: "12:00", + wishCount: 749, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 179, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 912, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 25302, name: "붓꽃섬아트인아일랜드", address: "강원특별자치도 평창군 봉평면 봉평북로 193-28", - phone_number: "033-336-1771", + phoneNumber: "033-336-1771", type: 4, - like_count: 841, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 272, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 205, - check_in: "18:00", - check_out: "12:00", + wishCount: 841, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 272, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 205, + checkIn: "18:00", + checkOut: "12:00", room_counts: 14, }, { id: 24121, name: "로터스모텔", address: "강원특별자치도 화천군 화천읍 상승로1길 6", - phone_number: "033-442-0414", + phoneNumber: "033-442-0414", type: 4, - like_count: 196, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 444, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 337, - check_in: "18:00", - check_out: "12:00", + wishCount: 196, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 444, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 337, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, { id: 32158, name: "서해송", address: "충청남도 태안군 남면 마검포길 335", - phone_number: "010-4172-8068", + phoneNumber: "010-4172-8068", type: 4, - like_count: 170, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 462, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 206, - check_in: "18:00", - check_out: "12:00", + wishCount: 170, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 462, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 206, + checkIn: "18:00", + checkOut: "12:00", room_counts: 5, }, { id: 32158, name: "엘도라도펜션", address: "충청남도 태안군 남면 마검포길 331", - phone_number: "010-9545-2434", + phoneNumber: "010-9545-2434", type: 3, - like_count: 498, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 319, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 797, - check_in: "18:00", - check_out: "12:00", + wishCount: 498, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 319, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 797, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 32166, name: "꽃향기펜션", address: "충청남도 태안군 안면읍 방포로 111", - phone_number: "010-2410-3500", + phoneNumber: "010-2410-3500", type: 1, - like_count: 101, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 458, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 16, - check_in: "18:00", - check_out: "12:00", + wishCount: 101, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 458, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 16, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 25111, name: "세도나 펜션", address: "강원특별자치도 홍천군 북방면 노일로238번길 33", - phone_number: "033-435-5156", + phoneNumber: "033-435-5156", type: 1, - like_count: 99, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 183, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 787, - check_in: "18:00", - check_out: "12:00", + wishCount: 99, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 183, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 787, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 26008, name: "알프스모텔", address: "강원특별자치도 태백시 서황지로 16-11", - phone_number: "033-552-2620", + phoneNumber: "033-552-2620", type: 6, - like_count: 370, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 171, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 713, - check_in: "18:00", - check_out: "12:00", + wishCount: 370, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 171, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 713, + checkIn: "18:00", + checkOut: "12:00", room_counts: 7, }, { id: 26008, name: "패스텔모텔", address: "강원특별자치도 태백시 서황지로 16-8", - phone_number: "033-553-1881", + phoneNumber: "033-553-1881", type: 3, - like_count: 729, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 265, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 116, - check_in: "18:00", - check_out: "12:00", + wishCount: 729, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 265, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 116, + checkIn: "18:00", + checkOut: "12:00", room_counts: 14, }, { id: 11119, name: "하늘풍경 펜션", address: "경기도 포천시 일동면 운악청계로1480번길 90-15", - phone_number: "031-536-2300", + phoneNumber: "031-536-2300", type: 0, - like_count: 176, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 495, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 893, - check_in: "18:00", - check_out: "12:00", + wishCount: 176, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 495, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 893, + checkIn: "18:00", + checkOut: "12:00", room_counts: 18, }, { id: 37928, name: "호미곶펜션", address: "경상북도 포항시 남구 호미곶면 구만길 128", - phone_number: "054-284-0226", + phoneNumber: "054-284-0226", type: 1, - like_count: 106, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 273, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 501, - check_in: "18:00", - check_out: "12:00", + wishCount: 106, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 273, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 501, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 32151, name: "그림속풍경 펜션", address: "충청남도 태안군 남면 진산1길 277-115", - phone_number: "010-7688-7080", + phoneNumber: "010-7688-7080", type: 1, - like_count: 357, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 362, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 329, - check_in: "18:00", - check_out: "12:00", + wishCount: 357, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 362, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 329, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 32151, name: "해나지풀빌라", address: "충청남도 태안군 남면 진산1길 277-129", - phone_number: "041-675-8114", + phoneNumber: "041-675-8114", type: 3, - like_count: 79, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 403, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 711, - check_in: "18:00", - check_out: "12:00", + wishCount: 79, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 403, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 711, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 17523, name: "안성 프로방스 펜션", address: "경기도 안성시 죽산면 용설호수길 134", - phone_number: "031-676-9904", + phoneNumber: "031-676-9904", type: 2, - like_count: 558, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 111, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 128, - check_in: "18:00", - check_out: "12:00", + wishCount: 558, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 111, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 128, + checkIn: "18:00", + checkOut: "12:00", room_counts: 6, }, { id: 59689, name: "여천모텔", address: "전라남도 여수시 시청동2길 36(학동)", - phone_number: "061-685-8051", + phoneNumber: "061-685-8051", type: 7, - like_count: 311, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 402, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 438, - check_in: "18:00", - check_out: "12:00", + wishCount: 311, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 402, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 438, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 59640, name: "카라모텔", address: "전라남도 여수시 무선6길 42-4(선원동)", - phone_number: "061-692-2877", + phoneNumber: "061-692-2877", type: 9, - like_count: 74, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 379, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 1, - view_count: 531, - check_in: "18:00", - check_out: "12:00", + wishCount: 74, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 379, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 531, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 59697, name: "트윈스모텔", address: "전라남도 여수시 둔덕3길 12", - phone_number: "061-653-3200", + phoneNumber: "061-653-3200", type: 8, - like_count: 328, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 90, - likes_available: true, - category_parking: 0, - category_cooking: 0, - category_pickup: 0, - view_count: 604, - check_in: "18:00", - check_out: "12:00", + wishCount: 328, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 90, + isWish: true, + categoryParking: 0, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 604, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 59714, name: "황제파크모텔", address: "전라남도 여수시 오림2길 12-4", - phone_number: "061-654-6644", + phoneNumber: "061-654-6644", type: 9, - like_count: 832, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 370, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 922, - check_in: "18:00", - check_out: "12:00", + wishCount: 832, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 370, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 922, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 63621, name: "노인과바다", address: "제주특별자치도 서귀포시 남원읍 남태해안로 11-12", - phone_number: "064-764-9966", + phoneNumber: "064-764-9966", type: 2, - like_count: 353, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 309, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 846, - check_in: "18:00", - check_out: "12:00", + wishCount: 353, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 309, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 846, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 63625, name: "아망뜨펜션(제주)", address: "제주특별자치도 서귀포시 표선면 민속해안로 11", - phone_number: "064-787-0300", + phoneNumber: "064-787-0300", type: 7, - like_count: 923, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 66, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 125, - check_in: "18:00", - check_out: "12:00", + wishCount: 923, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 66, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 125, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 63639, name: "해뜨는집 펜션", address: "제주특별자치도 서귀포시 성산읍 한도로 137", - phone_number: "064-784-8812", + phoneNumber: "064-784-8812", type: 3, - like_count: 321, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 235, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 903, - check_in: "18:00", - check_out: "12:00", + wishCount: 321, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 235, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 903, + checkIn: "18:00", + checkOut: "12:00", room_counts: 12, }, { id: 51101, name: "북면황토방온천장", address: "경상남도 창원시 의창구 북면 천주로1170번길 17-10", - phone_number: "055-298-9890", + phoneNumber: "055-298-9890", type: 6, - like_count: 803, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 255, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 569, - check_in: "18:00", - check_out: "12:00", + wishCount: 803, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 255, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 569, + checkIn: "18:00", + checkOut: "12:00", room_counts: 13, }, { id: 27347, name: "호텔 필림37.2[한국관광 품질인증/Korea Quality]", address: "충청북도 충주시 연원로 17 필립372호텔", - phone_number: "043-842-0515", + phoneNumber: "043-842-0515", type: 1, - like_count: 923, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 217, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 582, - check_in: "18:00", - check_out: "12:00", + wishCount: 923, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 217, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 582, + checkIn: "18:00", + checkOut: "12:00", room_counts: 19, }, { id: 25116, name: "강가의성", address: "강원특별자치도 홍천군 북방면 굴지강변로 281", - phone_number: "033-433-8309", + phoneNumber: "033-433-8309", type: 6, - like_count: 661, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 202, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 1, - view_count: 853, - check_in: "18:00", - check_out: "12:00", + wishCount: 661, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 202, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 853, + checkIn: "18:00", + checkOut: "12:00", room_counts: 23, }, { id: 23060, name: "태양의해변", address: "인천광역시 강화군 화도면 해안남로 2782-26", - phone_number: "032-937-7877", + phoneNumber: "032-937-7877", type: 6, - like_count: 998, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 158, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 1, - view_count: 896, - check_in: "18:00", - check_out: "12:00", + wishCount: 998, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 158, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 896, + checkIn: "18:00", + checkOut: "12:00", room_counts: 4, }, { id: 25351, name: "홀리데이인&스위트 알펜시아 평창", address: "강원특별자치도 평창군 대관령면 솔봉로 325", - phone_number: "033-339-1240", + phoneNumber: "033-339-1240", type: 3, - like_count: 94, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 451, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 931, - check_in: "18:00", - check_out: "12:00", + wishCount: 94, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 451, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 931, + checkIn: "18:00", + checkOut: "12:00", room_counts: 10, }, { id: 38117, name: "경주스위트호텔", address: "경상북도 경주시 보문로 280-12", - phone_number: null, + phoneNumber: null, type: 0, - like_count: 493, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 168, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 413, - check_in: "18:00", - check_out: "12:00", + wishCount: 493, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 168, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 413, + checkIn: "18:00", + checkOut: "12:00", room_counts: 8, }, { id: 63569, name: "샤뜰레 펜션", address: "제주특별자치도 서귀포시 서호호근로46번길 68", - phone_number: "064-738-9852", + phoneNumber: "064-738-9852", type: 8, - like_count: 687, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 281, - likes_available: true, - category_parking: 0, - category_cooking: 1, - category_pickup: 0, - view_count: 793, - check_in: "18:00", - check_out: "12:00", + wishCount: 687, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 281, + isWish: true, + categoryParking: 0, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 793, + checkIn: "18:00", + checkOut: "12:00", room_counts: 1, }, { id: 33509, name: "무창포 비체팰리스", address: "충청남도 보령시 웅천읍 열린바다1길 78", - phone_number: "041-939-5757", + phoneNumber: "041-939-5757", type: 9, - like_count: 581, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 432, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 1, - view_count: 992, - check_in: "18:00", - check_out: "12:00", + wishCount: 581, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 432, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 1, + viewCount: 992, + checkIn: "18:00", + checkOut: "12:00", room_counts: 20, }, { id: 63599, name: "제주펜션 향림원", address: "제주특별자치도 서귀포시 칠십리로394번길 8-3", - phone_number: "064-733-5799", + phoneNumber: "064-733-5799", type: 6, - like_count: 142, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 67, - likes_available: true, - category_parking: 1, - category_cooking: 1, - category_pickup: 0, - view_count: 684, - check_in: "18:00", - check_out: "12:00", + wishCount: 142, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 67, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 684, + checkIn: "18:00", + checkOut: "12:00", room_counts: 17, }, { id: 16491, name: "이비스 앰배서더 수원", address: "경기도 수원시 팔달구 권광로 132", - phone_number: "031-230-5000", + phoneNumber: "031-230-5000", type: 4, - like_count: 95, - thumbnail_url: - "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", - price: 262, - likes_available: true, - category_parking: 1, - category_cooking: 0, - category_pickup: 0, - view_count: 728, - check_in: "18:00", - check_out: "12:00", + wishCount: 95, + thumbnailUrl: + "https://a.cdn-hotels.com/gdcs/production121/d379/1ed93e45-eaf4-456f-b6d4-6e4a3aa2087e.jpg?impolicy=fcrop&w=1600&h=1066&q=medium", + lowest_price: 262, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 728, + checkIn: "18:00", + checkOut: "12:00", room_counts: 2, }, ]; diff --git a/src/mocks/browser/handlers.ts b/src/mocks/browser/handlers.ts index 8fe1981..3b12cbf 100644 --- a/src/mocks/browser/handlers.ts +++ b/src/mocks/browser/handlers.ts @@ -1,17 +1,17 @@ import { http, HttpResponse } from "msw"; import { MockUpData } from "./constant"; import { - cartData, cartList, orderData, roomDetail, + paymentData, } from "../../mock/myPageData"; import * as _ from "lodash"; // 전체 숙소 보기 + 필터링 export const handlers = [ // eslint-disable-next-line @typescript-eslint/no-unused-vars - http.get("/api/v1/accommodations", ({ request }) => { + http.get("/api/accommodations", ({ request }) => { const url = new URL(request.url); const page = Number(url.searchParams.get("page")) || 0; const typeParam = url.searchParams.get("type"); @@ -24,20 +24,20 @@ export const handlers = [ ? Number(regionParam) : null; - const categoryParkingParam = url.searchParams.get("category_parking"); - const category_parking = + const categoryParkingParam = url.searchParams.get("categoryParking"); + const categoryParking = categoryParkingParam !== null && categoryParkingParam !== undefined ? Number(categoryParkingParam) : null; - const categoryCookingParam = url.searchParams.get("category_cooking"); - const category_cooking = + const categoryCookingParam = url.searchParams.get("categoryCooking"); + const categoryCooking = categoryCookingParam !== null && categoryCookingParam !== undefined ? Number(categoryCookingParam) : null; - const categoryPickupParam = url.searchParams.get("category_pickup"); - const category_pickup = + const categoryPickupParam = url.searchParams.get("categoryPickup"); + const categoryPickup = categoryPickupParam !== null && categoryPickupParam !== undefined ? Number(categoryPickupParam) : null; @@ -45,19 +45,15 @@ export const handlers = [ const categoryData = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any[], // 데이터 배열 - category_parking: number | null, - category_cooking: number | null, - category_pickup: number | null, + categoryParking: number | null, + categoryCooking: number | null, + categoryPickup: number | null, ) => { const filterData = _.filter(data, (item) => { return ( - (category_parking - ? item.category_parking === category_parking - : true) && - (category_cooking - ? item.category_cooking === category_cooking - : true) && - (category_pickup ? item.category_pickup === category_pickup : true) + (categoryParking ? item.categoryParking === categoryParking : true) && + (categoryCooking ? item.categoryCooking === categoryCooking : true) && + (categoryPickup ? item.categoryPickup === categoryPickup : true) ); }); @@ -66,9 +62,9 @@ export const handlers = [ const allCategoryData = categoryData( MockUpData, - category_parking, - category_cooking, - category_pickup, + categoryParking, + categoryCooking, + categoryPickup, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -148,7 +144,7 @@ export const handlers = [ const data = { room_basket_id: 1, - accommdation_name: "최고 호텔", + accommodation_name: "최고 호텔", room_name: "스위트룸", price: 40000, number_guests: newPost.number_guests, @@ -164,13 +160,17 @@ export const handlers = [ }), // 장바구니에서 상품 선택 후 주문 주문 요청 - http.post("/api/v1/baskets/orders", async () => { - const data = [cartData[1], cartData[2]]; + http.post("/api/v1/baskets/orders", async ({ request }) => { + const newPost = (await request.json()) as { + room_basket_id: number[]; + }; + + // 처리할 로직 추가 return HttpResponse.json({ message: "성공", - order_id: 2, - data: data, + data: 2, + id: newPost, }); }), @@ -184,7 +184,7 @@ export const handlers = [ const data = { room_basket_id: 1, - accommdation_name: "최고 호텔", + accommodation_name: "최고 호텔", room_name: "스위트룸", price: 40000, number_guests: newPost.number_guests, @@ -221,12 +221,23 @@ export const handlers = [ }); }), + // 결제시 주문 취소 요청 + http.delete("/api/v1/orders/:order_id", () => { + return HttpResponse.json({ + message: "주문 취소 성공", + }); + }), + // 장바구니 조회 http.get("/api/v1/baskets", async () => { return HttpResponse.json({ message: "성공", - basket_id: 1, - order_datas: cartList, + data: { + basket_id: 1, + total_price: 30000, + total_Count: 5, + rooms: cartList, + }, }); }), @@ -240,7 +251,29 @@ export const handlers = [ }); }), + // 결제 내역 전체 조회 + http.get("/api/v1/payment", async () => { + return HttpResponse.json({ + message: "성공", + data: paymentData, + }); + }), + // 주문 내역 상세 조회 (주문 번호로 조회할 수 있는 api) + http.get("/api/v1/payment/:payment_id", async ({ params }) => { + const { payment_id } = params; + + const newData = paymentData.filter( + (item) => item.payment_id === ~~payment_id, + ); + + return HttpResponse.json({ + message: "성공", + data: newData, + }); + }), + + // 결제 내역 상세 조회 (주문 번호로 조회할 수 있는 api) http.get("/api/v1/orders/:order_id", async ({ params }) => { const { order_id } = params; @@ -279,6 +312,9 @@ export const handlers = [ const { room_id } = params; const resData = roomDetail.rooms.filter((room) => room.id === ~~room_id); + // const resData = roomDetail.rooms.find( + // (room) => room.id === Number(room_id), + // ); return HttpResponse.json({ message: "성공", @@ -294,14 +330,14 @@ export const handlers = [ }), // 숙소 좋아요 취소 시 요청 - http.delete("/api/vi/wish/:accommodationId", () => { + http.delete("/api/wish/:accommodationId", () => { return HttpResponse.json({ message: "좋아요 취소", }); }), // 숙소 좋아요 리스트 조회 (무작위로 1~40개의 데이터를 보내줌) - http.get("/api/v1/wish", ({ request }) => { + http.get("/api/wish", ({ request }) => { const url = new URL(request.url); console.log(url); diff --git a/src/pages/Cart/Cart.test.tsx b/src/pages/Cart/Cart.test.tsx new file mode 100644 index 0000000..3fd7395 --- /dev/null +++ b/src/pages/Cart/Cart.test.tsx @@ -0,0 +1,50 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; +import { RecoilRoot } from "recoil"; +import Cart from "."; +import { useGetMyCart } from "../../hooks/useCartFetch"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockedUseGetMyCart = useGetMyCart as jest.Mock; +jest.mock("../../hooks/useCartFetch"); + +const createWrapper = () => { + const queryClient = new QueryClient(); + + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); +}; + +describe("장바구니 로딩 테스트", () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + test("로딩중일 때 장바구니라는 문구가 보여서는 안된다.", () => { + mockedUseGetMyCart.mockImplementation(() => ({ + isLoading: true, + })); + + render(, { wrapper: createWrapper() }); + + const textElement = screen.queryByText("장바구니"); + expect(textElement).not.toBeInTheDocument(); + }); + + test("로딩중이 아닐 때 로딩중이라는 문구가 보여서는 안된다.", () => { + mockedUseGetMyCart.mockImplementation(() => ({ + isLoading: false, + })); + + render(, { wrapper: createWrapper() }); + + const textElement = screen.queryByText("로딩중"); + expect(textElement).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/Cart/index.tsx b/src/pages/Cart/index.tsx index 25cc7c9..d224cec 100644 --- a/src/pages/Cart/index.tsx +++ b/src/pages/Cart/index.tsx @@ -1,109 +1,10 @@ -import { useEffect, useState } from "react"; -import CartItem from "../../components/Cart/CartItem"; -import CartZero from "../../components/Cart/CartZero"; -import Checkbox from "../../components/Cart/Checkbox"; -import Estimate from "../../components/Cart/Estimate"; -import Button from "../../components/Common/Button"; +import CartList from "../../components/Cart/CartList"; import { useGetMyCart } from "../../hooks/useCartFetch"; -import { CartItemType } from "../../types"; -import * as Styled from "./Cart.styles"; -import { handleAllCheck, handleSelectDeleteClick } from "./Cart.utils"; const Cart = () => { const { data } = useGetMyCart(); // 카트 목록 데이터 요청 - const [cart, setCart] = useState([]); // 실제 api로 받을 데이터 - const [selected, setSelected] = useState(0); // 선택된 아이템 개수 - const [allSelected, setAllSelected] = useState(true); // 전체 선택 여부 - const [estimatedPrice, setEstimatedPrice] = useState([]); // 예상 구매 내역 리스트 - const [select, setSelect] = useState([]); // 개별 아이템에 대한 체크 여부 - // 카트 데이터 요청 후 페이지 상태 저장 - useEffect(() => { - if (data) { - setCart([...data.data.order_datas]); - setSelect( - Array.from({ length: data.data.order_datas.length }, () => true), - ); - setSelected(data.data.order_datas.length); - setEstimatedPrice([...data.data.order_datas]); - } - }, [data]); - - // 개별 아이템 중 1개라도 체크 해제 시 전체 채크 비활성 - useEffect(() => { - if (select.includes(false)) setAllSelected(false); - }, [select]); - - // 카트 아이템 없을 시 다른 화면 리턴 - if (cart.length === 0) { - return ; - } - - return ( - - - 장바구니 목록 - - - - { - handleAllCheck( - event, - cart, - cart.length, - setSelected, - setAllSelected, - setEstimatedPrice, - setSelect, - ); - }} - /> - - - - + + + +
+
); }; diff --git a/src/pages/Payment/Payment.test.tsx b/src/pages/Payment/Payment.test.tsx new file mode 100644 index 0000000..eed41a3 --- /dev/null +++ b/src/pages/Payment/Payment.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import Payment from '.'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; + +// useRecoilState를 mock하기 위한 코드 +jest.mock('recoil', () => ({ + ...jest.requireActual('recoil'), + useRecoilState: jest.fn(), +})); + +const createWrapper = () => { + const queryClient = new QueryClient(); + return ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + }; + +describe('Payment 컴포넌트', () => { + test('orderID가 null이면 NotFound 컴포넌트가 렌더링되어야 함', () => { + // useRecoilState가 order_id를 null로 반환하도록 설정 + // eslint-disable-next-line @typescript-eslint/no-var-requires + jest.spyOn(require('recoil'), 'useRecoilState').mockReturnValue([ { order_id: null }, jest.fn() ]); + + render(, { wrapper: createWrapper() }); + + // NotFound 텍스트가 있는지 확인 + const notFoundText = screen.getByText('뒤로가기'); + expect(notFoundText).toBeInTheDocument(); + }); + +}); diff --git a/src/pages/Payment/index.tsx b/src/pages/Payment/index.tsx index 8e22ddc..3687751 100644 --- a/src/pages/Payment/index.tsx +++ b/src/pages/Payment/index.tsx @@ -2,8 +2,18 @@ import ItemList from "../../components/Payment/ItemList/index"; import TermsAndConditions from "../../components/Payment/TermsAndConditions/index"; import Btn from "../../components/Payment/Btn/index"; import * as Styled from "./Payment.styles"; +import { useRecoilState } from "recoil"; +import { purchaseState } from "../../store/purchaseAtom"; +import NotFound from "../NotFound"; const Payment = () => { + const [purchaseList] = useRecoilState(purchaseState); + const orderId = purchaseList.order_id; + + if (orderId === null) { + return ; + } + return ( diff --git a/src/pages/Search/Search.styles.ts b/src/pages/Search/Search.styles.ts index 5d00a89..2ae90af 100644 --- a/src/pages/Search/Search.styles.ts +++ b/src/pages/Search/Search.styles.ts @@ -1,10 +1,4 @@ import styled from "@emotion/styled"; -import { - OptionItemProps, - RegionItemProps, - TypeItemProps, - TypeWrapperProps, -} from "./Search.types"; export const Container = styled.div` display: flex; @@ -13,74 +7,39 @@ export const Container = styled.div` align-items: center; max-width: 55rem; - padding: 2em; + padding: 2rem; margin: 0 auto; background-color: white; h2 { - margin-top: 1em; - margin-bottom: 1em; + margin-top: 1rem; + margin-bottom: 1rem; font-size: 1.25rem; font-weight: 700; } -`; -export const SearchHeader = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - gap: 1em; - - h2 { - margin-right: 1em; - } -`; - -export const SearchTitle = styled.div` - width: 20em; -`; - -export const SearchParamsWrapper = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - height: 100%; -`; - -export const SearchParams = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - flex: 1 0 40%; - - gap: 0.5em; - - font-size: 1.1em; - font-weight: 500; + @media (max-width: 768px) { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; - .region, - .type { - padding: 0.3em; - background-color: #e6e6e6; + width: 100%; + height: 100%; + padding: 0; } - .option { + @media (max-width: 480px) { display: flex; - flex-direction: row; + flex-direction: column; + justify-content: center; + align-items: center; - gap: 0.5em; - } - - .option-each { - padding: 0.3em; - background-color: #e6e6e6; + width: 100%; + height: 100%; + padding: 0; } `; @@ -90,201 +49,32 @@ export const SearchCard = styled.div` justify-content: flex-start; align-items: flex-start; - padding: 0.7em; - gap: 0.6em; + padding: 0.7rem; + gap: 0.6rem; width: 100%; max-width: 60rem; height: 30rem; border: 1px solid #e6e6e6; - border-radius: 1em; -`; - -export const SearchCardWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: 1; - position: relative; - - height: 100%; - - background-color: #ff6c27; - /* background-color: #191554; */ - border: 1px solid #e6e6e6; - border-radius: 1em; - - overflow: hidden; - transition: all 0.5s; - cursor: pointer; - &:hover { - flex: ${({ isType }) => (isType ? "4.5" : "2.5")}; - - background-color: white; - border: 1px solid #ff5100; + border-radius: 1rem; - span { - color: #ff5100; - } - } - - span { - min-width: 14em; - - transition: all 0.5s; - text-align: center; - text-transform: uppercase; - color: white; - font-weight: 700; - letter-spacing: 0.1em; - } - - img { - width: 3.7em; - height: 3.7em; - } -`; - -export const SelectedItemDisplay = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: 30% 0 0; - - width: 100%; - gap: 0.3em; - - background-color: white; - border-top: 1px solid #e6e6e6; - font-size: 1.3em; -`; - -export const RegionList = styled.div` - display: block; - flex-direction: column; - - width: 60%; -`; - -export const RegionItem = styled.div` - padding: 0.5em 1em; - margin-bottom: 0.5em; - - background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; - border: ${({ selected }) => - selected ? "1px solid #ff5100" : "1px solid #e6e6e6"}; - border-radius: 0.5em; - - color: ${({ selected }) => (selected ? "white" : "black")}; - - transition: - background-color 0.3s, - color 0.3s; - cursor: pointer; - &:hover { - background-color: #ff5100; - color: white; - } -`; - -export const TypeList = styled.div` - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(6, 1fr); - - gap: 0.2em; - width: 80%; -`; - -export const TypeItem = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - grid-column: ${({ isFullWidth }) => (isFullWidth ? "1 / -1" : "auto")}; - - padding-left: 1em; - padding-top: 0.6em; - padding-bottom: 0.6em; - - width: auto; - height: 2.2em; - border-radius: 0.625rem; - border: 1px solid #e6e6e6; - - background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; - color: ${({ selected }) => (selected ? "white" : "black")}; - transition: - background-color 0.3s, - color 0.3s; - cursor: pointer; - &:hover { - background-color: #ff5100; - color: white; - } - - img { - width: 3.4em; - height: 3.4em; - border-radius: 0 0.625rem 0.625rem 0; - filter: ${({ selected }) => (selected ? "none" : "grayscale(100%)")}; - transition: filter 0.3s; - } - - &:hover img { - filter: none; - } -`; - -export const OptionList = styled.div` - display: grid; - grid-template-rows: repeat(4, auto); - justify-content: center; - align-items: center; - - gap: 0.4em; - width: 90%; -`; - -export const OptionItem = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - - padding: 1em; - - width: 15em; - height: 3em; - border-radius: 0.5em; - border: ${({ selected }) => - selected ? "1px solid #ff5100" : "1px solid #e6e6e6"}; - - background-color: ${({ selected }) => (selected ? "#ff5100" : "white")}; - color: ${({ selected }) => (selected ? "white" : "black")}; - - transition: - background-color 0.3s, - color 0.3s; + @media (max-width: 768px) { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; - cursor: pointer; - &:hover { - background-color: #ff5100; - color: white; + width: 100%; + height: 100%; } - img { - width: 3.5em; - height: 3.5em; - - object-fit: contain; - filter: ${({ selected }) => (selected ? "none" : "grayscale(100%)")}; - transition: filter 0.3s; - } + @media (max-width: 480px) { + flex-direction: column; + justify-content: center; + align-items: center; - &:hover img { - filter: none; + width: 100%; + height: 100%; } `; diff --git a/src/pages/Search/Search.types.ts b/src/pages/Search/Search.types.ts index fe5ec56..e69de29 100644 --- a/src/pages/Search/Search.types.ts +++ b/src/pages/Search/Search.types.ts @@ -1,37 +0,0 @@ -export interface SearchType { - region: number | null; - type: number | null; - category_parking: boolean; - category_cooking: boolean; - category_pickup: boolean; -} - -export interface TypeImages { - [key: number]: string; -} - -export interface OptionImages { - [key: number]: string; -} - -export interface RegionItemProps { - selected: boolean; -} - -export interface TypeItemProps { - selected: boolean; - isFullWidth?: boolean; -} - -export interface TypeWrapperProps { - isType?: boolean; - isRegionHovered?: boolean; -} - -export interface OptionItemProps { - selected: boolean; -} - -export interface QueryParams { - [key: string]: string; -} diff --git a/src/pages/Search/index.tsx b/src/pages/Search/index.tsx index b1f6b1c..3b81223 100644 --- a/src/pages/Search/index.tsx +++ b/src/pages/Search/index.tsx @@ -1,321 +1,25 @@ -import { useState } from "react"; import * as Styled from "./Search.styles"; -import MapIcon from "../../assets/svg/map-icon.svg"; -import AccomIcon from "../../assets/svg/accom-icon.svg"; -import OptionIcon from "../../assets/svg/option-icon.svg"; -import Type0 from "../../assets/image/type0.png"; -import Type1 from "../../assets/image/type1.jpeg"; -import Type2 from "../../assets/image/type2.jpg"; -import Type3 from "../../assets/image/type3.jpg"; -import Type4 from "../../assets/image/type4.jpg"; -import Type5 from "../../assets/image/type5.jpg"; -import Type6 from "../../assets/image/type6.png"; -import Type7 from "../../assets/image/type7.jpg"; -import Type8 from "../../assets/image/type8.png"; -import Type9 from "../../assets/image/type9.png"; -import { OptionImages, QueryParams, TypeImages } from "./Search.types"; -import Option0 from "../../assets/svg/option0.svg"; -import Option1 from "../../assets/svg/option1.svg"; -import Option2 from "../../assets/svg/option2.svg"; -import { - SearchButton, - SearchResetButton, -} from "../../components/Search/Button"; -import { useNavigate } from "react-router-dom"; +import { SearchButton } from "../../components/Search/Button"; +import { options, regions, types } from "../../store/searchSelectorData"; +import { useSendSearchQuery } from "../../hooks/useSendSearchQuery"; +import { RegionWrapper } from "../../components/Search/SearchSelector/SearchRegion"; +import { TypeWrapper } from "../../components/Search/SearchSelector/SearchType"; +import { OptionsWrapper } from "../../components/Search/SearchSelector/SearchOptions"; +import { SearchParamsDisplay } from "../../components/Search/SearchParamsDisplay"; const Search = () => { - const [selectedRegion, setSelectedRegion] = useState(99); - const [selectedType, setSelectedType] = useState(99); - const [selectedOptions, setSelectedOptions] = useState([99]); - - const [isRegionSelected, setIsRegionSelected] = useState(false); - const [isRegionHovered, setIsRegionHovered] = useState(false); - const [isTypeSelected, setIsTypeSelected] = useState(false); - const [isTypeHovered, setIsTypeHovered] = useState(false); - const [isOptionSelected, setIsOptionSelected] = useState(false); - const [isOptionHovered, setIsOptionHovered] = useState(false); - - const navigate = useNavigate(); - - const handleResetSearch = () => { - setSelectedRegion(99); - setSelectedType(99); - setSelectedOptions([99]); - setIsRegionSelected(false); - setIsTypeSelected(false); - setIsOptionSelected(false); - }; - - const handleRegionClick = (value: number) => { - if (value === selectedRegion) { - setSelectedRegion(99); - setIsRegionSelected(false); - } else { - setSelectedRegion(value); - setIsRegionSelected(true); - } - }; - - const handleTypeClick = (value: number) => { - if (value === selectedType) { - setSelectedType(99); - setIsTypeSelected(false); - } else { - setSelectedType(value); - setIsTypeSelected(true); - } - }; - - const handleOptionClick = (value: number) => { - setSelectedOptions((currentOptions) => { - if (value === 99) { - setIsOptionSelected(true); - return [99]; - } else { - if (currentOptions.includes(99)) { - setIsOptionSelected(true); - return [value]; - } else if (currentOptions.includes(value)) { - return currentOptions.filter((option) => option !== value); - } else { - return [...currentOptions, value]; - } - } - }); - }; - - const handleOptionMouseEnter = () => { - setIsOptionHovered(true); - if (selectedOptions.length === 0) { - setSelectedOptions([99]); - } - }; - - const handleOptionMouseLeave = () => { - setIsOptionHovered(false); - if (selectedOptions.length === 1 && selectedOptions[0] === 99) { - setSelectedOptions([99]); - } - }; - - const sendSearchQuery = () => { - const optionsMap: Record = { - 0: "categoryParking", - 1: "categoryCooking", - 2: "categoryPickup", - }; - - const queryParams: QueryParams = {}; - - selectedOptions.forEach((optionValue) => { - if (optionValue !== 99) { - const key = optionsMap[optionValue]; - queryParams[key] = "1"; - } - }); - - if (selectedRegion !== 99) { - queryParams["region"] = selectedRegion.toString(); - } - if (selectedType !== 99) { - queryParams["type"] = selectedType.toString(); - } - - const queryString = new URLSearchParams(queryParams).toString(); - - navigate(`/results?${queryString}`); - }; - - const [regions] = useState([ - { name: "전국", value: 99 }, - { name: "서울시", value: 0 }, - { name: "경기도", value: 1 }, - { name: "강원도", value: 2 }, - { name: "충청도", value: 3 }, - { name: "전라도", value: 4 }, - { name: "경상도", value: 5 }, - { name: "제주도", value: 6 }, - ]); - - const [types] = useState([ - { name: "전체 타입", value: 99 }, - { name: "호텔", value: 0 }, - { name: "콘도", value: 1 }, - { name: "호스텔", value: 2 }, - { name: "펜션", value: 3 }, - { name: "모텔", value: 4 }, - { name: "민박", value: 5 }, - { name: "게스트하우스", value: 6 }, - { name: "홈스테이", value: 7 }, - { name: "레지던스", value: 8 }, - { name: "한옥", value: 9 }, - ]); - - const typeImages: TypeImages = { - 0: Type0, - 1: Type1, - 2: Type2, - 3: Type3, - 4: Type4, - 5: Type5, - 6: Type6, - 7: Type7, - 8: Type8, - 9: Type9, - }; - - const [options] = useState([ - { name: "상관 없음", value: 99 }, - { name: "주차 가능", value: 0 }, - { name: "조리 가능", value: 1 }, - { name: "픽업 가능", value: 2 }, - ]); - - const optionImages: OptionImages = { - 0: Option0, - 1: Option1, - 2: Option2, - }; + const sendSearchQuery = useSendSearchQuery(); return ( - <> - - - -

원하시는 숙소를 찾아드릴게요 👀 ❤️

-
- - -
- {`${regions.find((region) => region.value === selectedRegion) - ?.name} `} -
- -
- {`${types.find((type) => type.value === selectedType)?.name} `} -
- -
- {selectedOptions.map((optionValue, index) => ( -
- { - options.find((option) => option.value === optionValue) - ?.name - } -
- ))} -
-
- -
-
- - - setIsRegionHovered(true)} - onMouseLeave={() => setIsRegionHovered(false)} - > - - Check 1

지역 선택

- {!isRegionHovered && region-selector} -
- {isRegionHovered && ( - - {regions.map((region) => ( - handleRegionClick(region.value)} - > - {region.name} - - ))} - - )} - {!isRegionHovered && isRegionSelected && ( - - {!isRegionHovered && - regions.find((region) => region.value === selectedRegion) - ?.name}{" "} - ✔️ - - )} -
- - setIsTypeHovered(true)} - onMouseLeave={() => setIsTypeHovered(false)} - isType={true} - > - - Check 2

숙소 타입

- {!isTypeHovered && type-selector} -
- {isTypeHovered && ( - - {types.map((type) => ( - handleTypeClick(type.value)} - isFullWidth={type.name === "전체 타입"} - > - {type.name} - {type.name !== "전체 타입" && ( - {type.name} - )} - - ))} - - )} - {!isTypeHovered && isTypeSelected && ( - - {types.find((type) => type.value === selectedType)?.name} ✔️ - - )} -
- - - - Check 3

추가 옵션

- {!isOptionHovered && ( - option-selector - )} -
- {isOptionHovered && ( - - {options.map((option) => ( - handleOptionClick(option.value)} - > - {option.name} - {option.name !== "상관 없음" && ( - {option.name} - )} - - ))} - - )} - {!isOptionHovered && isOptionSelected && ( - - {selectedOptions.map((selectedValue) => { - const optionName = options.find( - (option) => option.value === selectedValue, - )?.name; - return
{optionName} ✔️
; - })} -
- )} -
- -
-
- + + + + + + + + + ); }; diff --git a/src/pages/SearchList/SearchList.styles.ts b/src/pages/SearchList/SearchList.styles.ts index 55570ac..eb0e24f 100644 --- a/src/pages/SearchList/SearchList.styles.ts +++ b/src/pages/SearchList/SearchList.styles.ts @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { motion } from "framer-motion"; export const SearchResultContainer = styled.div` display: flex; @@ -12,12 +13,45 @@ export const SearchResultContainer = styled.div` box-sizing: border-box; `; -export const ItemWrapper = styled.div` +export const ItemWrapper = styled(motion.div)` + display: flex; + flex-direction: row; + gap: 2rem; + position: relative; - display: flex; - flex-direction: column; - align-items: center; + width: 80%; + + margin: 2rem 0; + + @media (max-width: 768px) { + flex-direction: column; + align-items: center; + width: 100%; + } +`; + +export const Main = styled.div` + width: 75%; + padding: 1rem; + + background-color: #f6f6f6; + border-radius: 20px; + + filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.25)); +`; + +export const Aside = styled.div` + width: 25%; + position: relative; + + background-color: #f6f6f6; + border-radius: 20px; + + filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.25)); - width: 60%; + @media (max-width: 768px) { + width: 100%; + order: -1; + } `; diff --git a/src/pages/SearchList/index.tsx b/src/pages/SearchList/index.tsx index d92b1fb..c60e4e5 100644 --- a/src/pages/SearchList/index.tsx +++ b/src/pages/SearchList/index.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from "react-router-dom"; import CategoryBanner from "../../components/Category/CategoryBanner"; import CategoryQuery from "../../components/Category/CategoryQuery"; import * as Styled from "./SearchList.styles"; @@ -10,27 +9,23 @@ const SearchList = () => { const firstText = "숙소를"; const secondText = "찾으셨나요?"; - const router = useNavigate(); - const handleClickSearch = () => { - router("/search"); - }; return ( - + - - + + + + + + ); diff --git a/src/pages/Signin/Signin.styles.ts b/src/pages/Signin/Signin.styles.ts index ca6b5e9..a02e367 100644 --- a/src/pages/Signin/Signin.styles.ts +++ b/src/pages/Signin/Signin.styles.ts @@ -12,6 +12,7 @@ export const Container = styled.div` ${flexCenter} width: 100%; + min-height: 100vh; height: 100%; background-color: #37afff; diff --git a/src/pages/Signin/index.test.tsx b/src/pages/Signin/index.test.tsx new file mode 100644 index 0000000..6c21ca2 --- /dev/null +++ b/src/pages/Signin/index.test.tsx @@ -0,0 +1,56 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import Signin from "./index"; +import { createWrapper } from "../../test/test.utils"; + +describe("Signin", () => { + it("컴포넌트 렌더링 테스트", () => { + const wrapper = createWrapper(); + render(, { wrapper }); + }); + + it("아무것도 입력하지 않았을 때 에러 메시지 출력 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + userEvent.click(screen.getByRole("button", { name: "로그인" })); + + await waitFor(() => { + expect(screen.getByText("이메일 형식이 아닙니다")).toBeInTheDocument(); + expect(screen.getByText("비밀번호는 8~20자리입니다")).toBeInTheDocument(); + }); + }); + + it("이메일 형식에 123@naver.com을 입력했을 때 에러 메시지 미출력 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + await userEvent.type( + screen.getByRole("textbox", { name: "email" }), + "12345@naver.com", + ); + await userEvent.click(screen.getByRole("button", { name: "로그인" })); + + await waitFor(() => { + expect( + screen.queryByText("이메일 형식이 아닙니다"), + ).not.toBeInTheDocument(); + expect(screen.getByText("비밀번호는 8~20자리입니다")).toBeInTheDocument(); + }); + }); + + it("비밀번호에 12345678을 입력했을 때 에러 메시지 미출력 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + await userEvent.type(screen.getByText("password"), "12345678"); + await userEvent.click(screen.getByRole("button", { name: "로그인" })); + + await waitFor(() => { + expect(screen.getByText("이메일 형식이 아닙니다")).toBeInTheDocument(); + expect( + screen.queryByText("비밀번호는 8~20자리입니다"), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/Signin/index.tsx b/src/pages/Signin/index.tsx index 468165a..27ece27 100644 --- a/src/pages/Signin/index.tsx +++ b/src/pages/Signin/index.tsx @@ -4,15 +4,19 @@ import InputText from "../../components/Signin/InputText"; import { useForm, SubmitHandler } from "react-hook-form"; import { InputProps } from "./Signin.constant"; import { SignInInputs } from "./Signin.types"; +import { useLogin } from "../../api/Auth/query"; const Signin = () => { + const loginMutation = useLogin(); const { register, handleSubmit, formState: { errors }, } = useForm(); - const onSubmit: SubmitHandler = (data) => console.log(data); + const onSubmit: SubmitHandler = (data) => { + loginMutation.mutate(data); + }; return ( diff --git a/src/pages/Signup/Signup.styles.ts b/src/pages/Signup/Signup.styles.ts index 80a5da0..a95932c 100644 --- a/src/pages/Signup/Signup.styles.ts +++ b/src/pages/Signup/Signup.styles.ts @@ -13,6 +13,7 @@ export const Container = styled.div` ${flexCenter} width: 100%; + min-height: 100vh; height: 100%; background-color: #191554; diff --git a/src/pages/Signup/index.test.tsx b/src/pages/Signup/index.test.tsx new file mode 100644 index 0000000..fb4b680 --- /dev/null +++ b/src/pages/Signup/index.test.tsx @@ -0,0 +1,72 @@ +import { screen, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import Signup from "./index"; +import { createWrapper } from "../../test/test.utils"; + +describe("Signup", () => { + it("컴포넌트 렌더링 테스트", () => { + const wrapper = createWrapper(); + render(, { wrapper }); + }); + + it("아무것도 입력하지 않았을 때 에러 메시지 출력 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + userEvent.click(screen.getByRole("button", { name: "회원가입" })); + + await waitFor(() => { + expect(screen.getByText("이메일 형식이 아닙니다")).toBeInTheDocument(); + expect(screen.getByText("비밀번호는 8~20자리입니다")).toBeInTheDocument(); + expect(screen.getByText("닉네임은 2~10자리입니다")).toBeInTheDocument(); + expect(screen.getByText("전화번호 형식이 아닙니다")).toBeInTheDocument(); + }); + }); + + it("이메일 형식을 입력하면 에러 메시지가 사라지는지 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + userEvent.type( + screen.getByRole("textbox", { name: "email" }), + "kim@naver.com", + ); + userEvent.click(screen.getByRole("button", { name: "회원가입" })); + + await waitFor(() => { + expect( + screen.queryByText("이메일 형식이 아닙니다"), + ).not.toBeInTheDocument(); + expect(screen.getByText("비밀번호는 8~20자리입니다")).toBeInTheDocument(); + }); + }); + + it("모든 입력값을 입력하면 에러 메시지가 사라지는지 테스트", async () => { + const wrapper = createWrapper(); + render(, { wrapper }); + + userEvent.type( + screen.getByRole("textbox", { name: "email" }), + "kim@gmail.com", + ); + userEvent.type(screen.getByText("password"), "12345678"); + userEvent.type(screen.getByText("nickname"), "kim"); + userEvent.type(screen.getByText("phone"), "010-1234-5678"); + userEvent.click(screen.getByRole("button", { name: "회원가입" })); + + await waitFor(() => { + expect( + screen.queryByText("이메일 형식이 아닙니다"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("비밀번호는 8~20자리입니다"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("닉네임은 2~10자리입니다"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("전화번호 형식이 아닙니다"), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/Signup/index.tsx b/src/pages/Signup/index.tsx index ffabf20..caec356 100644 --- a/src/pages/Signup/index.tsx +++ b/src/pages/Signup/index.tsx @@ -3,16 +3,19 @@ import * as Styled from "./Signup.styles"; import { useForm, SubmitHandler } from "react-hook-form"; import { Inputs } from "./Signup.types"; import { InputProps } from "./Signup.constant"; +import { useSignUp } from "../../api/Auth/query"; const Signup = () => { + const mutationSignUp = useSignUp(); const { register, handleSubmit, formState: { errors }, } = useForm(); - const onSubmit: SubmitHandler = (data) => - console.log("로그인 데이터 전송", data); + const onSubmit: SubmitHandler = async (data) => { + mutationSignUp.mutate(data); + }; return ( @@ -36,7 +39,7 @@ const Signup = () => { })} 회원가입 - + 로그인 diff --git a/src/store/categoryViewAtom.ts b/src/store/categoryViewAtom.ts new file mode 100644 index 0000000..08eecb0 --- /dev/null +++ b/src/store/categoryViewAtom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil"; + +export const categoryViewAtom = atom({ + key: "categoryViewAtom", + + // 바둑판 뷰가 기본값이므로 true로 설정 + default: true, +}); diff --git a/src/store/checkinCheckOutAtom.ts b/src/store/checkinCheckOutAtom.ts new file mode 100644 index 0000000..6bdeed9 --- /dev/null +++ b/src/store/checkinCheckOutAtom.ts @@ -0,0 +1,19 @@ +import { atom } from "recoil"; +import { recoilPersist } from "recoil-persist"; + +const { persistAtom } = recoilPersist({ + key: "sessionStorage", + storage: sessionStorage, +}); + +export const checkInDateState = atom({ + key: "checkInDateState", + default: null, + effects_UNSTABLE: [persistAtom], +}); + +export const checkOutDateState = atom({ + key: "checkOutDateState", + default: null, + effects_UNSTABLE: [persistAtom], +}); diff --git a/src/store/dateState.ts b/src/store/dateState.ts new file mode 100644 index 0000000..b3cb03b --- /dev/null +++ b/src/store/dateState.ts @@ -0,0 +1,11 @@ +import { atom } from "recoil"; + +export const checkInDateState = atom({ + key: "checkInDateState", + default: null, +}); + +export const checkOutDateState = atom({ + key: "checkOutDateState", + default: null, +}); diff --git a/src/store/paymentCompletedAtom.ts b/src/store/paymentCompletedAtom.ts index cc0c09f..2165739 100644 --- a/src/store/paymentCompletedAtom.ts +++ b/src/store/paymentCompletedAtom.ts @@ -19,12 +19,12 @@ interface AtomType { export const paymentCompletedAtom = atom({ key: "paymentCompletedAtom", default: { - order_datas: [], payment_at: "", payment_id: null, payment_status: "", payment_type: "", total_price: 0, + order_datas: [], }, effects_UNSTABLE: [persistAtom], }); diff --git a/src/store/purchaseAtom.ts b/src/store/purchaseAtom.ts index 7ea5238..8045a12 100644 --- a/src/store/purchaseAtom.ts +++ b/src/store/purchaseAtom.ts @@ -1,16 +1,5 @@ import { atom } from "recoil"; import { recoilPersist } from "recoil-persist"; - -interface Type { - room_basket_id: number; - accommdation_name: string; - room_name: string; - check_in_at: string; - check_out_at: string; - number_guests: number; - price: number; -} - const { persistAtom } = recoilPersist({ key: "sessionStorage", storage: sessionStorage, @@ -19,23 +8,11 @@ const { persistAtom } = recoilPersist({ export const purchaseState = atom<{ totalPrice: number; order_id: number | null; - data: Type[]; }>({ key: "purchaseState", default: { totalPrice: 0, order_id: null, - data: [ - { - room_basket_id: 4, - accommdation_name: "제주 라마다 호텔", - room_name: "스위트룸", - check_in_at: "11-11-11", - check_out_at: "11-11-11", - number_guests: 3, - price: 1233, - }, - ], }, effects_UNSTABLE: [persistAtom], }); diff --git a/src/store/searchSelectorAtom.ts b/src/store/searchSelectorAtom.ts new file mode 100644 index 0000000..7ffccdc --- /dev/null +++ b/src/store/searchSelectorAtom.ts @@ -0,0 +1,16 @@ +import { atom } from "recoil"; + +export const searchStateAtom = atom({ + key: "searchState", + default: { + selectedRegion: 99, + selectedType: 99, + selectedOptions: [99], + isRegionSelected: false, + isRegionHovered: false, + isTypeSelected: false, + isTypeHovered: false, + isOptionsSelected: false, + isOptionsHovered: false, + }, +}); diff --git a/src/store/searchSelectorData.ts b/src/store/searchSelectorData.ts new file mode 100644 index 0000000..b479858 --- /dev/null +++ b/src/store/searchSelectorData.ts @@ -0,0 +1,31 @@ +export const regions = [ + { name: "전국", value: 99 }, + { name: "서울시", value: 0 }, + { name: "경기도", value: 1 }, + { name: "강원도", value: 2 }, + { name: "충청도", value: 3 }, + { name: "전라도", value: 4 }, + { name: "경상도", value: 5 }, + { name: "제주도", value: 6 }, +]; + +export const types = [ + { name: "전체 타입", value: 99 }, + { name: "호텔", value: 0 }, + { name: "콘도", value: 1 }, + { name: "호스텔", value: 2 }, + { name: "펜션", value: 3 }, + { name: "모텔", value: 4 }, + { name: "민박", value: 5 }, + { name: "게스트하우스", value: 6 }, + { name: "홈스테이", value: 7 }, + { name: "레지던스", value: 8 }, + { name: "한옥", value: 9 }, +]; + +export const options = [ + { name: "상관 없음", value: 99 }, + { name: "주차 가능", value: 0 }, + { name: "조리 가능", value: 1 }, + { name: "픽업 가능", value: 2 }, +]; diff --git a/src/store/userDataAtom.ts b/src/store/userDataAtom.ts new file mode 100644 index 0000000..4f810f1 --- /dev/null +++ b/src/store/userDataAtom.ts @@ -0,0 +1,19 @@ +import { atom } from "recoil"; +import { recoilPersist } from "recoil-persist"; + +const { persistAtom } = recoilPersist({ + key: "userDataSessionStorage", + storage: sessionStorage, +}); + +export const userDataAtom = atom<{ + nickName: string; + memberId: string; +}>({ + key: "userDataAtom", + default: { + nickName: "", + memberId: "", + }, + effects_UNSTABLE: [persistAtom], +}); diff --git a/src/test/test.utils.tsx b/src/test/test.utils.tsx new file mode 100644 index 0000000..2f92f4a --- /dev/null +++ b/src/test/test.utils.tsx @@ -0,0 +1,91 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { CategoryProps } from "../pages/Category/Category.types"; +import { CategoryItemPageProps } from "../components/Category/CategoryItemWrapper/CategoryItemWrapper.types"; +import { RecoilRoot } from "recoil"; +import { BrowserRouter } from "react-router-dom"; + +export const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + return ({ children }: { children: React.ReactNode }) => ( + + {" "} + + {children} + + + ); +}; + +export const createRecoilWrapper = () => { + return ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); +}; + +export const testData = { + message: "success", + data: [ + { + id: "53096", + name: "비진도 바다이야기 펜션", + address: "경상남도 통영시 한산면 외항길 78", + phoneNumber: "055-642-6171", + type: 4, + wishCount: 644, + thumbnailUrl: + "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", + lowest_price: 488, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 1, + viewCount: 707, + checkIn: "18:00", + checkOut: "12:00", + }, + { + id: "47247", + name: "토요코 인 서면", + address: "부산광역시 부산진구 서전로 39", + phoneNumber: "051-638-1045", + type: 1, + wishCount: 217, + thumbnailUrl: + "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", + lowest_price: 425, + isWish: true, + categoryParking: 1, + categoryCooking: 1, + categoryPickup: 0, + viewCount: 246, + checkIn: "18:00", + checkOut: "12:00", + }, + { + id: "4709", + name: "아모렉스관광호텔", + address: "서울특별시 성동구 왕십리로20길 19", + phoneNumber: "02-2292-7634", + type: 9, + wishCount: 556, + thumbnailUrl: + "https://res.cloudinary.com/dtf6uf7vi/image/upload/v1700183654/Home/testid.jpg", + lowest_price: 260, + isWish: true, + categoryParking: 1, + categoryCooking: 0, + categoryPickup: 0, + viewCount: 865, + checkIn: "18:00", + checkOut: "12:00", + }, + ] as CategoryProps[], +} as CategoryItemPageProps; diff --git a/src/types/index.ts b/src/types/index.ts index 997b863..4e166ba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,10 +1,10 @@ export interface CartItemType { - room_basket_id: number; - accommodation_name: string; - room_image_url: string[]; - room_name: string; + id: number; + accommodationName: string; + roomUrl: string; + roomName: string; price: number; - number_guests: number; - check_in_at: string; - check_out_at: string; + numberOfGuests: number; + checkInAt: string; + checkOutAt: string; } diff --git a/src/utils/filterDecoder.ts b/src/utils/filterDecoder.ts index 2add873..62adec4 100644 --- a/src/utils/filterDecoder.ts +++ b/src/utils/filterDecoder.ts @@ -1,32 +1,32 @@ export type CategoryFilterParams = { region: number; type: number; - category_parking: number; - category_cooking: number; - category_pickup: number; + categoryParking: number; + categoryCooking: number; + categoryPickup: number; }; export const checkCategoryQueryUrl = ({ region, type, - category_parking, - category_cooking, - category_pickup, + categoryParking, + categoryCooking, + categoryPickup, }: CategoryFilterParams) => { const queryObjects = { - regionUrl: region !== 99 && region !== null ? `®ion=${region}` : "", + regionUrl: region !== 99 && region !== null ? `®ion01=${region}` : "", typeUrl: type !== 99 && type !== null ? `&type=${type}` : "", - category_parkingUrl: - category_parking !== 2 && category_parking !== null - ? `&category_parking=${category_parking}` + categoryParkingUrl: + categoryParking !== 2 && categoryParking !== null + ? `&category-parking=${categoryParking}` : "", - category_cookingUrl: - category_cooking !== 2 && category_cooking !== null - ? `&category_cooking=${category_cooking}` + categoryCookingUrl: + categoryCooking !== 2 && categoryCooking !== null + ? `&category-cooking=${categoryCooking}` : "", - category_pickupUrl: - category_pickup !== 2 && category_pickup !== null - ? `&category_pickup=${category_pickup}` + categoryPickupUrl: + categoryPickup !== 2 && categoryPickup !== null + ? `&category-pickup=${categoryPickup}` : "", }; diff --git a/src/utils/filterTextDecoder.ts b/src/utils/filterTextDecoder.ts index 0345156..dff1ccc 100644 --- a/src/utils/filterTextDecoder.ts +++ b/src/utils/filterTextDecoder.ts @@ -2,7 +2,7 @@ import { accommoationTypes, regionTypes, } from "../components/Category/CategoryFilter/CategoryFilter.constants"; -import _ from "lodash"; +import * as _ from "lodash"; export type SearchListBannerProps = { validParams: Param; @@ -11,13 +11,13 @@ export type SearchListBannerProps = { type Param = { region: number; type: number; - category_parking: number; - category_cooking: number; - category_pickup: number; + categoryParking: number; + categoryCooking: number; + categoryPickup: number; }; export const filterTextDecoder = (validParams: Param) => { - const { region, type, category_cooking, category_parking, category_pickup } = + const { region, type, categoryCooking, categoryParking, categoryPickup } = validParams; const regionData = { @@ -33,21 +33,21 @@ export const filterTextDecoder = (validParams: Param) => { }; const cookingData = { - valid: category_cooking !== 2 || category_cooking === null, - label: category_cooking === 1 ? "조리가능" : "조리불가능", - url: `&category_cooking=${category_cooking}`, + valid: categoryCooking !== 2 || categoryCooking === null, + label: categoryCooking === 1 ? "조리가능" : "조리불가능", + url: `&categoryCooking=${categoryCooking}`, }; const parkingData = { - valid: category_parking !== 2 || category_parking === null, - label: category_parking === 1 ? "주차가능" : "주차불가능", - url: `&category_parking=${category_parking}`, + valid: categoryParking !== 2 || categoryParking === null, + label: categoryParking === 1 ? "주차가능" : "주차불가능", + url: `&categoryParking=${categoryParking}`, }; const pickupData = { - valid: category_pickup !== 2 || category_pickup === null, - label: category_pickup === 1 ? "픽업가능" : "픽업불가능", - url: `&category_pickup=${category_pickup}`, + valid: categoryPickup !== 2 || categoryPickup === null, + label: categoryPickup === 1 ? "픽업가능" : "픽업불가능", + url: `&categoryPickup=${categoryPickup}`, }; const returnArray = [ diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 0000000..260febd --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,17 @@ +const formatDate = (date: Date | null): string => { + if (!date) return ""; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + +const formatDateToYYMMDD = (date: Date | null): string => { + if (!date) return ""; + const year = date.getFullYear().toString().slice(2); // 년도의 마지막 두 자리 + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + +export { formatDate, formatDateToYYMMDD }; diff --git a/src/utils/getTokenRefresh.ts b/src/utils/getTokenRefresh.ts new file mode 100644 index 0000000..4dcc411 --- /dev/null +++ b/src/utils/getTokenRefresh.ts @@ -0,0 +1,41 @@ +import { fetchToken } from "../api/Auth"; + +export const getTokenRefresh = async () => { + const accessToken = sessionStorage.getItem("accessToken"); + const expirationTime = sessionStorage.getItem("expirationTime"); + + // 토큰이 있는 경우 + if (accessToken && expirationTime) { + const currentTime = new Date().getTime(); + const isExpired = Number(expirationTime) - currentTime < 0; + + // 토큰이 만료되지 않은 경우 + if (!isExpired) { + return accessToken; + // 토큰이 만료된 경우 + } else { + try { + console.log("토큰이 만료되었습니다. 재발급합니다."); + const refreshToken = sessionStorage.getItem("refreshToken"); + + if (!refreshToken) { + return null; + } + + // 리프레시 토큰이 있는 경우 토큰 갱신 + const newAccessToken = await fetchToken(refreshToken); + + console.log("토큰이 갱신되었습니다."); + + return newAccessToken; + } catch { + return null; + } + } + } + + // 토큰이 없는 경우 + else { + return null; + } +}; diff --git a/src/utils/isLogined.ts b/src/utils/isLogined.ts new file mode 100644 index 0000000..850bf0e --- /dev/null +++ b/src/utils/isLogined.ts @@ -0,0 +1,6 @@ +export const isLogined = () => { + return ( + !!sessionStorage.getItem("accessToken") && + !!sessionStorage.getItem("refreshToken") + ); +}; diff --git a/src/utils/setSessionStorage.ts b/src/utils/setSessionStorage.ts new file mode 100644 index 0000000..e3f111a --- /dev/null +++ b/src/utils/setSessionStorage.ts @@ -0,0 +1,9 @@ +export const setSessionStorage = ( + accessToken: string, + refreshToken: string, +) => { + const expirationTime = new Date().getTime() + 1000 * 270; + sessionStorage.setItem("expirationTime", expirationTime.toString()); + sessionStorage.setItem("accessToken", accessToken); + sessionStorage.setItem("refreshToken", refreshToken); +}; diff --git a/tsconfig.json b/tsconfig.json index 2c0ea73..feb3803 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2020", + "target": "esnext", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", + "lib": ["esnext", "DOM", "DOM.Iterable"], + "module": "esnext", "skipLibCheck": true, /* Bundler mode */ diff --git a/tsconfig.node.json b/tsconfig.node.json index 28a9658..5033438 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,10 +2,12 @@ "compilerOptions": { "composite": true, "skipLibCheck": true, - "module": "ESNext", + "module": "esnext", "moduleResolution": "node", "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "strict": true }, "include": ["vite.config.ts"] } diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..a8082b1 --- /dev/null +++ b/vercel.json @@ -0,0 +1,9 @@ +{ + "rewrites": [ + { + "source": "/api/:path*", + "destination": "http://43.200.54.142:8080/api/v1/:path*" + }, + { "source": "/(.*)", "destination": "/" } + ] +} diff --git a/vite.config.ts b/vite.config.ts index 9cc50ea..5bea18a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,22 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://43.200.54.142:8080/api/v1", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, + build: { + minify: "terser", + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + }, });