diff --git a/package-lock.json b/package-lock.json index f0c1998f..86670dd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,13 @@ "dotenv": "^16.3.1", "firebase": "^9.23.0", "framer-motion": "^10.16.16", + "immutability-helper": "^3.1.1", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-cookie": "^7.0.1", "react-datepicker": "^4.25.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", @@ -28,6 +31,7 @@ "react-mobile-datepicker": "^4.0.2", "react-router-dom": "^6.21.1", "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "swiper": "^11.0.5" }, "devDependencies": { @@ -3486,10 +3490,82 @@ "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/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, + "node_modules/@remix-run/router": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz", + "integrity": "sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz", + "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz", + "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -4294,6 +4370,7 @@ "version": "20.10.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz", "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==", + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -5923,6 +6000,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6511,8 +6598,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7263,6 +7349,11 @@ "node": ">= 4" } }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, "node_modules/immutable": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", @@ -10184,6 +10275,43 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -10443,6 +10571,14 @@ } } }, + "node_modules/recoil-persist": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/recoil-persist/-/recoil-persist-5.1.0.tgz", + "integrity": "sha512-sew4k3uBVJjRWKCSFuBw07Y1p1pBOb0UxLJPxn4G2bX/9xNj+r2xlqYy/BRfyofR/ANfqBU04MIvulppU4ZC0w==", + "peerDependencies": { + "recoil": "^0.7.2" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10456,6 +10592,14 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -11379,7 +11523,8 @@ "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==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true }, "node_modules/universal-cookie": { "version": "7.0.1", diff --git a/package.json b/package.json index 1edc8bdc..95e080ba 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,13 @@ "dotenv": "^16.3.1", "firebase": "^9.23.0", "framer-motion": "^10.16.16", + "immutability-helper": "^3.1.1", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-cookie": "^7.0.1", "react-datepicker": "^4.25.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", @@ -30,6 +33,7 @@ "react-mobile-datepicker": "^4.0.2", "react-router-dom": "^6.21.1", "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", "swiper": "^11.0.5" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index a7d40ad5..0fa8770b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {Suspense} from 'react'; import {CookiesProvider} from 'react-cookie'; +import {DndProvider} from 'react-dnd'; +import {HTML5Backend} from 'react-dnd-html5-backend'; import {BrowserRouter} from 'react-router-dom'; import './firebase/messaging-init-in-sw'; @@ -15,9 +17,11 @@ function App() { - - - + + + + + diff --git a/src/api/vote.ts b/src/api/vote.ts index 71091d28..6583339a 100644 --- a/src/api/vote.ts +++ b/src/api/vote.ts @@ -19,7 +19,7 @@ export const getVoteInfo = async (voteId: number): Promise => { //보트 리스트 export const getVoteListInfo = async (spaceId: number): Promise => { - const response = await axios.get(`/api/votes/${spaceId}`); + const response = await axios.get(`/api/votes`, {params: {spaceId, voteStatusOption: 'ALL'}}); return response.data; }; @@ -28,7 +28,7 @@ export const getVoteListInfo = async (spaceId: number): Promise /* ----------------------------------- P O S T ---------------------------------- */ //vote 추가 -export const PostNewVote = async ({spaceId, title}: PostVoteTitleProps) => { +export const postNewVote = async ({spaceId, title}: PostVoteTitleProps) => { try { const response = await axios.post('/api/votes', {spaceId, title}); console.log('axios 포스트 성공', response); diff --git a/src/assets/homeIcons/search/nullImg.svg b/src/assets/homeIcons/search/nullImg.svg new file mode 100644 index 00000000..ee839bf9 --- /dev/null +++ b/src/assets/homeIcons/search/nullImg.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crying_imoji.svg b/src/assets/icons/crying_imoji.svg new file mode 100644 index 00000000..1f81bbca --- /dev/null +++ b/src/assets/icons/crying_imoji.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/error-warning-line.svg b/src/assets/icons/error-warning-line.svg new file mode 100644 index 00000000..05fe8515 --- /dev/null +++ b/src/assets/icons/error-warning-line.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/mapPin.svg b/src/assets/icons/mapPin.svg new file mode 100644 index 00000000..9da4371c --- /dev/null +++ b/src/assets/icons/mapPin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/meatball.svg b/src/assets/icons/meatball.svg new file mode 100644 index 00000000..2a6e7da7 --- /dev/null +++ b/src/assets/icons/meatball.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/pencil.svg b/src/assets/icons/pencil.svg new file mode 100644 index 00000000..197ed04b --- /dev/null +++ b/src/assets/icons/pencil.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/pencil_with_line.svg b/src/assets/icons/pencil_with_line.svg new file mode 100644 index 00000000..d31ae5be --- /dev/null +++ b/src/assets/icons/pencil_with_line.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/star.svg b/src/assets/icons/star.svg new file mode 100644 index 00000000..869d1899 --- /dev/null +++ b/src/assets/icons/star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/star_fill.svg b/src/assets/icons/star_fill.svg new file mode 100644 index 00000000..e92e6779 --- /dev/null +++ b/src/assets/icons/star_fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/tooltipFrame.svg b/src/assets/icons/tooltipFrame.svg new file mode 100644 index 00000000..81599275 --- /dev/null +++ b/src/assets/icons/tooltipFrame.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/trash_icon.svg b/src/assets/icons/trash_icon.svg new file mode 100644 index 00000000..3a70928f --- /dev/null +++ b/src/assets/icons/trash_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Auth/Input/Input.module.scss b/src/components/Auth/Input/Input.module.scss index e3ce9bf3..2ebe0eea 100644 --- a/src/components/Auth/Input/Input.module.scss +++ b/src/components/Auth/Input/Input.module.scss @@ -154,4 +154,8 @@ margin-top: 2px; margin-bottom: 12px; } + + .removeBtn { + top: 81px; + } } diff --git a/src/components/Auth/Input/InputOldPassword.tsx b/src/components/Auth/Input/InputOldPassword.tsx new file mode 100644 index 00000000..c38680fe --- /dev/null +++ b/src/components/Auth/Input/InputOldPassword.tsx @@ -0,0 +1,39 @@ +import styles from "./Input.module.scss"; + +import validationForm from "@/utils/inputValidation"; + +import { InputOldPasswordProps } from "@/types/auth"; + +function InputOldPassword({ + register, + dirtyFields, + errors, +}: InputOldPasswordProps) { + return ( +
+ + + + + {!dirtyFields?.oldPassword || errors?.oldPassword ? ( + {errors?.oldPassword?.message} + ) : null} +
+ ); +} + +export default InputOldPassword; diff --git a/src/components/Auth/Login/LoginForm.tsx b/src/components/Auth/Login/LoginForm.tsx index 9bce2cd4..f601a2f0 100644 --- a/src/components/Auth/Login/LoginForm.tsx +++ b/src/components/Auth/Login/LoginForm.tsx @@ -49,10 +49,14 @@ function LoginForm() { if (showError(email as string, password as string)) return; try { - const res = await axios.post("/api/login", { - email, - password, - }); + const res = await axios.post( + "/api/login", + { + email, + password, + }, + { withCredentials: true }, + ); console.log(res.data); navigate("/", { replace: true }); diff --git a/src/components/Auth/ModifyPassword/ModifyPasswordForm.module.scss b/src/components/Auth/ModifyPassword/ModifyPasswordForm.module.scss new file mode 100644 index 00000000..61891890 --- /dev/null +++ b/src/components/Auth/ModifyPassword/ModifyPasswordForm.module.scss @@ -0,0 +1,10 @@ +@use "@/sass" as *; + +.container { + position: relative; + color: $neutral900; + + h2 { + @include typography(headline); + } +} diff --git a/src/components/Auth/ModifyPassword/ModifyPasswordForm.tsx b/src/components/Auth/ModifyPassword/ModifyPasswordForm.tsx new file mode 100644 index 00000000..ee39b1a1 --- /dev/null +++ b/src/components/Auth/ModifyPassword/ModifyPasswordForm.tsx @@ -0,0 +1,71 @@ +import axios from "axios"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; + +import styles from "./ModifyPasswordForm.module.scss"; + +import StepNewPassword from "./Step/StepNewPassword"; +import StepOldPassword from "./Step/StepOldPassword"; + +import { AuthForm } from "@/types/auth"; + +function ModifyPasswordForm() { + const { + register, + resetField, + handleSubmit, + watch, + formState: { errors, dirtyFields }, + } = useForm({ + mode: "onChange", + defaultValues: { + oldPassword: "", + password: "", + passwordConfirm: "", + }, + }); + const watchFields = watch(); + + // steps : oldPassword, newPassword + const [step, setStep] = useState("oldPassword"); + const [token, setToken] = useState(""); + + const onSubmit = async () => { + try { + const res = axios.post("/api/auth/modify/password", { + token, + newPassword: watchFields.password, + }); + + console.log(res); + } catch (error) { + console.log(error); + } + }; + + return ( +
+ {step === "oldPassword" && ( + + )} + + {step === "newPassword" && ( + + )} + + ); +} + +export default ModifyPasswordForm; diff --git a/src/components/Auth/ModifyPassword/Step/Step.module.scss b/src/components/Auth/ModifyPassword/Step/Step.module.scss new file mode 100644 index 00000000..7b195097 --- /dev/null +++ b/src/components/Auth/ModifyPassword/Step/Step.module.scss @@ -0,0 +1,41 @@ +@use "@/sass" as *; + +.container { + position: relative; + + label { + display: block; + + @include typography(tabLabel); + color: $neutral900; + } + + small { + width: 100%; + position: absolute; + bottom: -4px; + left: 0; + + @include typography(captionSmall); + color: $danger300; + + &.step__desc { + position: relative; + margin-top: 8px; + color: $neutral400; + } + } + + .toFindPassword { + display: flex; + justify-content: center; + margin-top: 16px; + + @include typography(captionSmall); + color: $neutral400; + + &:hover { + font-weight: 600; + } + } +} diff --git a/src/components/Auth/ModifyPassword/Step/StepNewPassword.tsx b/src/components/Auth/ModifyPassword/Step/StepNewPassword.tsx new file mode 100644 index 00000000..99bc89dc --- /dev/null +++ b/src/components/Auth/ModifyPassword/Step/StepNewPassword.tsx @@ -0,0 +1,51 @@ +import styles from "./Step.module.scss"; + +import AuthButton from "../../Button/AuthButton"; +import InputPassword from "../../Input/InputPassword"; +import InputPasswordConfirm from "../../Input/InputPasswordConfirm"; + +import { StepPasswordProps } from "@/types/auth"; + +function StepNewPassword({ + register, + resetField, + watchFields: { password, passwordConfirm }, + dirtyFields, + errors, +}: StepPasswordProps) { + return ( +
+

새로운 비밀번호를 설정해주세요

+ + + + + + +
+ ); +} + +export default StepNewPassword; diff --git a/src/components/Auth/ModifyPassword/Step/StepOldPassword.tsx b/src/components/Auth/ModifyPassword/Step/StepOldPassword.tsx new file mode 100644 index 00000000..642b7315 --- /dev/null +++ b/src/components/Auth/ModifyPassword/Step/StepOldPassword.tsx @@ -0,0 +1,59 @@ +import axios from "axios"; +import { Link } from "react-router-dom"; + +import styles from "./Step.module.scss"; + +import AuthButton from "../../Button/AuthButton"; +import InputOldPassword from "../../Input/InputOldPassword"; + +import { StepOldPasswordProps } from "@/types/auth"; + +function StepOldPassword({ + register, + dirtyFields, + errors, + setStep, + setToken, +}: StepOldPasswordProps) { + const onClickNext = async () => { + try { + const res = await axios.post("/api/auth/modify/password/check"); + + setToken!(await res.data.data.token); + setStep!("newPassword"); + } catch (error) { + console.log(error); + } + }; + + return ( +
+

비밀번호 확인

+ + + 비밀번호 재설정을 위해 현재 비밀번호를 입력해주세요. + + + + + + +
+ 비밀번호를 잊으셨나요? +
+
+ ); +} + +export default StepOldPassword; diff --git a/src/components/Auth/Signup/AgreeForm.tsx b/src/components/Auth/Signup/AgreeForm.tsx index 300ea930..e0cad767 100644 --- a/src/components/Auth/Signup/AgreeForm.tsx +++ b/src/components/Auth/Signup/AgreeForm.tsx @@ -84,7 +84,7 @@ function AgreeForm({ setSignupStep }: AgreeProps) { required: true, })} /> - + @@ -97,7 +97,7 @@ function AgreeForm({ setSignupStep }: AgreeProps) { required: true, })} /> - + diff --git a/src/components/Auth/Signup/SignupForm.tsx b/src/components/Auth/Signup/SignupForm.tsx index 2f61f998..3daf72a3 100644 --- a/src/components/Auth/Signup/SignupForm.tsx +++ b/src/components/Auth/Signup/SignupForm.tsx @@ -1,4 +1,5 @@ import axios from "axios"; +import { useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; @@ -30,21 +31,22 @@ function SignupForm({ signupStep, setSignupStep }: SignupFormProps) { }, }); + const [code, setCode] = useState(""); const navigate = useNavigate(); const watchFields = watch(); const onSubmit: SubmitHandler = async (data) => { + console.log(code); if (Object.keys(dirtyFields).length < 5) return; console.log(data); try { - const { email, emailSert, password, image, nickname } = data; + const { email, password, nickname } = data; const res = await axios.post("/api/auth/register", { email, password, nickname, - profile: image, - token: emailSert, + token: code, }); console.log(res); @@ -89,6 +91,7 @@ function SignupForm({ signupStep, setSignupStep }: SignupFormProps) { watchFields={watchFields} dirty={dirtyFields.emailSert} error={errors.emailSert} + setCode={setCode} /> )} diff --git a/src/components/Auth/Signup/Step/StepEmailSert.tsx b/src/components/Auth/Signup/Step/StepEmailSert.tsx index 1b752070..76218bab 100644 --- a/src/components/Auth/Signup/Step/StepEmailSert.tsx +++ b/src/components/Auth/Signup/Step/StepEmailSert.tsx @@ -15,6 +15,7 @@ function StepEmailSert({ watchFields: { email, emailSert }, dirty, error, + setCode, }: StepEmailSertProps) { const [due, setDue] = useState(1800); const showToast = CustomToast(); @@ -35,7 +36,7 @@ function StepEmailSert({ try { const res = await axios.post("/api/auth/register/check-token", { email, - token: emailSert, + code: emailSert, }); console.log(res); @@ -44,6 +45,8 @@ function StepEmailSert({ return; } + console.log(await res.data.data.token); + setCode!(await res.data.data.token); setSignupStep!("password"); } catch (error) { console.log(error); diff --git a/src/components/BottomSlide/BottomSlide.module.scss b/src/components/BottomSlide/BottomSlide.module.scss index 5e3196b2..931c6d46 100644 --- a/src/components/BottomSlide/BottomSlide.module.scss +++ b/src/components/BottomSlide/BottomSlide.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .slideContainer { position: fixed; @@ -25,6 +25,11 @@ display: flex; justify-content: flex-end; } + + .leftCloseButtonContainer { + display: flex; + justify-content: flex-start; + } } } } diff --git a/src/components/BottomSlide/BottomSlide.tsx b/src/components/BottomSlide/BottomSlide.tsx index 5abad792..02566aac 100644 --- a/src/components/BottomSlide/BottomSlide.tsx +++ b/src/components/BottomSlide/BottomSlide.tsx @@ -1,33 +1,33 @@ -import { Slide } from "@chakra-ui/react"; -import { useRef } from "react"; +import {Slide} from '@chakra-ui/react'; +import {useRef} from 'react'; -import styles from "./BottomSlide.module.scss"; +import styles from './BottomSlide.module.scss'; -import useOnClickOutside from "@/hooks/useOnClickOutside"; +import useOnClickOutside from '@/hooks/useOnClickOutside'; -import CloseIcon from "@/assets/close.svg?react"; +import CloseIcon from '@/assets/close.svg?react'; -import { BottomSlideProps } from "../../types/bottomSlide"; +import {BottomSlideProps} from '../../types/bottomSlide'; -function BottomSlide({ isOpen, onClose, children }: BottomSlideProps) { +function BottomSlide({isOpen, onClose, children}: BottomSlideProps) { const containerStyle = { - display: isOpen ? "block" : "none", + display: isOpen ? 'block' : 'none', + }; + + const closeModal = () => { + onClose(); + document.body.style.overflow = 'visible'; }; const slideRef = useRef(null); - useOnClickOutside(slideRef, onClose); + useOnClickOutside(slideRef, closeModal); return (
- +
-
diff --git a/src/components/BottomSlide/BottomSlideLeft.tsx b/src/components/BottomSlide/BottomSlideLeft.tsx new file mode 100644 index 00000000..a40f9352 --- /dev/null +++ b/src/components/BottomSlide/BottomSlideLeft.tsx @@ -0,0 +1,36 @@ +import {Slide} from '@chakra-ui/react'; +import {useRef} from 'react'; + +import styles from './BottomSlide.module.scss'; + +import useOnClickOutside from '@/hooks/useOnClickOutside'; + +import CloseIcon from '@/assets/close.svg?react'; + +import {BottomSlideProps} from '../../types/bottomSlide'; + +function BottomSlideLeft({isOpen, onClose, children}: BottomSlideProps) { + const containerStyle = { + display: isOpen ? 'block' : 'none', + }; + + const slideRef = useRef(null); + useOnClickOutside(slideRef, onClose); + + return ( +
+ +
+
+ +
+
{children}
+
+
+
+ ); +} + +export default BottomSlideLeft; diff --git a/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx b/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx index fd7d18a0..061421b6 100644 --- a/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx +++ b/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx @@ -1,20 +1,34 @@ import {useState} from 'react'; +import {useRecoilValue} from 'recoil'; import styles from './SelectButton.module.scss'; -import useGetSelectedCandidates from '@/hooks/useGetSelectedCandidates'; +import useGetSelectedArray from '@/hooks/useGetSelectedArray'; + +import {selectedPlaceState} from '@/recoil/vote/selectPlace'; + +// //"선택된 장소 객체" +const placeInfo = { + placeId: 23, + placeName: '안녕호텔', + category: '호텔', + location: '서울', + placeImageURL: 'https://img-cf.kurly.com/shop/data/goodsview/20210218/gv30000159355_1.jpg', + latlng: {lat: 33.450936, lng: 126.569477}, +}; + const SelectButton = () => { const [isClicked, setIsClicked] = useState(false); - const {addCandidateInSelectedList} = useGetSelectedCandidates(); + const selectedPlaces = useRecoilValue(selectedPlaceState); - //"선택된 장소의 ID" - const placeId = 22; + const {toggleItemInNewArray} = useGetSelectedArray(selectedPlaceState); const handleClick = () => { setIsClicked((prev) => !prev); - addCandidateInSelectedList(placeId); + toggleItemInNewArray(placeInfo); }; + console.log('선택한 배열', selectedPlaces); return ( +
+ ))} +
+ + ); +} + +export default DayMove; diff --git a/src/components/Route/DayNavigationBar/DayNavigationBar.module.scss b/src/components/Route/DayNavigationBar/DayNavigationBar.module.scss index 6bf93015..2a532732 100644 --- a/src/components/Route/DayNavigationBar/DayNavigationBar.module.scss +++ b/src/components/Route/DayNavigationBar/DayNavigationBar.module.scss @@ -1,13 +1,15 @@ @use '@/sass' as *; .container { + position: sticky; + top: 25.5rem; + nav { height: 7rem; display: flex; justify-content: space-between; padding: 16px 20px; - position: sticky; - top: 25.5rem; + background-color: $neutral0; margin-bottom: 2rem; @@ -40,9 +42,9 @@ // FIXME: 그라데이션 수정 background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0.2%, #fff 22.5%); - .wholeEditButton { + .wholeEditButton, + .wholeCompleteButton { @include typography(button); - color: $neutral400; display: flex; justify-content: center; align-items: center; @@ -50,6 +52,14 @@ gap: 0.8rem; white-space: nowrap; } + + .wholeEditButton { + color: $neutral400; + } + + .wholeCompleteButton { + color: $primary300; + } } } } diff --git a/src/components/Route/DayNavigationBar/DayNavigationBar.tsx b/src/components/Route/DayNavigationBar/DayNavigationBar.tsx index 3b07b854..aadaf5dd 100644 --- a/src/components/Route/DayNavigationBar/DayNavigationBar.tsx +++ b/src/components/Route/DayNavigationBar/DayNavigationBar.tsx @@ -4,7 +4,7 @@ import styles from './DayNavigationBar.module.scss'; import {DayNavigationBarProps} from '@/types/route'; -function DayNavigationBar({dateList}: DayNavigationBarProps) { +function DayNavigationBar({dateList, editMode, handleEditMode}: DayNavigationBarProps) { const [selectedDay, setSelectedDay] = useState(1); // TODO: 클릭 시 해당 day로 스크롤 이동 @@ -27,7 +27,9 @@ function DayNavigationBar({dateList}: DayNavigationBarProps) { ))}
- +
diff --git a/src/components/Route/DayRoute/DayRoute.tsx b/src/components/Route/DayRoute/DayRoute.tsx index e520fac0..e5fb8358 100644 --- a/src/components/Route/DayRoute/DayRoute.tsx +++ b/src/components/Route/DayRoute/DayRoute.tsx @@ -1,18 +1,55 @@ -import { useDisclosure } from "@chakra-ui/react"; -import { AiOutlinePlus as PlusIcon } from "react-icons/ai"; +import {useDisclosure} from '@chakra-ui/react'; +import update from 'immutability-helper'; +import {useCallback, useEffect, useState} from 'react'; +import {useDrop} from 'react-dnd'; +import {AiOutlinePlus as PlusIcon} from 'react-icons/ai'; -import styles from "./DayRoute.module.scss"; +import styles from './DayRoute.module.scss'; -import BottomSlide from "@/components/BottomSlide/BottomSlide"; +import BottomSlide from '@/components/BottomSlide/BottomSlide'; -import AddPlace from "../AddPlace/AddPlace"; -import EmptyRoute from "../EmptyRoute/EmptyRoute"; -import PlaceCard from "../PlaceCard/PlaceCard"; +import AddPlace from '../AddPlace/AddPlace'; +import DraggablePlaceCard from '../DraggablePlaceCard/DraggablePlaceCard'; +import EmptyRoute from '../EmptyRoute/EmptyRoute'; -import { DayRouteProps } from "@/types/route"; +import {DayRouteProps} from '@/types/route'; + +function DayRoute({day, date, placeList, editMode, selectedPlaces, handlePlaceSelection}: DayRouteProps) { + const {isOpen, onOpen, onClose} = useDisclosure(); + const [placeCards, setPlaceCards] = useState(placeList); + + const findCard = useCallback( + (id: number) => { + const card = placeCards.filter((item) => item.id == id)[0]; + return { + card, + index: placeCards.indexOf(card), + }; + }, + [placeCards], + ); + + const moveCard = useCallback( + (id: number, atIndex: number) => { + const {card, index} = findCard(id); + setPlaceCards( + update(placeCards, { + $splice: [ + [index, 1], + [atIndex, 0, card], + ], + }), + ); + }, + [findCard, placeCards, setPlaceCards], + ); + + const [, drop] = useDrop(() => ({accept: 'CARD'})); + + useEffect(() => { + console.log(placeList); + }, [placeList]); -function DayRoute({ day, date, placeList }: DayRouteProps) { - const { isOpen, onOpen, onClose } = useDisclosure(); return ( <>
@@ -22,20 +59,26 @@ function DayRoute({ day, date, placeList }: DayRouteProps) { {date}
-
- {placeList.length ? ( - placeList.map((place, index) => ( - + {placeCards.length ? ( + placeCards.map((place) => ( + )) ) : ( diff --git a/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx b/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx new file mode 100644 index 00000000..b8c40b01 --- /dev/null +++ b/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx @@ -0,0 +1,88 @@ +import {useState} from 'react'; +import {useDrag, useDrop} from 'react-dnd'; +import {IoMdMenu as MoveIcon} from 'react-icons/io'; +import {RiCheckboxCircleFill as SelectedIcon} from 'react-icons/ri'; +import {RiCheckboxBlankCircleLine as UnselectedIcon} from 'react-icons/ri'; + +import styles from '../PlaceCard/PlaceCard.module.scss'; + +import {Item} from '@/types/route'; +import {DraggablePlaceCardProps} from '@/types/route'; + +function PlaceCard({ + id, + order, + name, + category, + address, + editMode, + onSelect, + moveCard, + findCard, +}: DraggablePlaceCardProps) { + const [isChecked, setIsChecked] = useState(false); + const originalIndex = findCard(id).index; + + const [, drag] = useDrag( + () => ({ + type: 'CARD', + item: {id, originalIndex}, + canDrag: () => !isChecked && editMode, + end: (item, monitor) => { + const {id: droppedId, originalIndex} = item; + const didDrop = monitor.didDrop(); + if (!didDrop) { + moveCard(droppedId, originalIndex); + } + }, + }), + [id, originalIndex, moveCard, editMode, isChecked], + ); + + const [, drop] = useDrop( + () => ({ + accept: 'CARD', + hover({id: draggedId}: Item) { + if (draggedId !== id) { + const {index: overIndex} = findCard(id); + moveCard(draggedId, overIndex); + } + }, + }), + [findCard, moveCard], + ); + + const handleSelect = () => { + setIsChecked(!isChecked); + // TODO: place id 넘기기 + onSelect(name); + }; + + return ( +
drag(drop(node))} className={styles.cardContainer}> + + +
+ {!editMode &&
{order}
} +
+ {editMode &&
{order}
} +
+

{name}

+

{category}

+

{address}

+
+
+
+
{editMode && }
+
+ ); +} + +export default PlaceCard; diff --git a/src/components/Route/PlaceCard/PlaceCard.module.scss b/src/components/Route/PlaceCard/PlaceCard.module.scss index c719c137..af83c47d 100644 --- a/src/components/Route/PlaceCard/PlaceCard.module.scss +++ b/src/components/Route/PlaceCard/PlaceCard.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .cardContainer { display: flex; @@ -6,6 +6,14 @@ gap: 0.8rem; // padding: 16px 20px; + .placeInformationContainer { + display: flex; + gap: 0.8rem; + align-items: center; + width: 100%; + margin-left: 8px; + } + .numberContainer { @include typography(captionSmall); display: flex; @@ -16,30 +24,37 @@ background-color: $secondary400; border-radius: 50%; color: $neutral0; + margin-left: -8px; } .placeContainer { width: 100%; display: flex; - flex-direction: column; + align-items: center; + gap: 1.2rem; padding: 16px 20px; border-radius: 16px; background-color: $neutral0; box-shadow: $shadow200; - h1 { - @include typography(titleMedium); - color: $neutral900; - } + .placeInformation { + display: flex; + flex-direction: column; - h2 { - @include typography(captionSmall); - color: $primary200; - } + h1 { + @include typography(titleMedium); + color: $neutral900; + } + + h2 { + @include typography(captionSmall); + color: $primary200; + } - p { - @include typography(captionSmall); - color: $neutral400; + p { + @include typography(captionSmall); + color: $neutral400; + } } } } diff --git a/src/components/Route/PlaceCard/PlaceCard.tsx b/src/components/Route/PlaceCard/PlaceCard.tsx index ee6b84bc..1a78029d 100644 --- a/src/components/Route/PlaceCard/PlaceCard.tsx +++ b/src/components/Route/PlaceCard/PlaceCard.tsx @@ -1,15 +1,19 @@ -import styles from "./PlaceCard.module.scss"; +import styles from './PlaceCard.module.scss'; -import { PlaceCardProps } from "@/types/route"; +import {PlaceCardProps} from '@/types/route'; -function PlaceCard({ index, name, category, address }: PlaceCardProps) { +function PlaceCard({order, name, category, address}: PlaceCardProps) { return (
-
{index}
-
-

{name}

-

{category}

-

{address}

+
+
{order}
+
+
+

{name}

+

{category}

+

{address}

+
+
); diff --git a/src/components/Route/RouteTabPanel/RouteTabPanel.module.scss b/src/components/Route/RouteTabPanel/RouteTabPanel.module.scss index dea14daf..8f6feb87 100644 --- a/src/components/Route/RouteTabPanel/RouteTabPanel.module.scss +++ b/src/components/Route/RouteTabPanel/RouteTabPanel.module.scss @@ -14,13 +14,13 @@ min-width: 36rem; max-width: 45rem; height: 16rem; - } - .zoomInbutton { - position: absolute; - top: 16rem; - right: 2rem; - z-index: 10; + .zoomInButton { + position: absolute; + top: 11rem; + right: 2rem; + z-index: 10; + } } .routeContainer { @@ -31,4 +31,46 @@ padding-bottom: 12rem; } } + + .bottomButtonContainer, + .activeBottomButtonContainer { + position: fixed; + bottom: 0; + width: 100%; + min-width: 36rem; + max-width: 45rem; + z-index: 3; + transition: 0.5s; + + box-shadow: $shadow200; + + display: flex; + // height: 70px; + padding: 16px 20px; + justify-content: center; + align-items: center; + flex-shrink: 0; + + button { + width: 100%; + padding: 8px 0; + display: flex; + justify-content: center; + align-items: center; + gap: 0.4rem; + + span { + @include typography(button); + color: $neutral0; + } + } + } + + .bottomButtonContainer { + background-color: $neutral200; + } + + .activeBottomButtonContainer { + background-color: $primary300; + } } diff --git a/src/components/Route/RouteTabPanel/RouteTabPanel.tsx b/src/components/Route/RouteTabPanel/RouteTabPanel.tsx index 4115798d..8ae88540 100644 --- a/src/components/Route/RouteTabPanel/RouteTabPanel.tsx +++ b/src/components/Route/RouteTabPanel/RouteTabPanel.tsx @@ -1,10 +1,20 @@ +import {useDisclosure} from '@chakra-ui/react'; +import {useEffect, useState} from 'react'; +import {HiOutlineTrash as DeleteIcon} from 'react-icons/hi'; +import {RiArrowUpDownFill as MoveIcon} from 'react-icons/ri'; import {useNavigate} from 'react-router-dom'; +import {useSetRecoilState} from 'recoil'; import styles from './RouteTabPanel.module.scss'; +import AlertModal from '@/components/AlertModal/AlertModal'; +import BottomSlideLeft from '@/components/BottomSlide/BottomSlideLeft'; + import ZoomInIcon from '@/assets/icons/zoomIn.svg?react'; +import {isModalOpenState} from '@/recoil/vote/alertModal'; import {getSpaceId} from '@/utils/getSpaceId'; +import DayMove from '../DayMove/DayMove'; import DayNavigationBar from '../DayNavigationBar/DayNavigationBar'; import DayRoute from '../DayRoute/DayRoute'; import EmptyDate from '../EmptyDate/EmptyDate'; @@ -105,6 +115,33 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) { ], }; + const [isEditMode, setIsEditMode] = useState(false); + const [selectedPlaces, setSelectedPlaces] = useState([]); + const {isOpen, onOpen, onClose} = useDisclosure(); + const setIsModalOpen = useSetRecoilState(isModalOpenState); + + const handleEditMode = () => { + setIsEditMode(!isEditMode); + + // TODO: 완료 버튼 눌렀을 때 처리 + }; + + const handlePlaceSelection = (placeName: string) => { + if (selectedPlaces.includes(placeName)) { + setSelectedPlaces((prevSelectedPlaces) => prevSelectedPlaces.filter((place) => place !== placeName)); + } else { + setSelectedPlaces((prevSelectedPlaces) => [...prevSelectedPlaces, placeName]); + } + }; + + const deletePlaces = (placeList: string[]) => { + console.log('삭제', placeList); + }; + + useEffect(() => { + console.log(selectedPlaces); + }, [selectedPlaces]); + const navigate = useNavigate(); const spaceId = getSpaceId(); @@ -120,19 +157,51 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) {
+
-
- +
{data.journeys && data.journeys.map((journey, index) => ( - + ))}
+ {isEditMode && ( +
0 ? styles.activeBottomButtonContainer : styles.bottomButtonContainer}> + + +
+ )} + 0 && isOpen} + onClose={onClose} + children={} + /> + + deletePlaces(selectedPlaces)} + />
); } diff --git a/src/components/SearchFromHome/SearchBar/SearchBar.tsx b/src/components/SearchFromHome/SearchBar/SearchBar.tsx index 2fd6207a..378ff283 100644 --- a/src/components/SearchFromHome/SearchBar/SearchBar.tsx +++ b/src/components/SearchFromHome/SearchBar/SearchBar.tsx @@ -34,10 +34,9 @@ function SearchBar({forSearch, setForSearch}: PropsType) { } function search() { - const beforeData = forSearch; - beforeData.keyword = inputValue; - setForSearch(beforeData); - navigate(`/home/search?keyword=${inputValue}&category=0&map=false&location=${forSearch.location}&sort=등록순`); + navigate( + `/home/search?keyword=${inputValue}&category=0&map=false&location=${forSearch.location}&sort=등록순&hot=false`, + ); } function removeValue() { @@ -48,6 +47,7 @@ function SearchBar({forSearch, setForSearch}: PropsType) { setInputValue(''); const beforeData = forSearch; beforeData.keyword = ''; + beforeData.hot = 'false'; setForSearch(beforeData); } } diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss index 8c2e3964..0121a469 100644 --- a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { display: flex; @@ -7,9 +7,11 @@ img { min-width: 10.6rem; - min-height: 10.6rem; + height: 10.6rem; border-radius: 1.6rem; + + background-color: $neutral200; } .text_box { display: flex; diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx index 531b6e4a..b8512538 100644 --- a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx @@ -1,25 +1,33 @@ -import { Link } from "react-router-dom"; +import {Link} from 'react-router-dom'; -import styles from "./HotItem.module.scss"; +import styles from './HotItem.module.scss'; -import areas from "@/utils/areas.json"; +import {translateCategoryToStr} from '@/hooks/Search/useSearch'; -import { SearchHotItemType } from "@/types/home"; +import nullImg from '@/assets/homeIcons/search/nullImg.svg'; +import areas from '@/utils/areas.json'; +import titleCaseChange from '@/utils/titleCaseChange'; + +import {SearchHotItemType} from '@/types/home'; interface PropsData { data: SearchHotItemType; } -function HotItem({ data }: PropsData) { - const location = areas.filter((area) => area.areaCode === data.areaCode)[0] - .name; +function HotItem({data}: PropsData) { + const location = areas.filter((area) => area.areaCode === data.areaCode)[0].name; + const category = translateCategoryToStr(data.contentTypeId); + const title = titleCaseChange(data.title); + const imgSrc = data.thumbnail ? data.thumbnail : nullImg; return ( - {`${data.title}의 + {`${data.title}의

- {data.title} - {location} + {title} + + {category} · {location} +

); diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx index db7421be..f28f5d81 100644 --- a/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx @@ -9,19 +9,19 @@ import SlideButton from '@/components/SlideButton/SlideButton'; import HotItem from './HotItem/HotItem'; -import {SearchHotItemType} from '@/types/home'; +import {Popular, SearchDataType, SearchHotItemType} from '@/types/home'; interface PropsType { type: number; } function HotItems({type}: PropsType) { - const [data, setData] = useState(); + const [data, setData] = useState(); const [slideLocation, setSlideLocation] = useState(0); const [componentRef, size] = useComponentSize(); useEffect(() => { - async function getData(apiURL: string, set: Dispatch>) { + async function getData(apiURL: string, set: Dispatch>) { try { const fetchData = await axios.get(`${apiURL}`, { params: { @@ -29,12 +29,14 @@ function HotItems({type}: PropsType) { placeTypeId: type, }, }); - set(fetchData.data); + const data: SearchDataType = fetchData.data; + + set(data.data.places); } catch (error) { console.log(error); } } - getData('api/places/popular', setData); + getData('/api/places/popular', setData); }, [type]); return (
diff --git a/src/components/SearchFromHome/SearchHome/SearchHome.tsx b/src/components/SearchFromHome/SearchHome/SearchHome.tsx index 0dcb6628..c98c1edc 100644 --- a/src/components/SearchFromHome/SearchHome/SearchHome.tsx +++ b/src/components/SearchFromHome/SearchHome/SearchHome.tsx @@ -3,16 +3,12 @@ import styles from './SearchHome.module.scss'; import HotItems from './HotItems/HotItems'; import SearchKeyword from './SearchKeyword/SearchKeyword'; -interface Propstype { - setKeywordClick: React.Dispatch>; -} - -function SearchHome({setKeywordClick}: Propstype) { +function SearchHome() { return (

인기 검색 키워드

- +

최근 30일간 인기 장소

diff --git a/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx index 9e28494c..dcb4b35e 100644 --- a/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx +++ b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx @@ -1,4 +1,5 @@ -import {useEffect, useState} from 'react'; +import axios from 'axios'; +import {Dispatch, useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; import styles from './SearchKeyword.module.scss'; @@ -7,21 +8,26 @@ import useComponentSize from '@/hooks/useComponetSize'; import SlideButton from '@/components/SlideButton/SlideButton'; -import {getData} from '@/mocks/handlers/home'; +import {Keywords, SearchDataType, SearchKeywordType} from '@/types/home'; -interface Propstype { - setKeywordClick: React.Dispatch>; -} - -function SearchKeyword({setKeywordClick}: Propstype) { - const [data, setData] = useState<{name: string; code: string}[] | undefined>(); +function SearchKeyword() { + const [data, setData] = useState(); const [listWidth, setListWidth] = useState(0); const [slideLocation, setSlideLocation] = useState(0); const [componentRef, size] = useComponentSize(); const navigate = useNavigate(); useEffect(() => { - getData<{name: string; code: string}[] | undefined>('api/places/popular/keywords', setData); + async function getData(apiURL: string, set: Dispatch) { + try { + const fetchData = await axios.get(`${apiURL}`); + const data: SearchDataType = fetchData.data; + set(data.data.keywords); + } catch (error) { + console.log(error); + } + } + getData('/api/places/popular/keywords', setData); }, []); // 각 키워드의 너비를 모두 더한 값을 구함 @@ -66,11 +72,9 @@ function SearchKeyword({setKeywordClick}: Propstype) {

{ - setKeywordClick(true); - setTimeout(() => { - setKeywordClick(false); - }, 2000); - navigate(`/home/search?keyword=${keyword.name}&category=0&map=false&location=전국&sort=등록순`); + navigate( + `/home/search?keyword=${keyword.name}&category=0&map=false&location=전국&sort=등록순&hot=true`, + ); }} > {keyword.name} diff --git a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss index e90ca855..8ecbb00c 100644 --- a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss +++ b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss @@ -1,6 +1,8 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { + position: relative; + width: 7.9rem; height: 2.4rem; @@ -17,4 +19,30 @@ width: 2.4rem; height: 2.4rem; } + + .modal { + position: absolute; + top: 32px; + right: 0; + + width: 10.8rem; + + display: flex; + flex-direction: column; + justify-content: space-between; + + padding: 20px 32px; + + border-radius: 16px; + + background-color: $neutral0; + + box-shadow: + 0px 1px 8px 2px rgba(20, 20, 20, 0.1), + 0px 0px 1px 0px rgba(20, 20, 20, 0.04); + + overflow: hidden; + + transition: 0.5s all; + } } diff --git a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx index 3db7424d..e26c4bc0 100644 --- a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx +++ b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx @@ -1,12 +1,50 @@ -import { BsFilterLeft } from "react-icons/bs"; +import {useEffect, useState} from 'react'; +import {BsFilterLeft} from 'react-icons/bs'; +import {useNavigate} from 'react-router-dom'; -import styles from "./DateFilter.module.scss"; +import styles from './DateFilter.module.scss'; + +import {ForSearchType} from '@/types/home'; + +interface PropsType { + forSearch: ForSearchType; +} + +function DateFilter({forSearch}: PropsType) { + const [click, setClick] = useState(false); + const filterData = ['등록순', '이름순', '인기순']; + const navigate = useNavigate(); + + function handleModal() { + setClick((prev) => !prev); + } + + function selectSort(sort: string) { + navigate( + `/home/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${forSearch.location}&sort=${sort}&hot=${forSearch.hot}`, + ); + } + + useEffect(() => { + setClick(false); + }, [forSearch.sort]); -function DateFilter() { return ( -

+
- 등록순 + {forSearch.sort} +
+ {filterData.map((data) => ( + { + selectSort(data); + }} + > + {data} + + ))} +
); } diff --git a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx index db343e43..b75da888 100644 --- a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx +++ b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx @@ -32,13 +32,18 @@ function LocationFliterPage({forSearch, click, handleClick}: PropsType) { useEffect(() => { const locationData = forSearch.location.split(' '); - setArea(locationData[0]); - setSigungu(locationData[1]); + if (locationData[0] === '전국') { + setArea('전국'); + setSigungu('전체 지역'); + } else { + setArea(locationData[0]); + setSigungu(locationData[1]); + } }, [forSearch.location]); function submit() { navigate( - `/home/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${area} ${sigungu}&sort=${forSearch.sort}`, + `/home/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${area} ${sigungu}&sort=${forSearch.sort}&hot=${forSearch.hot}`, ); handleClick(); } @@ -48,7 +53,7 @@ function LocationFliterPage({forSearch, click, handleClick}: PropsType) { className={styles.container} style={{ position: window.innerWidth > 450 ? 'absolute' : 'fixed', - top: window.innerWidth > 450 ? '-88px' : 0, + top: 0, right: click ? '-100%' : 0, height: `${vh * 100}px`, }} diff --git a/src/components/SearchFromHome/SearchList/Map/Map.tsx b/src/components/SearchFromHome/SearchList/Map/Map.tsx index 10e974fd..e43b5669 100644 --- a/src/components/SearchFromHome/SearchList/Map/Map.tsx +++ b/src/components/SearchFromHome/SearchList/Map/Map.tsx @@ -45,7 +45,7 @@ function Map({data, categoryChange}: PropsType) { const markerImage = new window.kakao.maps.MarkerImage(image, imageSize, imageOption); const marker = new window.kakao.maps.Marker({ - position: new window.kakao.maps.LatLng(data.location.latitude, data.location.longtitude), + position: new window.kakao.maps.LatLng(data.location.latitude, data.location.longitude), image: markerImage, }); return marker; @@ -121,6 +121,7 @@ function Map({data, categoryChange}: PropsType) { level: 4, }; setMap(new window.kakao.maps.Map(container, options)); + console.log(data); }, [data]); // 맵 생성 시 현재 데이터 좌표들의 바운드를 만들어 중심점 생성 @@ -130,7 +131,7 @@ function Map({data, categoryChange}: PropsType) { setSmallPin(data); setBigPin(data); data.map((data) => { - bounds.extend(new window.kakao.maps.LatLng(data.location.latitude, data.location.longtitude)); + bounds.extend(new window.kakao.maps.LatLng(data.location.latitude, data.location.longitude)); }); map.setBounds(bounds); } diff --git a/src/components/SearchFromHome/SearchList/Map/MapItems/MapItem/MapItem.tsx b/src/components/SearchFromHome/SearchList/Map/MapItems/MapItem/MapItem.tsx index 20b0c8ed..293e84d6 100644 --- a/src/components/SearchFromHome/SearchList/Map/MapItems/MapItem/MapItem.tsx +++ b/src/components/SearchFromHome/SearchList/Map/MapItems/MapItem/MapItem.tsx @@ -1,36 +1,26 @@ -import { Link } from "react-router-dom"; +import {Link} from 'react-router-dom'; -import styles from "./MapItem.module.scss"; +import styles from './MapItem.module.scss'; -import areas from "@/utils/areas.json"; +import {translateCategoryToStr} from '@/hooks/Search/useSearch'; -import { SearchItemType } from "@/types/home"; +import areas from '@/utils/areas.json'; + +import {SearchItemType} from '@/types/home'; interface PropsType { data: SearchItemType; categoryChange: boolean; } -function MapItem({ data, categoryChange }: PropsType) { - const location = areas.filter( - (area) => area.areaCode === data.location.areaCode, - )[0].name; - const category = - data.category === "관광지" || - data.category === "문화시설" || - data.category === "레포츠" || - data.category === "쇼핑" - ? "관광" - : data.category; +function MapItem({data, categoryChange}: PropsType) { + const location = areas.filter((area) => area.areaCode === data.location.areaCode)[0].name; + const category = translateCategoryToStr(data.contentTypeId); return ( - {`${data.title}의 -

+ {`${data.title}의 +

{data.title} {category}·{location} diff --git a/src/components/SearchFromHome/SearchList/Map/MapItems/MapItems.tsx b/src/components/SearchFromHome/SearchList/Map/MapItems/MapItems.tsx index 60940f27..ef32a661 100644 --- a/src/components/SearchFromHome/SearchList/Map/MapItems/MapItems.tsx +++ b/src/components/SearchFromHome/SearchList/Map/MapItems/MapItems.tsx @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; +import {useEffect, useState} from 'react'; -import styles from "./MapItems.module.scss"; +import styles from './MapItems.module.scss'; -import useComponentSize from "@/hooks/useComponetSize"; +import useComponentSize from '@/hooks/useComponetSize'; -import SlideButton from "@/components/SlideButton/SlideButton"; +import SlideButton from '@/components/SlideButton/SlideButton'; -import MapItem from "./MapItem/MapItem"; +import MapItem from './MapItem/MapItem'; -import { SearchItemType } from "@/types/home"; +import {SearchItemType} from '@/types/home'; interface PropsType { data: SearchItemType[]; @@ -32,14 +32,12 @@ function MapItems({ const [throttle, setThrottle] = useState(true); function setCurrentIndex() { - const criterion = document.querySelector("#map_slide_container"); - const elements = document.querySelectorAll("#map_slide"); + const criterion = document.querySelector('#map_slide_container'); + const elements = document.querySelectorAll('#map_slide'); const childrenArray = Array.from(elements[0].children); for (const item of childrenArray) { - const currentLeft = - criterion && - item.getBoundingClientRect().x - criterion.getBoundingClientRect().x; + const currentLeft = criterion && item.getBoundingClientRect().x - criterion.getBoundingClientRect().x; if (currentLeft) { if (0 < currentLeft && currentLeft < 150) { const index = childrenArray.indexOf(item); @@ -65,10 +63,11 @@ function MapItems({ useEffect(() => { if (size.width < 449) { - componentRef.current?.scrollTo({ left: 0, behavior: "smooth" }); + componentRef.current?.scrollTo({left: 0, behavior: 'smooth'}); } else { setSlideLocation(0); } + console.log(data); }, [data, size, componentRef]); useEffect(() => { @@ -96,7 +95,7 @@ function MapItems({ }, [throttle]); return ( -

+
{data && ( - {data && - data.map((data, i) => ( - - ))} + {data && data.map((data, i) => )}
); diff --git a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss index a5e0b57d..99ed157b 100644 --- a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss +++ b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { width: 100%; @@ -18,6 +18,8 @@ opacity: 1; transition: 0.3s all linear; + + background-color: $neutral200; } .text { diff --git a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx index 21c82a0d..30508ddd 100644 --- a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx +++ b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx @@ -1,37 +1,35 @@ -import { Link } from "react-router-dom"; +import {Link} from 'react-router-dom'; -import styles from "./SearchItem.module.scss"; +import styles from './SearchItem.module.scss'; -import areas from "@/utils/areas.json"; +import {translateCategoryToStr} from '@/hooks/Search/useSearch'; -import { SearchItemType } from "@/types/home"; +import nullImg from '@/assets/homeIcons/search/nullImg.svg'; +import areas from '@/utils/areas.json'; +import titleCaseChange from '@/utils/titleCaseChange'; + +import {SearchItemType} from '@/types/home'; interface PropsType { data: SearchItemType; categoryChange: boolean; } -function SearchItem({ data, categoryChange }: PropsType) { - const location = areas.filter( - (area) => area.areaCode === data.location.areaCode, - )[0].name; - const category = - data.category === "관광지" || - data.category === "문화시설" || - data.category === "레포츠" || - data.category === "쇼핑" - ? "관광" - : data.category; +function SearchItem({data, categoryChange}: PropsType) { + const title = titleCaseChange(data.title); + const location = areas.filter((area) => area.areaCode === data.location.areaCode)[0].name; + const category = translateCategoryToStr(data.contentTypeId); + const imgSrc = data.thumbnail ? data.thumbnail : nullImg; return ( {`${data.title}의 -

- {data.title} +

+ {title} {category}·{location} diff --git a/src/components/SearchFromHome/SearchList/SearchList.module.scss b/src/components/SearchFromHome/SearchList/SearchList.module.scss index 5836bbf5..6688c567 100644 --- a/src/components/SearchFromHome/SearchList/SearchList.module.scss +++ b/src/components/SearchFromHome/SearchList/SearchList.module.scss @@ -1,8 +1,6 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { - position: relative; - width: 100%; display: flex; diff --git a/src/components/SearchFromHome/SearchList/SearchList.tsx b/src/components/SearchFromHome/SearchList/SearchList.tsx index b27d2703..8de7683e 100644 --- a/src/components/SearchFromHome/SearchList/SearchList.tsx +++ b/src/components/SearchFromHome/SearchList/SearchList.tsx @@ -1,9 +1,9 @@ import {useEffect, useState} from 'react'; -import {useNavigate, useSearchParams} from 'react-router-dom'; +import {useNavigate} from 'react-router-dom'; import styles from './SearchList.module.scss'; -import {getData} from '@/mocks/handlers/home'; +import {keywordSearch, search} from '@/hooks/Search/useSearch'; import DateFilter from './DateFilter/DateFilter'; import LocationFilter from './LocationFilter/LocationFilter'; @@ -16,23 +16,21 @@ import {ForSearchType, SearchItemType} from '@/types/home'; interface PropsType { forSearch: ForSearchType; - keywordClick: boolean; } -function SearchList({forSearch, keywordClick}: PropsType) { - const [data, setData] = useState(); - const [filterData, setFilterData] = useState(); +function SearchList({forSearch}: PropsType) { + const [data, setData] = useState(); + const [filterData, setFilterData] = useState(); const [categoryChange, setCategoryChange] = useState(false); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); useEffect(() => { - if (!keywordClick) { - getData('api/home/search/search', setData); + if (forSearch.hot === 'true') { + keywordSearch(forSearch.keyword, forSearch.location, forSearch.sort, setData); } else { - getData('api/home/search/search', setData); + search(forSearch.keyword, forSearch.location, forSearch.sort, setData); } - }, [searchParams]); + }, [forSearch.keyword, forSearch.location, forSearch.sort, forSearch.hot]); useEffect(() => { if (data) { @@ -52,20 +50,20 @@ function SearchList({forSearch, keywordClick}: PropsType) { function onMap() { navigate( - `/home/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=true&location=${forSearch.location}&sort=${forSearch.sort}`, + `/home/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=true&location=${forSearch.location}&sort=${forSearch.sort}&hot=${forSearch.hot}`, ); } return (

- + {forSearch.hot === 'false' && } {forSearch.map === 'true' && filterData ? ( ) : ( <>
- +
    {filterData && diff --git a/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx index f0fdbbdf..5e0a150d 100644 --- a/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx +++ b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx @@ -21,7 +21,7 @@ function Tab({forSearch, thisCategory, setCategoryChange}: PropsType) { setCategoryChange(false); }, 150); navigate( - `/home/search?keyword=${forSearch.keyword}&category=${key}&map=${forSearch.map}&location=${forSearch.location}&sort=${forSearch.sort}`, + `/home/search?keyword=${forSearch.keyword}&category=${key}&map=${forSearch.map}&location=${forSearch.location}&sort=${forSearch.sort}&hot=${forSearch.hot}`, ); } diff --git a/src/components/User/EditProfileForm/EditProfileForm.module.scss b/src/components/User/EditProfileForm/EditProfileForm.module.scss new file mode 100644 index 00000000..ea311f5f --- /dev/null +++ b/src/components/User/EditProfileForm/EditProfileForm.module.scss @@ -0,0 +1,22 @@ +@use "@/sass" as *; + +.container { + padding: 0 20px; + + label { + display: block; + + @include typography(tabLabel); + color: $neutral900; + } + + small { + width: 100%; + position: absolute; + bottom: -4px; + left: 0; + + @include typography(captionSmall); + color: $danger300; + } +} diff --git a/src/components/User/EditProfileForm/EditProfileForm.tsx b/src/components/User/EditProfileForm/EditProfileForm.tsx new file mode 100644 index 00000000..3ed0cd0a --- /dev/null +++ b/src/components/User/EditProfileForm/EditProfileForm.tsx @@ -0,0 +1,45 @@ +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; + +import styles from "./EditProfileForm.module.scss"; + +import AuthButton from "@/components/Auth/Button/AuthButton"; +import InputImage from "@/components/Auth/Input/InputImage"; +import InputNickname from "@/components/Auth/Input/InputNickname"; + +import { AuthForm } from "@/types/auth"; + +function EditProfileForm() { + const { + register, + resetField, + formState: { errors, dirtyFields }, + handleSubmit, + } = useForm({ + mode: "onChange", + }); + + const navigate = useNavigate(); + + const onSubmit = () => { + // 프로필 변경 api 요청 + navigate("/user", { replace: true }); + }; + + return ( +
    + + + + + + + ); +} + +export default EditProfileForm; diff --git a/src/components/User/MypageList/MypageList.module.scss b/src/components/User/MypageList/MypageList.module.scss new file mode 100644 index 00000000..cfe7c895 --- /dev/null +++ b/src/components/User/MypageList/MypageList.module.scss @@ -0,0 +1,120 @@ +@use "@/sass" as *; + +.container { + & > li { + display: flex; + justify-content: space-between; + align-items: center; + + padding: 24px 20px; + border-bottom: 1px solid $neutral200; + cursor: pointer; + + @include typography(tabLabel); + + &:hover { + font-weight: 600; + border-color: $neutral300; + } + + svg { + fill: $neutral400; + } + } + + .alert { + &__left { + display: flex; + align-items: center; + gap: 4px; + + &__tooltip { + position: relative; + + .tooltipList { + display: none; + position: absolute; + left: -38px; + width: 20rem; + padding: 21px 8px 8px 16px; + margin-top: 2px; + border-radius: 8px; + + background-image: url("@/assets/icons/tooltipFrame.svg"); + background-repeat: no-repeat; + background-size: auto; + + list-style-type: disc; + list-style-position: inside; + + @include typography(captionSmall); + line-height: 1.8rem; + color: $neutral0; + + @include animate(showTooltip, 0.2s, ease-in, forwards); + + & > li > span { + margin-left: -5px; + } + } + + & > button { + display: block; + + &:focus ~ .tooltipList, + &:hover ~ .tooltipList { + display: block; + } + } + } + } + + &__button { + position: relative; + width: 5.6rem; + height: 3.2rem; + border-radius: 32px; + + .alertState { + position: absolute; + top: 0; + width: 3.2rem; + height: 3.2rem; + + background-color: $neutral0; + border-radius: 50%; + transition: all 0.2s ease-in; + } + + &.on { + background-color: $success300; + .alertState { + left: calc(100% - 3.2rem); + border: 2px solid $success300; + } + } + &.off { + background-color: $neutral200; + .alertState { + left: 0; + border: 2px solid $neutral200; + } + } + } + } + + .version { + color: $neutral400; + } +} + +@include keyframes(showTooltip) { + 0% { + transform: scale(0); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +} diff --git a/src/components/User/MypageList/MypageList.tsx b/src/components/User/MypageList/MypageList.tsx new file mode 100644 index 00000000..7fcbc998 --- /dev/null +++ b/src/components/User/MypageList/MypageList.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { RiArrowRightSLine } from "react-icons/ri"; +import { useNavigate } from "react-router-dom"; + +import styles from "./MypageList.module.scss"; + +import AlertIcon from "@/assets/icons/error-warning-line.svg?react"; + +function MypageList() { + const [alertOn, setAlertOn] = useState( + Notification.permission === "granted" ? true : false, + ); + const navigate = useNavigate(); + + const onClickAlert = () => { + if (Notification.permission === "denied") { + return; + } + + setAlertOn(!alertOn); + }; + + return ( +
      +
    • { + navigate("/user/privacy"); + }} + > +
      계정 관리
      + +
    • + +
    • +
      + 알림 +
      + + +
        +
      • + + 구글 크롬(Chrome) 브라우저에 최적화 되어 있어 크롬 브라우저 + 사용을 권장합니다. + +
      • +
      • + + 브라우저의 알림 설정을 켜주셔야 서비스 알림을 받을 수 + 있습니다. + +
      • +
      +
      +
      + + +
    • + +
    • +
      버전 정보
      +
      1.1.0
      +
    • +
    + ); +} + +export default MypageList; diff --git a/src/components/User/Mywork/Mywork.module.scss b/src/components/User/Mywork/Mywork.module.scss new file mode 100644 index 00000000..055963d1 --- /dev/null +++ b/src/components/User/Mywork/Mywork.module.scss @@ -0,0 +1,40 @@ +@use "@/sass" as *; + +.container { + display: flex; + align-items: center; + gap: 8px; + margin: 0 20px; + margin-bottom: 8px; + + border: 1px solid $neutral200; + border-radius: 16px; + + button { + padding: 16px; + flex: 1; + + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + + &:hover { + border-color: $primary200; + + .title { + font-weight: 600; + } + } + } + + .colBar { + width: 1px; + height: 5rem; + background-color: $neutral200; + } + + .title { + @include typography(subTitle); + } +} diff --git a/src/components/User/Mywork/Mywork.tsx b/src/components/User/Mywork/Mywork.tsx new file mode 100644 index 00000000..943d736e --- /dev/null +++ b/src/components/User/Mywork/Mywork.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from "react-router-dom"; + +import styles from "./Mywork.module.scss"; + +import MapPin from "@/assets/icons/mapPin.svg?react"; +import Star from "@/assets/icons/star.svg?react"; + +function Mywork() { + const navigate = useNavigate(); + + return ( +
    + + +
    + + +
    + ); +} + +export default Mywork; diff --git a/src/components/User/Profile/Profile.module.scss b/src/components/User/Profile/Profile.module.scss new file mode 100644 index 00000000..32ea1f7b --- /dev/null +++ b/src/components/User/Profile/Profile.module.scss @@ -0,0 +1,39 @@ +@use "@/sass" as *; + +.container { +} +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + .image { + position: relative; + width: 7.2rem; + height: 7.2rem; + border-radius: 50%; + + background-position: center; + background-repeat: no-repeat; + background-size: cover; + + &__edit { + position: absolute; + bottom: 0; + right: 0; + + background-color: $neutral100; + border: 1px solid $neutral200; + border-radius: 50%; + + width: 2.6rem; + height: 2.6rem; + padding: 4px; + } + } + + .userName { + @include typography(headline); + } +} diff --git a/src/components/User/Profile/Profile.tsx b/src/components/User/Profile/Profile.tsx new file mode 100644 index 00000000..9ebd52ae --- /dev/null +++ b/src/components/User/Profile/Profile.tsx @@ -0,0 +1,33 @@ +import { useNavigate } from "react-router-dom"; + +import styles from "./Profile.module.scss"; + +import Pencil from "@/assets/icons/pencil.svg?react"; + +import { ProfileProps } from "@/types/user"; + +function Profile({ userInfo }: ProfileProps) { + const navigate = useNavigate(); + + return ( +
    +
    + +
    + +
    {userInfo.nickname}
    +
    + ); +} + +export default Profile; diff --git a/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.module.scss b/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.module.scss new file mode 100644 index 00000000..a9033aa2 --- /dev/null +++ b/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.module.scss @@ -0,0 +1,21 @@ +@use '@/sass' as *; + +.container { + display: flex; + flex-direction: column; + + width: 100%; + // margin: 15px 0; + padding: 15px 8px 50px; + + // background-color: aqua; + // z-index: 102; +} + +.dayBox { + @include typography(bodyLarge); + padding: 12px 0; + + display: flex; + justify-content: space-between; +} diff --git a/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.tsx b/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.tsx new file mode 100644 index 00000000..4052bd16 --- /dev/null +++ b/src/components/Vote/VoteBottomSlideContent/AddToJourney/AddToJourney.tsx @@ -0,0 +1,21 @@ +import {Button, Checkbox} from '@chakra-ui/react'; + +import styles from './AddToJourney.module.scss'; + +const AddToJourney = () => { + const days = [1, 2, 3]; + + return ( +
    + {days.map((day, i) => ( + + ))} + +
    + ); +}; + +export default AddToJourney; diff --git a/src/components/Vote/VoteBottomSlideContent/VoteMeatball/VoteMeatball.tsx b/src/components/Vote/VoteBottomSlideContent/VoteMeatball/VoteMeatball.tsx index 59c78950..d1d0ea4a 100644 --- a/src/components/Vote/VoteBottomSlideContent/VoteMeatball/VoteMeatball.tsx +++ b/src/components/Vote/VoteBottomSlideContent/VoteMeatball/VoteMeatball.tsx @@ -21,7 +21,7 @@ import CreateVoteModal from '../../CreateVoteModal/CreateVoteModal'; import {AlertModalProps, VoteMeatballProps} from '@/types/vote'; -const VoteMeatball = ({state, title, isZeroCandidates}: VoteMeatballProps) => { +const VoteMeatball = ({state, title, isZeroCandidates, allCandidatesNotVoted}: VoteMeatballProps) => { const {id: voteId} = useParams(); const setIsCreateModalOpen = useSetRecoilState(isCreateModalOpenState); const setIsBTOpen = useSetRecoilState(isBottomSlideOpenState); @@ -65,7 +65,7 @@ const VoteMeatball = ({state, title, isZeroCandidates}: VoteMeatballProps) => { ) : ( - diff --git a/src/components/Vote/VoteContent/CandidateCard/CandidateCard.module.scss b/src/components/Vote/VoteContent/CandidateCard/CandidateCard.module.scss index 0cc00386..3171b07f 100644 --- a/src/components/Vote/VoteContent/CandidateCard/CandidateCard.module.scss +++ b/src/components/Vote/VoteContent/CandidateCard/CandidateCard.module.scss @@ -69,6 +69,7 @@ // gap: 8px; &__name { + text-align: start; @include typography(titleSmall); color: $neutral900; } @@ -110,3 +111,9 @@ } } } + +.isCandidateSelecting { + * { + color: $neutral300; + } +} diff --git a/src/components/Vote/VoteContent/CandidateCard/CandidateCard.tsx b/src/components/Vote/VoteContent/CandidateCard/CandidateCard.tsx index 2e6ace77..36cec270 100644 --- a/src/components/Vote/VoteContent/CandidateCard/CandidateCard.tsx +++ b/src/components/Vote/VoteContent/CandidateCard/CandidateCard.tsx @@ -1,6 +1,5 @@ import {useState} from 'react'; import {FaRegStar, FaStar} from 'react-icons/fa'; -import {Link} from 'react-router-dom'; import {useRecoilValue} from 'recoil'; import styles from './CandidateCard.module.scss'; @@ -11,6 +10,7 @@ import ThirdIcon from '@/assets/voteIcons/rank_3.svg?react'; import AddDayIcon from '@/assets/voteIcons/vote_addDay.svg?react'; import {isCandidateSelectingState} from '@/recoil/vote/alertModal'; +import AddToJourney from '../../VoteBottomSlideContent/AddToJourney/AddToJourney'; import VotedUserList from '../../VoteBottomSlideContent/VotedUserList/VotedUserList'; import {CandidateCardProps} from '@/types/vote'; @@ -18,7 +18,8 @@ import {CandidateCardProps} from '@/types/vote'; const CandidateCard = ({onBottomSlideOpen, candidate, showResults, index, isMapStyle}: CandidateCardProps) => { const [isVoted, setIsVoted] = useState(false); const isCandidateSelecting = useRecoilValue(isCandidateSelectingState); - const voteCounts = candidate.voteUserId.length; + + const placeInfo = candidate.placeInfo; const getRankClassName = (index: number) => { switch (index) { @@ -50,7 +51,7 @@ const CandidateCard = ({onBottomSlideOpen, candidate, showResults, index, isMapS const RankIcon = showResults && getRankIcon(index); const voteStarIcon = () => { - if (isVoted) return ; + if (isVoted || candidate.amIVoted) return ; else if (isMapStyle) return ; else return ; }; @@ -65,7 +66,7 @@ const CandidateCard = ({onBottomSlideOpen, candidate, showResults, index, isMapS return (
    - {candidate.placeName} + {placeInfo.placeName} {RankIcon && (
    @@ -74,14 +75,18 @@ const CandidateCard = ({onBottomSlideOpen, candidate, showResults, index, isMapS
    - - {candidate.placeName} {'>'} - +
    - {candidate.category} + {placeInfo.category} {'ꞏ'} - {candidate.location} + {placeInfo.location}
    {/* 일정 담기 @@ -90,15 +95,19 @@ const CandidateCard = ({onBottomSlideOpen, candidate, showResults, index, isMapS 있 : 바텀시트 -> 일정 추가api -> 시트close, 완료 토스트 */} - {showResults && ( - )}
    -
    diff --git a/src/components/Vote/VoteContent/CandidateList/CandidateList.module.scss b/src/components/Vote/VoteContent/CandidateList/CandidateList.module.scss index c5ff580d..342c668b 100644 --- a/src/components/Vote/VoteContent/CandidateList/CandidateList.module.scss +++ b/src/components/Vote/VoteContent/CandidateList/CandidateList.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { margin-bottom: 50px; @@ -18,7 +18,7 @@ &__memo { display: flex; align-items: center; - gap: 6px; + gap: 8px; margin-top: 16px; &__text { diff --git a/src/components/Vote/VoteContent/CandidateList/CandidateList.tsx b/src/components/Vote/VoteContent/CandidateList/CandidateList.tsx index 9a8f01dc..4f786258 100644 --- a/src/components/Vote/VoteContent/CandidateList/CandidateList.tsx +++ b/src/components/Vote/VoteContent/CandidateList/CandidateList.tsx @@ -1,8 +1,11 @@ import {Avatar, Checkbox} from '@chakra-ui/react'; +import {useSetRecoilState} from 'recoil'; import styles from './CandidateList.module.scss'; -import useGetSelectedCandidates from '@/hooks/useGetSelectedCandidates'; +import useGetSelectedSet from '@/hooks/useGetSelectedSet'; + +import {selectedCandidatesState} from '@/recoil/vote/candidateList'; import CandidateCard from '../CandidateCard/CandidateCard'; import VoteContentEmpty from '../VoteContentEmpty/VoteContentEmpty'; @@ -10,7 +13,8 @@ import VoteContentEmpty from '../VoteContentEmpty/VoteContentEmpty'; import {CandidateListProps} from '@/types/vote'; const CandidateList = ({candidates, onBottomSlideOpen, showResults, isCandidateSelecting}: CandidateListProps) => { - const {addCandidateInSelectedList} = useGetSelectedCandidates(); + const setSelectedCandidates = useSetRecoilState(selectedCandidatesState); + const {addItemInNewSet} = useGetSelectedSet(setSelectedCandidates); return (
    @@ -24,7 +28,7 @@ const CandidateList = ({candidates, onBottomSlideOpen, showResults, isCandidateS fontSize='2rem' id={`${i}checkbox`} variant='candidateCheckbox' - onChange={() => addCandidateInSelectedList(candidate.id)} + onChange={() => addItemInNewSet(candidate.id)} /> )}
    diff --git a/src/components/Vote/VoteContent/VoteContent.module.scss b/src/components/Vote/VoteContent/VoteContent.module.scss index 64459e1d..5541e0b4 100644 --- a/src/components/Vote/VoteContent/VoteContent.module.scss +++ b/src/components/Vote/VoteContent/VoteContent.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { width: 100%; @@ -26,8 +26,12 @@ } } &__addCandidate { - @include typography(button); + @include typography(captionSmall); color: $neutral900; + + &:disabled { + color: $neutral300; + } } } } diff --git a/src/components/Vote/VoteContent/VoteContent.tsx b/src/components/Vote/VoteContent/VoteContent.tsx index 6de5fc6f..4059ae90 100644 --- a/src/components/Vote/VoteContent/VoteContent.tsx +++ b/src/components/Vote/VoteContent/VoteContent.tsx @@ -14,7 +14,7 @@ import AddCandidate from '../VoteBottomSlideContent/AddCandidate/AddCandidate'; import {VoteContentProps} from '@/types/vote'; -const VoteContent = ({onBottomSlideOpen, data, showResults}: VoteContentProps) => { +const VoteContent = ({onBottomSlideOpen, data, isZeroCandidates, showResults}: VoteContentProps) => { const candidates = data.candidates; const isCandidateSelecting = useRecoilValue(isCandidateSelectingState); @@ -32,12 +32,15 @@ const VoteContent = ({onBottomSlideOpen, data, showResults}: VoteContentProps) = {data.voteStatus}
    - + {!showResults && ( + + )}
    - {/* 후보지&여행지 X -> 상품 추천 없음 */} - + + {!isZeroCandidates && } + {isCandidateSelecting && }
    ); diff --git a/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.module.scss b/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.module.scss index 2780bd0a..2cf284eb 100644 --- a/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.module.scss +++ b/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.module.scss @@ -1,7 +1,8 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { margin-bottom: 70px; + position: relative; &__title { @include typography(titleLarge); @@ -9,3 +10,11 @@ margin-bottom: 15px; } } + +.dimmedOverlay { + position: absolute; + width: 100%; + height: 30rem; + background: #ffffff80; + z-index: 3; +} diff --git a/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.tsx b/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.tsx index 0cf42f7c..dbbb66a1 100644 --- a/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.tsx +++ b/src/components/Vote/VoteContent/VoteRecommendList/VoteRecommendList.tsx @@ -1,25 +1,21 @@ -import { Navigation } from "swiper/modules"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/scss"; -import "swiper/scss/navigation"; +import {Navigation} from 'swiper/modules'; +import {Swiper, SwiperSlide} from 'swiper/react'; +import 'swiper/scss'; +import 'swiper/scss/navigation'; -import styles from "./VoteRecommendList.module.scss"; +import styles from './VoteRecommendList.module.scss'; -import VoteRecommendItem from "./VoteRecommendItem/VoteRecommendItem"; +import VoteRecommendItem from './VoteRecommendItem/VoteRecommendItem'; // 후보지&여행지 X -> 상품 추천 없음 -const VoteRecommendList = ({ state }: { state: string }) => { +const VoteRecommendList = ({state, isCandidateSelecting}: {state: string; isCandidateSelecting: boolean}) => { return (
    + {isCandidateSelecting &&
    }

    이런 카페는 어때요?

    - + diff --git a/src/components/Vote/VoteHeader/VoteHeader.module.scss b/src/components/Vote/VoteHeader/VoteHeader.module.scss index 7134ae13..f8032a67 100644 --- a/src/components/Vote/VoteHeader/VoteHeader.module.scss +++ b/src/components/Vote/VoteHeader/VoteHeader.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { width: 100%; @@ -42,4 +42,8 @@ gap: 12px; font-size: 2.2rem; + + & > *:disabled { + color: $neutral300; + } } diff --git a/src/components/Vote/VoteHeader/VoteHeader.tsx b/src/components/Vote/VoteHeader/VoteHeader.tsx index bc7efeb7..52dd3a63 100644 --- a/src/components/Vote/VoteHeader/VoteHeader.tsx +++ b/src/components/Vote/VoteHeader/VoteHeader.tsx @@ -3,15 +3,19 @@ import {BsThreeDots} from 'react-icons/bs'; import {MdOutlineArrowBackIosNew} from 'react-icons/md'; import {RiMap2Line} from 'react-icons/ri'; import {useLocation, useNavigate} from 'react-router-dom'; +import {useRecoilValue} from 'recoil'; import styles from './VoteHeader.module.scss'; +import {isCandidateSelectingState} from '@/recoil/vote/alertModal'; + import {VoteHeaderProps} from '@/types/vote'; -const VoteHeader = ({onBottomSlideOpen, title, isNoCandidate}: VoteHeaderProps) => { +const VoteHeader = ({onBottomSlideOpen, title, isZeroCandidates}: VoteHeaderProps) => { const navigate = useNavigate(); const location = useLocation(); const path = location.pathname.split('/')[3]; + const isCandidateSelecting = useRecoilValue(isCandidateSelectingState); const setRightIcons = (path: string) => { switch (path) { @@ -26,10 +30,13 @@ const VoteHeader = ({onBottomSlideOpen, title, isNoCandidate}: VoteHeaderProps) default: return ( <> - - diff --git a/src/components/VoteMemo/MemoContent.tsx b/src/components/VoteMemo/MemoContent.tsx index ad8629cc..bd9c68cf 100644 --- a/src/components/VoteMemo/MemoContent.tsx +++ b/src/components/VoteMemo/MemoContent.tsx @@ -1,30 +1,24 @@ -import {useEffect} from 'react'; -import {useSetRecoilState} from 'recoil'; - import styles from './MemoContent.module.scss'; -import {selectedCandidatesState} from '@/recoil/vote/candidateList'; - import MemoItem from './MemoItem/MemoItem'; -import {VoteInfo} from '@/types/vote'; +import {TaglineType, VoteInfo} from '@/types/vote'; const MemoContent = ({data}: {data: VoteInfo}) => { - const setSelectedCandidates = useSetRecoilState(selectedCandidatesState); const candidates = data.candidates; - const CheckAllCandidates = () => { - const newCandidateIds = candidates.map((candidate) => candidate.id); - setSelectedCandidates(new Set(newCandidateIds)); - }; - - useEffect(() => { - CheckAllCandidates(); - }, []); + const getExistingTaglines = localStorage.getItem('recoil-persist'); + const existingTaglines: TaglineType[] = getExistingTaglines && JSON.parse(getExistingTaglines).selectedTaglineState; return (
    - {candidates?.map((candidate) => )} + {candidates?.map((candidate) => ( + tagline.placeId === candidate.placeInfo.placeId)} + /> + ))}
    ); }; diff --git a/src/components/VoteMemo/MemoItem/MemoItem.tsx b/src/components/VoteMemo/MemoItem/MemoItem.tsx index 798e8d31..1ce907ba 100644 --- a/src/components/VoteMemo/MemoItem/MemoItem.tsx +++ b/src/components/VoteMemo/MemoItem/MemoItem.tsx @@ -1,49 +1,79 @@ import {Checkbox} from '@chakra-ui/react'; -import {useState} from 'react'; +import {useCallback, useEffect, useState} from 'react'; import styles from './MemoItem.module.scss'; -import useGetSelectedCandidates from '@/hooks/useGetSelectedCandidates'; +import {useDebounce} from '@/hooks/useDebounce'; +import useGetSelectedArray from '@/hooks/useGetSelectedArray'; -import {CandidatesInfo} from '@/types/vote'; +import {selectedTaglineState} from '@/recoil/vote/voteMemo'; -const MemoItem = ({candidate}: {candidate: CandidatesInfo}) => { - const [text, setText] = useState(0); +import {MemoItemProps} from '@/types/vote'; - const {addCandidateInSelectedList} = useGetSelectedCandidates(); +const MemoItem = ({candidate, existingTagline}: MemoItemProps) => { + const [text, setText] = useState(''); + // const [selectedTagline, setSelectedTagline] = useRecoilState(selectedTaglineState); + const {toggleItemInNewArray, setMemoArray} = useGetSelectedArray(selectedTaglineState); + const debouncedText = useDebounce(text, 500); + const placeInfo = candidate.placeInfo; + + useEffect(() => { + if (existingTagline) { + setText(existingTagline.tagline); + } + }, []); + + const handleCheckboxChange = () => { + toggleItemInNewArray({ + placeId: placeInfo.placeId, + tagline: debouncedText, + }); + }; + + const handleDebouncedTextChange = useCallback(() => { + setMemoArray({ + placeId: placeInfo.placeId, + tagline: debouncedText, + }); + }, [debouncedText, placeInfo.placeId, setMemoArray]); + + useEffect(() => { + handleDebouncedTextChange(); + }, [debouncedText]); return (
    addCandidateInSelectedList(candidate.id)} + onChange={handleCheckboxChange} />
    -
    +
    +