diff --git a/src/features/bookmark/hook/usePostMovieBookMark.ts b/src/features/bookmark/hook/usePostMovieBookMark.ts index 45035fb..668661f 100644 --- a/src/features/bookmark/hook/usePostMovieBookMark.ts +++ b/src/features/bookmark/hook/usePostMovieBookMark.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-toastify' import { postMovieBookMark } from '@/features/bookmark/lib' import { LOCAL_QUERY_KEY } from '@/shared/constants' @@ -13,10 +14,12 @@ export function UsePostMovieBookMark(movieId: string) { return { isLike: !prevData.isLike } }) }, - onError: () => { - queryClient.invalidateQueries({ - queryKey: LOCAL_QUERY_KEY.movieBookMark(movieId), - }) + onError: error => { + queryClient + .invalidateQueries({ + queryKey: LOCAL_QUERY_KEY.movieBookMark(movieId), + }) + .then(() => toast.error(error.message)) }, }) } diff --git a/src/features/profile/hooks/index.ts b/src/features/profile/hooks/index.ts index 1478ae8..18ed922 100644 --- a/src/features/profile/hooks/index.ts +++ b/src/features/profile/hooks/index.ts @@ -1 +1,2 @@ export { useImageOnChange } from './useImageOnChange' +export { usePutUserNickname } from './usePutUserNickname' diff --git a/src/features/profile/hooks/useImageOnChange.ts b/src/features/profile/hooks/useImageOnChange.ts index 87faa6d..5a6d8b0 100644 --- a/src/features/profile/hooks/useImageOnChange.ts +++ b/src/features/profile/hooks/useImageOnChange.ts @@ -2,13 +2,13 @@ import { ChangeEventHandler, useRef, useState } from 'react' -import { UsePutUserProfile } from '@/features/profile/hooks/usePutUserProfile' +import { usePutUserProfile } from './usePutUserProfile' export function useImageOnChange() { const [imgFile, setImageFile] = useState(null) const imgRef = useRef(null) - const { mutateAsync } = UsePutUserProfile() + const { mutateAsync } = usePutUserProfile() const onChange: ChangeEventHandler = () => { if (!imgRef?.current || !imgRef.current.files) return diff --git a/src/features/profile/hooks/usePutUserNickname.ts b/src/features/profile/hooks/usePutUserNickname.ts new file mode 100644 index 0000000..145a40e --- /dev/null +++ b/src/features/profile/hooks/usePutUserNickname.ts @@ -0,0 +1,29 @@ +import { useMutation } from '@tanstack/react-query' +import { useSession } from 'next-auth/react' + +import { putUserNickname } from '@/features/profile/lib' + +type FetchFUNC = { + formData: FormData + nickname?: string +} + +export function usePutUserNickname() { + const { data: session, update } = useSession() + + return useMutation({ + mutationFn: ({ formData }: FetchFUNC) => putUserNickname(formData), + onMutate: async ({ nickname }: FetchFUNC) => { + const prevUser = session?.user + if (!prevUser) return + await update({ nickname }) + return { prevUser } + }, + onError: async (_1, _2, context) => { + await update({ ...context?.prevUser }) + }, + onSettled: async () => { + await update() + }, + }) +} diff --git a/src/features/profile/hooks/usePutUserProfile.ts b/src/features/profile/hooks/usePutUserProfile.ts index ee0610f..6f1d2b5 100644 --- a/src/features/profile/hooks/usePutUserProfile.ts +++ b/src/features/profile/hooks/usePutUserProfile.ts @@ -8,7 +8,7 @@ type FetchFUNC = { url: string } -export function UsePutUserProfile() { +export function usePutUserProfile() { const { data: session, update } = useSession() return useMutation({ diff --git a/src/features/profile/lib/index.ts b/src/features/profile/lib/index.ts index 61ab6ee..e3a2b8d 100644 --- a/src/features/profile/lib/index.ts +++ b/src/features/profile/lib/index.ts @@ -1 +1,3 @@ +export { putUserNickname } from './putUserNickname' +export { putUserPassword } from './putUserPassword' export { putUserProfile } from './putUserProfile' diff --git a/src/features/profile/lib/putUserNickname.ts b/src/features/profile/lib/putUserNickname.ts new file mode 100644 index 0000000..b7246ce --- /dev/null +++ b/src/features/profile/lib/putUserNickname.ts @@ -0,0 +1,14 @@ +export const putUserNickname = async (formData: FormData) => { + const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/nickname`, { + method: 'PUT', + body: formData, + credentials: 'include', + cache: 'no-store', + }) + + if (!res.ok) { + throw new Error('Failed to fetch data') + } + + return res.json() +} diff --git a/src/features/profile/lib/putUserPassword.ts b/src/features/profile/lib/putUserPassword.ts new file mode 100644 index 0000000..97184f8 --- /dev/null +++ b/src/features/profile/lib/putUserPassword.ts @@ -0,0 +1,14 @@ +export const putUserPassword = async (formData: FormData) => { + const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/password`, { + method: 'PUT', + body: formData, + credentials: 'include', + cache: 'no-store', + }) + + if (!res.ok) { + throw new Error('Failed to fetch data') + } + + return res.json() +} diff --git a/src/features/profile/schema/index.ts b/src/features/profile/schema/index.ts new file mode 100644 index 0000000..a81ff23 --- /dev/null +++ b/src/features/profile/schema/index.ts @@ -0,0 +1,2 @@ +export { nicknameFiledSchema } from './nickname-filed-schema' +export { passwordFiledSchema } from './password-filed-schema' diff --git a/src/features/profile/schema/nickname-filed-schema.ts b/src/features/profile/schema/nickname-filed-schema.ts new file mode 100644 index 0000000..57f8b39 --- /dev/null +++ b/src/features/profile/schema/nickname-filed-schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +import { nicknameSchema } from '@/features/auth/schema' + +export const nicknameFiledSchema = z.object({ + nickname: nicknameSchema, +}) diff --git a/src/features/profile/schema/password-filed-schema.ts b/src/features/profile/schema/password-filed-schema.ts new file mode 100644 index 0000000..f5e24c8 --- /dev/null +++ b/src/features/profile/schema/password-filed-schema.ts @@ -0,0 +1,14 @@ +import { string, z } from 'zod' + +import { passwordSchema } from '@/features/auth/schema' + +export const passwordFiledSchema = z + .object({ + prevPassword: string().optional(), + password: passwordSchema, + confirmPassword: passwordSchema, + }) + .refine(data => data.password === data.confirmPassword, { + message: '비밀번호가 일치하지 않습니다.', + path: ['confirmPassword'], + }) diff --git a/src/features/profile/ui/UserEmailFiled.tsx b/src/features/profile/ui/UserEmailFiled.tsx new file mode 100644 index 0000000..53e70c3 --- /dev/null +++ b/src/features/profile/ui/UserEmailFiled.tsx @@ -0,0 +1,16 @@ +import { Input, InputLabel } from '@mui/material' + +import styles from './user-change-filed.module.scss' + +type Props = { + email: string +} + +export function UserEmailFiled({ email }: Props) { + return ( + + 이메일 + + + ) +} diff --git a/src/features/profile/ui/UserNickNameChangeFiled.tsx b/src/features/profile/ui/UserNickNameChangeFiled.tsx new file mode 100644 index 0000000..150e10b --- /dev/null +++ b/src/features/profile/ui/UserNickNameChangeFiled.tsx @@ -0,0 +1,65 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { Input, InputLabel } from '@mui/material' +import Button from '@mui/material/Button' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'react-toastify' +import { z } from 'zod' + +import { nicknameCheck } from '@/features/auth/lib' +import { usePutUserNickname } from '@/features/profile/hooks' +import { nicknameFiledSchema } from '@/features/profile/schema' + +import styles from './user-change-filed.module.scss' + +export function UserNickNameChangeFiled() { + const { mutateAsync } = usePutUserNickname() + + const onSubmit: SubmitHandler> = async data => { + try { + const { nickname } = data + await nicknameCheck(nickname) + const formData = new FormData() + formData.append('nickname', nickname) + await mutateAsync({ formData, nickname }) + toast.success('닉네임이 변경되었습니다.') + } catch (err) { + if (err instanceof Error) { + console.error(err.message) + } + } + } + + const { + handleSubmit, + register, + formState: { errors }, + } = useForm>({ + resolver: zodResolver(nicknameFiledSchema), + defaultValues: { + nickname: '', + }, + }) + + return ( + <> +
+ + 닉네임 +
+ + +
+
+
+ {errors['nickname'] && {errors['nickname']?.message}} + + ) +} diff --git a/src/features/profile/ui/UserPasswordChangeFiled.tsx b/src/features/profile/ui/UserPasswordChangeFiled.tsx new file mode 100644 index 0000000..b8377dc --- /dev/null +++ b/src/features/profile/ui/UserPasswordChangeFiled.tsx @@ -0,0 +1,96 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { Input, InputLabel } from '@mui/material' +import Button from '@mui/material/Button' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'react-toastify' +import { z } from 'zod' + +import { nicknameCheck } from '@/features/auth/lib' +import { putUserPassword } from '@/features/profile/lib' +import { passwordFiledSchema } from '@/features/profile/schema' + +import styles from './user-change-filed.module.scss' + +export function UserPasswordChangeFiled() { + const onSubmit: SubmitHandler> = async data => { + try { + const { prevPassword, password } = data + if (typeof prevPassword !== 'string') return + await nicknameCheck(password) + const formData = new FormData() + formData.append('prevPassword', prevPassword) + formData.append('password', password) + await nicknameCheck(password) + await putUserPassword(formData) + toast.success('비밀번호가 변경되었습니다.') + reset() + } catch (err) { + if (err instanceof Error) { + toast.error(err.message) + } + } + } + + const { + handleSubmit, + register, + formState: { errors }, + reset, + } = useForm>({ + resolver: zodResolver(passwordFiledSchema), + defaultValues: { + prevPassword: '', + password: '', + confirmPassword: '', + }, + }) + + return ( + <> +
+ + 이전 비밀번호 + + + + 새 비밀번호 + + + + 비밀번호 확인 +
+ + +
+
+
+ {errors.prevPassword ? ( + {errors.prevPassword.message} + ) : errors.password ? ( + {errors.password.message} + ) : errors.confirmPassword ? ( + {errors.confirmPassword.message} + ) : null} + + ) +} diff --git a/src/features/profile/ui/index.ts b/src/features/profile/ui/index.ts index 54a6a7c..c0bc297 100644 --- a/src/features/profile/ui/index.ts +++ b/src/features/profile/ui/index.ts @@ -1 +1,4 @@ export { ProfileImageChangeButton } from './ProfileImageChangeButton' +export { UserEmailFiled } from './UserEmailFiled' +export { UserNickNameChangeFiled } from './UserNickNameChangeFiled' +export { UserPasswordChangeFiled } from './UserPasswordChangeFiled' diff --git a/src/features/profile/ui/user-change-filed.module.scss b/src/features/profile/ui/user-change-filed.module.scss new file mode 100644 index 0000000..efe6585 --- /dev/null +++ b/src/features/profile/ui/user-change-filed.module.scss @@ -0,0 +1,38 @@ +.labelName { + display: inline-block; + width: 150px; + color: #8e95a9; +} + +.input { + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + transition: 0.5s; + &:focus { + background-color: #fffffe !important; + outline: none; + } + + &:disabled { + background-color: #101820 !important; + } + + &::placeholder { + color: #8e95a9; + } +} + +.inputButton { + display: flex; + gap: 10px; +} + +.changeButton { + border-radius: 5px; +} + +.errorMessage { + color: red; + margin-left: 50px; +} \ No newline at end of file diff --git a/src/features/search/ui/HeaderSearchBar.tsx b/src/features/search/ui/HeaderSearchBar.tsx index 12b44ed..2d1b3ae 100644 --- a/src/features/search/ui/HeaderSearchBar.tsx +++ b/src/features/search/ui/HeaderSearchBar.tsx @@ -25,7 +25,6 @@ export function HeaderSearchBar() { try { searchKeywordSchema.parse(keyword) - router.push(SITE_PATH.search(keyword)) } catch (error) { if (error instanceof ZodError) { diff --git a/src/widgets/profile/RightProfileSection.tsx b/src/widgets/profile/RightProfileSection.tsx index aac2910..f415348 100644 --- a/src/widgets/profile/RightProfileSection.tsx +++ b/src/widgets/profile/RightProfileSection.tsx @@ -1,11 +1,12 @@ 'use client' -import { Input, InputLabel } from '@mui/material' -import Button from '@mui/material/Button' import { notFound } from 'next/navigation' import { useSession } from 'next-auth/react' +import { UserEmailFiled } from '@/features/profile/ui' import { ProfileImageChangeButton } from '@/features/profile/ui/ProfileImageChangeButton' +import { UserNickNameChangeFiled } from '@/features/profile/ui/UserNickNameChangeFiled' +import { UserPasswordChangeFiled } from '@/features/profile/ui/UserPasswordChangeFiled' import { getImageWithDefault } from '@/shared/util' import { ProfileTitle } from '@/widgets/profile/ProfileTitle' @@ -29,36 +30,9 @@ export function ProfileBody() {
- - 이메일 - - - - 닉네임 -
- - -
-
- - 이전 비밀번호 - - - - 새 비밀번호 - - - - 비밀번호 확인 -
- - -
-
+ + +
diff --git a/src/widgets/profile/right-profile-section.module.scss b/src/widgets/profile/right-profile-section.module.scss index 8a28032..11a5ea1 100644 --- a/src/widgets/profile/right-profile-section.module.scss +++ b/src/widgets/profile/right-profile-section.module.scss @@ -45,12 +45,6 @@ gap: 10px; } -.imageBox { - position: relative; - width: 200px; - height: 200px; -} - .changeButton { border-radius: 5px; }