diff --git a/pages/profile/edit/index.tsx b/pages/profile/edit/index.tsx index 1d31143..716ee31 100644 --- a/pages/profile/edit/index.tsx +++ b/pages/profile/edit/index.tsx @@ -1,165 +1,29 @@ -import styled from "@emotion/styled"; -import TextInput from "@components/common/TextInput"; -import patchProfile from "src/api/patchProfile"; -import Image from "next/image"; -import getProfile from "src/api/getProfile"; -import { DefaultHeader } from "@components/common/DefaultHeader"; -import { HeaderBackButton } from "@components/common/HeaderBackButton"; -import { DefaultButton } from "@components/common/DefaultButton"; -import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; -import { API_GET_PROFILE_KEY } from "src/api/getProfile"; -import { postUploadImage } from "src/api/postUploadImage"; -import { ChangeEvent } from "react"; -import { useState } from "react"; -import { useRef } from "react"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/router"; +import ProfileEditController from '@components/profile/edit/ProfileEditController'; +import { GetServerSideProps, NextPage } from 'next'; -interface ProfileEditForm { - imgFile: FileList; - nickname: string; -} +interface ReviewEditPageProps {} -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - margin: 0 auto; - padding: 45px; - min-height: calc(100vh - 80px); - width: 100%; - max-width: 768px; -`; - -const Main = styled.div` - display: flex; - width: 400px; - height: calc(100% - 76px); - overflow-y: auto; - flex-direction: column; -`; - -const Form = styled.form` - display: flex; - flex-direction: column; - justify-content: space-between; -`; - -const ImageContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 200px; - font-size: 40px; - cursor: pointer; - margin-bottom: 100px; -`; - -const Profile = () => { - const router = useRouter(); - const queryClient = useQueryClient(); - const inputFileRef = useRef(null); - - const [localImgUrl, setLocalImgUrl] = useState( - "/images/profile/profile.png" - ); - - const { data } = useQuery({ - queryKey: [API_GET_PROFILE_KEY], - queryFn: () => getProfile(), - }); - - const { register, handleSubmit } = useForm(); - - const { mutate: patchProfileMutate } = useMutation({ - mutationFn: patchProfile, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [API_GET_PROFILE_KEY], - }); - router.replace("/profile"); - }, - }); - - const { mutateAsync: postUploadImageMutate } = useMutation({ - mutationFn: postUploadImage, - }); +const ProfileEditPage: NextPage = () => { + return ; +}; - const handleChangeThumbnail = (event: ChangeEvent) => { - event.preventDefault(); - const file = event.target.files?.[0]; - if (file && file.type.startsWith("image/")) { - const reader = new FileReader(); - reader.onload = (e: ProgressEvent) => { - const target = e.target as FileReader; - setLocalImgUrl(target.result as string); - }; - reader.readAsDataURL(file); - } - }; +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; - const handleClickImage = () => { - inputFileRef.current?.click(); - }; + const refreshToken = req.cookies.refreshToken; - const onSubmitPartyForm = async (formData: ProfileEditForm) => { - const { nickname, imgFile } = formData; - if (imgFile) { - const uploadResult = await postUploadImageMutate(imgFile[0]); - if (uploadResult.imgUrl) { - patchProfileMutate({ imgUrl: uploadResult.imgUrl, nickname }); - } - return; + if (!refreshToken) { + return { + redirect: { + permanent: false, + destination: '/signin', + }, + }; } - patchProfileMutate({ nickname }); - }; - - if (!data) { - return; - } - return ( - - } - rightArea={ - - } - /> -
-
- - profile - - - - -
-
- ); + return { + props: {}, + }; }; -export default Profile; +export default ProfileEditPage; diff --git a/src/api/postUploadImage.ts b/src/api/postUploadImage.ts index 67e4142..2ce36fd 100644 --- a/src/api/postUploadImage.ts +++ b/src/api/postUploadImage.ts @@ -1,18 +1,18 @@ -import defaultRequest from "src/lib/axios/defaultRequest"; +import defaultRequest from 'src/lib/axios/defaultRequest'; export interface SetImageResponse { - imgUrl: string; + imgUrl: string; } -export const postUploadImage = async (image: File) => - ( - await defaultRequest.post( - "/api/image", - { image }, - { - headers: { - "Content-Type": "multipart/form-data", +export const postUploadImage = async (image: File): Promise => { + const { data } = await defaultRequest.post( + '/api/image', + { image }, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, }, - } - ) - ).data; + ); + return data; +}; diff --git a/src/components/profile/edit/ProfileEditController.tsx b/src/components/profile/edit/ProfileEditController.tsx new file mode 100644 index 0000000..fd588a6 --- /dev/null +++ b/src/components/profile/edit/ProfileEditController.tsx @@ -0,0 +1,106 @@ +import styled from '@emotion/styled'; +import { yupResolver } from '@hookform/resolvers/yup'; +import useToast from '@hooks/useToast'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { useCallback } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import getProfile, { API_GET_PROFILE_KEY } from 'src/api/getProfile'; +import patchProfile from 'src/api/patchProfile'; +import { postUploadImage } from 'src/api/postUploadImage'; +import * as yup from 'yup'; +import ProfileEditScreen from './ProfileEditScreen'; + +export interface ProfileEditForm { + nickname: string; + image: string | File; +} + +const Form = styled.form` + display: flex; + flex-direction: column; +`; + +const ProfileEditController = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + + const { showToast } = useToast(); + + const { data } = useQuery({ + queryKey: [API_GET_PROFILE_KEY], + queryFn: () => getProfile(), + }); + + const formSchema = yup.object({ + nickname: yup + .string() + .min(1, '닉네임을 입력해주세요.') + .max(10, '10자를 넘기지 말아주세요.') + .required(), + image: yup.mixed().required(), + }); + + const form = useForm({ + resolver: yupResolver(formSchema), + values: { + nickname: data?.nickname || '', + image: data?.imgUrl || '', + }, + }); + + const { handleSubmit } = form; + + const { mutate: patchProfileMutate } = useMutation({ + mutationFn: patchProfile, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [API_GET_PROFILE_KEY], + }); + router.replace('/profile'); + }, + onError: (error) => { + console.error(error); + return Promise.reject(error); + }, + }); + + const { mutateAsync: postUploadImageMutate } = useMutation({ + mutationFn: postUploadImage, + onError: (error) => { + console.error(error); + return Promise.reject(error); + }, + }); + + const onSubmit = useCallback( + async (formData: ProfileEditForm) => { + const { nickname, image } = formData; + let imgUrl = image; + if (imgUrl && typeof imgUrl !== 'string') { + const uploadResult = await postUploadImageMutate(imgUrl); + imgUrl = uploadResult.imgUrl; + } + const payload = { nickname, imgUrl }; + patchProfileMutate(payload); + }, + [postUploadImageMutate, patchProfileMutate], + ); + + const onSubmitError = (errors: Object) => { + for (const error of Object.values(errors)) { + showToast(error.message); + break; + } + }; + + return ( + +
+ + +
+ ); +}; + +export default ProfileEditController; diff --git a/src/components/profile/edit/ProfileEditScreen.tsx b/src/components/profile/edit/ProfileEditScreen.tsx new file mode 100644 index 0000000..b52cb06 --- /dev/null +++ b/src/components/profile/edit/ProfileEditScreen.tsx @@ -0,0 +1,85 @@ +import { DefaultButton } from '@components/common/DefaultButton'; +import { DefaultHeader } from '@components/common/DefaultHeader'; +import { HeaderBackButton } from '@components/common/HeaderBackButton'; +import TextInput from '@components/common/TextInput'; +import styled from '@emotion/styled'; +import Image from 'next/image'; +import defaultProfileImage from 'public/images/profile/profile.png'; +import { ChangeEventHandler, useCallback } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { ProfileEditForm } from './ProfileEditController'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; + min-height: calc(100vh - 80px); + width: 100%; + max-width: 768px; + margin-top: 80px; +`; + +const InputLabel = styled.label` + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + width: 100%; + max-width: 400px; +`; +const UserImage = styled(Image)` + border-radius: 50%; + object-fit: cover; + cursor: pointer; +`; + +const Input = styled.input` + display: none; +`; + +const ProfileEditScreen = () => { + const { register, setValue, control } = useFormContext(); + + const image = useWatch({ control, name: 'image' }); + + const getImageUrl = useCallback((src: File | string) => { + if (typeof src === 'string' || !src) return src; + return URL.createObjectURL(src); + }, []); + + const onChangeHandler = useCallback>( + (event) => { + const file = event.currentTarget.files?.[0]; + if (!file) return; + setValue('image', file); + }, + [getImageUrl, setValue], + ); + + return ( + + } + rightArea={} + /> + + + + + + + ); +}; + +export default ProfileEditScreen;