diff --git a/index.html b/index.html index 82c2ddf2..6a7807f4 100644 --- a/index.html +++ b/index.html @@ -13,5 +13,5 @@ + src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%VITE_KAKAO_API_KEY%"> diff --git a/package-lock.json b/package-lock.json index 4b259404..8dbc2df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,14 +18,18 @@ "path": "^0.12.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-infinite-scroller": "^1.2.6", "react-kakao-maps-sdk": "^1.1.24", "react-router-dom": "^6.21.1", "recoil": "^0.7.7", - "styled-components": "^6.1.3" + "styled-components": "^6.1.3", + "uuid": "^9.0.1" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-infinite-scroller": "^1.2.5", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", @@ -3382,6 +3386,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-infinite-scroller": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz", + "integrity": "sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -3415,6 +3428,12 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz", @@ -6229,7 +6248,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6799,6 +6817,16 @@ "node": ">= 0.6.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6856,6 +6884,22 @@ "react": "^18.2.0" } }, + "node_modules/react-infinite-scroller": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", + "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-kakao-maps-sdk": { "version": "1.1.24", "resolved": "https://registry.npmjs.org/react-kakao-maps-sdk/-/react-kakao-maps-sdk-1.1.24.tgz", @@ -7784,6 +7828,18 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", @@ -10131,6 +10187,15 @@ "@types/react": "*" } }, + "@types/react-infinite-scroller": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz", + "integrity": "sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -10164,6 +10229,12 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz", @@ -12118,8 +12189,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-hash": { "version": "3.0.0", @@ -12445,6 +12515,16 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12479,6 +12559,19 @@ "scheduler": "^0.23.0" } }, + "react-infinite-scroller": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", + "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", + "requires": { + "prop-types": "^15.5.8" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "react-kakao-maps-sdk": { "version": "1.1.24", "resolved": "https://registry.npmjs.org/react-kakao-maps-sdk/-/react-kakao-maps-sdk-1.1.24.tgz", @@ -13127,6 +13220,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, "vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", diff --git a/package.json b/package.json index 50bdf042..8439277e 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,18 @@ "path": "^0.12.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-infinite-scroller": "^1.2.6", "react-kakao-maps-sdk": "^1.1.24", "react-router-dom": "^6.21.1", "recoil": "^0.7.7", - "styled-components": "^6.1.3" + "styled-components": "^6.1.3", + "uuid": "^9.0.1" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-infinite-scroller": "^1.2.5", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", @@ -39,6 +43,7 @@ "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.5.9", "tailwindcss": "^3.4.0", + "terser": "^5.26.0", "typescript": "^5.2.2", "vite": "^5.0.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c09288d..3163368e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-infinite-scroller: + specifier: ^1.2.6 + version: 1.2.6(react@18.2.0) + react-kakao-maps-sdk: + specifier: ^1.1.24 + version: 1.1.24(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.21.1 version: 6.21.1(react-dom@18.2.0)(react@18.2.0) @@ -44,6 +50,9 @@ dependencies: styled-components: specifier: ^6.1.3 version: 6.1.4(react-dom@18.2.0)(react@18.2.0) + uuid: + specifier: ^9.0.1 + version: 9.0.1 devDependencies: '@types/react': @@ -52,6 +61,12 @@ devDependencies: '@types/react-dom': specifier: ^18.2.17 version: 18.2.18 + '@types/react-infinite-scroller': + specifier: ^1.2.5 + version: 1.2.5 + '@types/uuid': + specifier: ^9.0.7 + version: 9.0.7 '@typescript-eslint/eslint-plugin': specifier: ^6.14.0 version: 6.16.0(@typescript-eslint/parser@6.16.0)(eslint@8.56.0)(typescript@5.3.3) @@ -85,12 +100,15 @@ devDependencies: tailwindcss: specifier: ^3.4.0 version: 3.4.0 + terser: + specifier: ^5.26.0 + version: 5.26.0 typescript: specifier: ^5.2.2 version: 5.3.3 vite: specifier: ^5.0.8 - version: 5.0.10 + version: 5.0.10(terser@5.26.0) packages: @@ -1698,6 +1716,13 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} @@ -2322,6 +2347,12 @@ packages: dependencies: '@types/react': 18.2.45 + /@types/react-infinite-scroller@1.2.5: + resolution: {integrity: sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==} + dependencies: + '@types/react': 18.2.45 + dev: true + /@types/react@18.2.45: resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==} dependencies: @@ -2352,6 +2383,10 @@ packages: '@types/node': 20.10.5 dev: false + /@types/uuid@9.0.7: + resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} + dev: true + /@typescript-eslint/eslint-plugin@6.16.0(@typescript-eslint/parser@6.16.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2499,7 +2534,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.0.10 + vite: 5.0.10(terser@5.26.0) transitivePeerDependencies: - supports-color dev: true @@ -2707,6 +2742,10 @@ packages: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -2831,6 +2870,10 @@ packages: delayed-stream: 1.0.0 dev: false + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -3623,6 +3666,10 @@ packages: engines: {node: '>=6'} hasBin: true + /kakao.maps.d.ts@0.1.39: + resolution: {integrity: sha512-KXENJ8hHYtjb5G+0vf8TXx/PwWW4j5ndDiQTSMvGtF7EFWu2P3N/+Zivcj9/UKn3j29Iz/sIUaA7WL8Ug3IDGQ==} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -3857,7 +3904,6 @@ packages: /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} @@ -4147,6 +4193,14 @@ packages: engines: {node: '>= 0.6.0'} dev: false + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -4170,6 +4224,31 @@ packages: scheduler: 0.23.0 dev: false + /react-infinite-scroller@1.2.6(react@18.2.0): + resolution: {integrity: sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==} + peerDependencies: + react: ^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: false + + /react-kakao-maps-sdk@1.1.24(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-leLbFwBj6zbTdDg6A9U7EwYT2oq0+2F+NHZSVTyCmmvyc4yt2zpRvUmcAt8I6h2SDUdgHbpvKAV1sZoRIxD4JQ==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + dependencies: + '@babel/runtime': 7.23.6 + kakao.maps.d.ts: 0.1.39 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -4429,6 +4508,18 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4584,6 +4675,17 @@ packages: - ts-node dev: true + /terser@5.26.0: + resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.11.2 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -4726,7 +4828,12 @@ packages: inherits: 2.0.3 dev: false - /vite@5.0.10: + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /vite@5.0.10(terser@5.26.0): resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -4757,6 +4864,7 @@ packages: esbuild: 0.19.10 postcss: 8.4.32 rollup: 4.9.1 + terser: 5.26.0 optionalDependencies: fsevents: 2.3.3 dev: true diff --git a/src/@types/tours.types.ts b/src/@types/tours.types.ts new file mode 100644 index 00000000..f3789a88 --- /dev/null +++ b/src/@types/tours.types.ts @@ -0,0 +1,35 @@ +export type TourKeywordInfo = { + keywordId: number; + content: string; + type: string; + keywordCount: number; +}; + +export interface RegionTypes { + areaCode?: number; + subAreaCode?: number; + name: string; +} + +export interface ToursCategoryItemProps extends RegionTypes { + isSelected: boolean; + onSelect: (name: string) => void; +} + +export interface ToursListProps { + selectedRegion: string; +} + +export interface ToursCategoryProps extends ToursListProps { + setSelectedRegion: (region: string) => void; +} + +export interface TourType { + id: number; + title: string; + liked: boolean; + likedCount: number; + ratingAverage: number; + reviewCount: number; + smallThumbnailUrl: string; +} diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355df..00000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index 35a48aa4..bfa01682 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import React, { Suspense } from 'react'; import { ThemeProvider } from 'styled-components'; import { GlobalStyle } from '@styles/globalStyles'; import { theme } from '@styles/theme'; diff --git a/src/api/region.ts b/src/api/region.ts index e64e8515..7b5db167 100644 --- a/src/api/region.ts +++ b/src/api/region.ts @@ -3,13 +3,17 @@ import client from './client'; // 지역 관련 API // 전체 지역 조회 -export const getTours = async (areaCode: number) => { +export const getRegion = async (areaCode: number) => { const res = await client.get(`region?areaCode=${areaCode}`); return res; }; -// 인기 지역 조회 -export const getPopularTours = async () => { - const res = await client.get(`region/popular`); - return res; +// 인기 지역 카테고리 조회 +export const getPopularRegion = async () => { + try { + const res = await client.get(`region/popular`); + return res; + } catch (e) { + console.error(e); + } }; diff --git a/src/api/tours.ts b/src/api/tours.ts index 888e7b7c..a7baee6c 100644 --- a/src/api/tours.ts +++ b/src/api/tours.ts @@ -3,11 +3,21 @@ import client from './client'; // 여행지 관련 API // 인기 여행지 조회 -export const getTours = async (region: number, page: number, size: number) => { - const res = await client.get( - `tours?region=${region}&page=${page}&size=${size}`, - ); - return res; +export const getTours = async ( + region?: string, + page?: number, + size?: number, +) => { + try { + const res = await client.get( + `tours?${ + region !== '전체' && `region=${region}` + }&page=${page}&size=${size}`, + ); + return res; + } catch (e) { + console.error(e); + } }; // 여행지 상세 조회 @@ -25,6 +35,8 @@ export const getDetailTours = async (tourItemId: number) => { // 여행 상품 리뷰 조회 export const getToursReviews = async (tourItemId: number) => { const res = await client.get(`tours/${tourItemId}/reviews`); + console.log('res', res); + console.log('res.data.data.reviewInfos', res.data.data.reviewInfos); return res; }; diff --git a/src/assets/images/Star.svg b/src/assets/images/Star.svg index 833ebb39..5c27ea56 100644 --- a/src/assets/images/Star.svg +++ b/src/assets/images/Star.svg @@ -5,4 +5,4 @@ fill="current" xmlns="http://www.w3.org/2000/svg"> - + \ No newline at end of file diff --git a/src/components/DetailSectionBottom/DetailReview.tsx b/src/components/DetailSectionBottom/DetailReview.tsx new file mode 100644 index 00000000..b68f9377 --- /dev/null +++ b/src/components/DetailSectionBottom/DetailReview.tsx @@ -0,0 +1,85 @@ +// import { getToursReviews } from '@api/tours'; +// import { useEffect, useState } from 'react'; +// import InfiniteScroll from 'react-infinite-scroller'; +// import { useInfiniteQuery } from '@tanstack/react-query'; +// import ReviewItem from './ReviewItem'; +// import { StarIcon } from '@components/common/icons/Icons'; + +// export default function DetailReview() { +// const [reviewDataLength, setReviewDataLength] = useState(0); +// const tourItemId = 1; + +// const { +// data: toursReviews, +// fetchNextPage, +// hasNextPage, +// } = useInfiniteQuery({ +// queryKey: ['toursReviews'], +// queryFn: ({ pageParam }) => getToursReviews(tourItemId), +// initialPageParam: 0, +// getNextPageParam: (lastPage, allPages, lastPageParam) => { +// const lastData = lastPage?.data?.data?.reviewInfos; +// return lastData && lastData.length === 4 ? lastPageParam + 1 : undefined; +// }, +// }); + +// useEffect(() => { +// if (toursReviews) { +// const totalCount = toursReviews.pages.reduce( +// (accumulator, page) => +// accumulator + (page?.data?.data?.reviewInfos?.length || 0), +// 0, +// ); +// setReviewDataLength(totalCount); +// } +// }, [toursReviews]); + +// return ( +// <> +//
+// 리뷰 {reviewDataLength} +//
+// {reviewDataLength > 0 && ( +// fetchNextPage()} +// initialLoad={false}> +// {toursReviews?.pages?.map((page, pageIndex) => ( +//
+// {page?.data?.data?.reviewInfos?.map( +// (item: any, index: number) => ( +// +// ), +// )} +//
+// ))} +//
+// )} +// {reviewDataLength == 0 && ( +//
+//
+// {Array.from({ length: 5 }, (_, index) => ( +// +// ))} +//
+//
첫번째 리뷰를 남겨주세요!
+//
+// )} +// +// ); +// } diff --git a/src/components/DetailSectionBottom/DetailReviewStats.tsx b/src/components/DetailSectionBottom/DetailReviewStats.tsx new file mode 100644 index 00000000..61a80623 --- /dev/null +++ b/src/components/DetailSectionBottom/DetailReviewStats.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { useGetToursReviews } from '@hooks/useReviewStats'; +import { v4 as uuidv4 } from 'uuid'; +import { DownIcon } from '@components/common/icons/Icons'; +import useReviewStatsCalculator from '@hooks/useReviewStatsCalculator'; +import { getEmoji } from '@utils/utils'; + +const DetailReviewStats = () => { + const { reviewStats } = useGetToursReviews(); + const { calculateWidth, getColor } = useReviewStatsCalculator(reviewStats); + const [showAll, setShowAll] = useState(false); + + return ( + <> +
이런 점이 좋았어요
+ {reviewStats && reviewStats.length > 0 ? ( + reviewStats.slice(0, showAll ? reviewStats.length : 3).map((data) => ( +
+
+
+
+
+

{getEmoji(data.content)}

+

+ {data.content} +

+
+

{data.keywordCount}

+
+
+
+ )) + ) : ( +
+

+ 첫번째 리뷰를 남겨주세요! +

+
+ )} + {reviewStats && reviewStats.length > 3 && ( +
+
setShowAll(!showAll)} + className="cursor-pointer transition-transform duration-300" + style={{ + transform: showAll ? 'rotate(180deg)' : 'rotate(0deg)', + }}> + +
+
+ )} + + ); +}; + +export default DetailReviewStats; diff --git a/src/components/DetailSectionBottom/DetailSectionBottom.tsx b/src/components/DetailSectionBottom/DetailSectionBottom.tsx new file mode 100644 index 00000000..9455ba17 --- /dev/null +++ b/src/components/DetailSectionBottom/DetailSectionBottom.tsx @@ -0,0 +1,11 @@ +import { DetailReviewStats } from '.'; + +// 담당 컴포넌트들 호출하는 컴포넌트(분업 때문에 페이지 느낌으로 나눠봤습니다), API 호출 컴포넌트, +export default function DetailSectionBottom() { + return ( + <> + + {/* */} + + ); +} diff --git a/src/components/DetailSectionBottom/ReviewItem.tsx b/src/components/DetailSectionBottom/ReviewItem.tsx new file mode 100644 index 00000000..ae09f613 --- /dev/null +++ b/src/components/DetailSectionBottom/ReviewItem.tsx @@ -0,0 +1,96 @@ +import { useEffect } from 'react'; +import { StarIcon, ChatIcon } from '@components/common/icons/Icons'; + +interface Keyword { + keywordId: number; + content: string; + type: string; +} + +interface ItemProps { + authorNickname: string; + authorProfileImageUrl: string; + rating: number; + createdTime: any; + content: string; + keywords: Keyword[]; // keywordId, content, type + commentCount: number; +} + +const Item: React.FC = (props: ItemProps) => { + const { + authorNickname, + // authorProfileImageUrl, + rating, + createdTime, + content, + keywords, + commentCount, + } = props; + + const formatCreatedTime = (timeString: string): string => { + const date = new Date(timeString); + const formattedDate = new Intl.DateTimeFormat('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(date); + + return formattedDate; + }; + useEffect(() => { + console.log('commentCount', commentCount); + }, []); + return ( +
+
+ {/* {authorProfileImageUrl} */} +
+ 유저 프로필 +
+
+
{authorNickname}
+
+ {Array.from({ length: 5 }, (_, index) => ( + + ))} +
+
+
+ {formatCreatedTime(createdTime)} +
+
+
{content}
+
+
+ {keywords.map((keyword, idx) => { + return ( +
+ {keyword.content} +
+ ); + })} +
+
+ +
{commentCount}
+
+
+
+ ); +}; + +export default Item; diff --git a/src/components/DetailSectionBottom/index.tsx b/src/components/DetailSectionBottom/index.tsx new file mode 100644 index 00000000..d1ca1b41 --- /dev/null +++ b/src/components/DetailSectionBottom/index.tsx @@ -0,0 +1,3 @@ +import DetailReviewStats from './DetailReviewStats'; + +export { DetailReviewStats }; diff --git a/src/components/DetailSectionTop/DetailSectionTop.tsx b/src/components/DetailSectionTop/DetailSectionTop.tsx index a663f268..34eb2cbf 100644 --- a/src/components/DetailSectionTop/DetailSectionTop.tsx +++ b/src/components/DetailSectionTop/DetailSectionTop.tsx @@ -2,34 +2,37 @@ import { useParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { getDetailTours } from '@api/tours'; +import { getDetailTours, getToursReviews } from '@api/tours'; import { DetailToursInfo, DetailToursRating, DetailToursMap, - DetailTourButtons, + DetailToursButtons, } from '.'; export default function DetailSectionTop() { const params = useParams(); const tourId = Number(params.id); - const { isError, isLoading, isFetching, data } = useQuery({ + const detailQuery = useQuery({ queryKey: ['details', tourId], queryFn: () => getDetailTours(tourId), }); - if (data) { - return ( - <> - - - - - - ); - } + const reviewQuery = useQuery({ + queryKey: ['reviews', tourId], + queryFn: () => getToursReviews(tourId), + }); + + if (detailQuery.error || reviewQuery.error) console.log('error - 예외 처리'); - if (isError) console.log('error'); + return detailQuery.data && reviewQuery.data?.data.data ? ( +
+ + + + +
+ ) : null; } diff --git a/src/components/DetailSectionTop/DetailTourButtons.tsx b/src/components/DetailSectionTop/DetailToursButtons.tsx similarity index 62% rename from src/components/DetailSectionTop/DetailTourButtons.tsx rename to src/components/DetailSectionTop/DetailToursButtons.tsx index 7038e566..05e79202 100644 --- a/src/components/DetailSectionTop/DetailTourButtons.tsx +++ b/src/components/DetailSectionTop/DetailToursButtons.tsx @@ -3,12 +3,12 @@ import { ReactComponent as CalendarIcon } from '@assets/images/Calendar.svg'; export default function DetailTourButtons() { return ( -
- - diff --git a/src/components/DetailSectionTop/DetailToursInfo.tsx b/src/components/DetailSectionTop/DetailToursInfo.tsx index 46021d11..4c906c1a 100644 --- a/src/components/DetailSectionTop/DetailToursInfo.tsx +++ b/src/components/DetailSectionTop/DetailToursInfo.tsx @@ -1,10 +1,10 @@ import { ReactComponent as HeartIcon } from '@assets/images/Heart.svg'; -interface InfoProps { +interface DetailToursInfoProps { infoData: tourDetail; } -export default function DetailToursInfo({ infoData }: InfoProps) { +export default function DetailToursInfo({ infoData }: DetailToursInfoProps) { const { title, liked, originalThumbnailUrl } = infoData; return ( diff --git a/src/components/DetailSectionTop/DetailToursMap.tsx b/src/components/DetailSectionTop/DetailToursMap.tsx new file mode 100644 index 00000000..90719757 --- /dev/null +++ b/src/components/DetailSectionTop/DetailToursMap.tsx @@ -0,0 +1,69 @@ +import { ReactComponent as MapIcon } from '@assets/images/Map.svg'; +import { ReactComponent as CheckIcon } from '@assets/images/Check.svg'; +import { ReactComponent as PhoneIcon } from '@assets/images/Phone.svg'; + +import { Map, MapMarker } from 'react-kakao-maps-sdk'; +import { useState } from 'react'; + +interface DetailToursMapProps { + mapData: tourDetail; +} + +export default function DetailToursMap({ mapData }: DetailToursMapProps) { + const { fullAddress, longitude, latitude, tel } = mapData; + const [isMapVisible, setIsMapVisible] = useState(false); + + const MapStyle = { + width: '100%', + height: isMapVisible ? 0 : '180px', + marginTop: isMapVisible ? '15px' : '15px', + marginBottom: isMapVisible ? '15px' : '15px', + transition: 'height 0.3s ease-in-out', + }; + + const closeMap = () => { + setIsMapVisible((prev) => !prev); + }; + + return ( +
+
+
+ + {fullAddress} +
+ +
+
+ + + +
+
+ + {tel} +
+
+ ); +} diff --git a/src/components/DetailSectionTop/DetailToursRating.tsx b/src/components/DetailSectionTop/DetailToursRating.tsx index b1fdf0bf..db9a9f5b 100644 --- a/src/components/DetailSectionTop/DetailToursRating.tsx +++ b/src/components/DetailSectionTop/DetailToursRating.tsx @@ -1,15 +1,72 @@ -import { ReactComponent as StarIcon } from '@assets/images/Star.svg'; +import { useEffect, useState } from 'react'; + +interface ReviewData { + ratingAverage: number; + reviewTotalCount: number; +} + +interface DetailToursRatingProps { + reviewData: ReviewData; +} + +export default function DetailToursRating({ + reviewData, +}: DetailToursRatingProps) { + const { reviewTotalCount, ratingAverage } = reviewData; + + const STAR_IDX_ARR = ['1', '2', '3', '4', '5']; + const [ratedStarArr, setRatedStarArr] = useState([0, 0, 0, 0, 0]); + + const calculateRates = (average: number) => { + let tempRatedStarArr = [0, 0, 0, 0, 0]; + let exchangedStarScore = (average * 80) / 5; + let idx = 0; + while (exchangedStarScore > 16) { + tempRatedStarArr[idx] = 16; + idx += 1; + exchangedStarScore -= 16; + } + tempRatedStarArr[idx] = Number(exchangedStarScore.toFixed(1)); + return tempRatedStarArr; + }; + + useEffect(() => { + setRatedStarArr(calculateRates(ratingAverage)); + }, []); -export default function DetailToursRating() { return ( -
- - - - - - -

(4)

+
+ {STAR_IDX_ARR.map((item, idx) => { + return ( + + + + + + + + + + ); + })} + +
+

+ {reviewTotalCount} +

+
); } diff --git a/src/components/DetailSectionTop/DetailToutsMap.tsx b/src/components/DetailSectionTop/DetailToutsMap.tsx deleted file mode 100644 index fabac7e6..00000000 --- a/src/components/DetailSectionTop/DetailToutsMap.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { ReactComponent as MapIcon } from '@assets/images/Map.svg'; -import { ReactComponent as CheckIcon } from '@assets/images/Check.svg'; -import { ReactComponent as PhoneIcon } from '@assets/images/Phone.svg'; - -import { Map, MapMarker } from 'react-kakao-maps-sdk'; - -interface MapProps { - mapData: tourDetail; -} - -export default function DetailToursMap({ mapData }: MapProps) { - const { fullAddress, longitude, latitude, tel } = mapData; - - return ( -
-
-
- - {fullAddress} -
- -
- - - -
- - {tel} -
-
- ); -} diff --git a/src/components/DetailSectionTop/index.tsx b/src/components/DetailSectionTop/index.tsx index cc88b9e9..150b6ffd 100644 --- a/src/components/DetailSectionTop/index.tsx +++ b/src/components/DetailSectionTop/index.tsx @@ -1,11 +1,12 @@ import DetailToursInfo from './DetailToursInfo'; import DetailToursRating from './DetailToursRating'; -import DetailToursMap from './DetailToutsMap'; -import DetailTourButtons from './DetailTourButtons'; +import DetailToursMap from './DetailToursMap'; +import DetailToursButtons from './DetailToursButtons'; + export { DetailToursInfo, DetailToursRating, DetailToursMap, - DetailTourButtons, + DetailToursButtons, }; diff --git a/src/components/Review/Review.tsx b/src/components/Review/Review.tsx new file mode 100644 index 00000000..6114bb50 --- /dev/null +++ b/src/components/Review/Review.tsx @@ -0,0 +1,15 @@ +import ReviewButton from './ReviewButton'; +import ReviewKeyword from './ReviewKeyword'; +import ReviewPosting from './ReviewPosting'; +import ReviewRating from './ReviewRating'; + +export default function Review() { + return ( + <> + + + + + + ); +} diff --git a/src/components/Review/ReviewButton.tsx b/src/components/Review/ReviewButton.tsx new file mode 100644 index 00000000..eaf9c5a4 --- /dev/null +++ b/src/components/Review/ReviewButton.tsx @@ -0,0 +1,5 @@ +import { ButtonPrimary } from '@components/common/button/Button'; + +export default function ReviewButton() { + return {}}>완료; +} diff --git a/src/components/Review/ReviewKeyword.tsx b/src/components/Review/ReviewKeyword.tsx new file mode 100644 index 00000000..79bb0f5e --- /dev/null +++ b/src/components/Review/ReviewKeyword.tsx @@ -0,0 +1,47 @@ +export default function ReviewKeyword() { + return ( +
+
어떤 점이 좋았나요?
+
+
+
+ 깨끗해요 +
+
+ 친절해요 +
+
+ 뷰가 좋아요 +
+
+
+
+ 침구가 좋아요 +
+
+ 주차하기 편해요 +
+
+ 냉난방이 잘돼요 +
+
+
+
+ 대중교통이 편해요 +
+
+ 호캉스하기 좋아요 +
+
+
+
+ 조식이 맛있어요 +
+
+ 사진 찍기 좋아요 +
+
+
+
+ ); +} diff --git a/src/components/Review/ReviewPosting.tsx b/src/components/Review/ReviewPosting.tsx new file mode 100644 index 00000000..e0a37d4a --- /dev/null +++ b/src/components/Review/ReviewPosting.tsx @@ -0,0 +1,26 @@ +import { useState, ChangeEvent } from 'react'; + +export default function ReviewPosting() { + const [textLength, setTextLength] = useState(0); + + const handleTextChange = (event: ChangeEvent) => { + const inputText = event.target.value; + setTextLength(inputText.length); + }; + + return ( +
+
리뷰를 작성해주세요
+
+