Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 20 additions & 156 deletions pages/profile/edit/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(null);

const [localImgUrl, setLocalImgUrl] = useState<string>(
"/images/profile/profile.png"
);

const { data } = useQuery({
queryKey: [API_GET_PROFILE_KEY],
queryFn: () => getProfile(),
});

const { register, handleSubmit } = useForm<ProfileEditForm>();

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<ReviewEditPageProps> = () => {
return <ProfileEditController />;
};

const handleChangeThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const file = event.target.files?.[0];
if (file && file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
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 (
<Container>
<DefaultHeader
leftArea={<HeaderBackButton />}
rightArea={
<DefaultButton
text="완료"
onClick={handleSubmit(onSubmitPartyForm)}
/>
}
/>
<Main>
<Form onSubmit={handleSubmit(onSubmitPartyForm)}>
<ImageContainer onClick={handleClickImage}>
<Image
width={200}
height={200}
src={data.imgUrl || localImgUrl}
style={{ borderRadius: "50%", objectFit: "cover" }}
alt="profile"
/>
<input
id="profile-tumbnail-input"
type="file"
hidden
{...register("imgFile", {
onChange: handleChangeThumbnail,
})}
ref={inputFileRef}
/>
</ImageContainer>
<TextInput
placeholder="닉네임을 입력해주세요."
isBorderRadius={false}
maxLength={20}
{...register("nickname")}
defaultValue={data.nickname}
/>
</Form>
</Main>
</Container>
);
return {
props: {},
};
};

export default Profile;
export default ProfileEditPage;
26 changes: 13 additions & 13 deletions src/api/postUploadImage.ts
Original file line number Diff line number Diff line change
@@ -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<SetImageResponse> => {
const { data } = await defaultRequest.post(
'/api/image',
{ image },
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
}
)
).data;
);
return data;
};
106 changes: 106 additions & 0 deletions src/components/profile/edit/ProfileEditController.tsx
Original file line number Diff line number Diff line change
@@ -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<string | File>().required(),
});

const form = useForm<ProfileEditForm>({
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 (
<FormProvider {...form}>
<Form onSubmit={handleSubmit(onSubmit, onSubmitError)}>
<ProfileEditScreen />
</Form>
</FormProvider>
);
};

export default ProfileEditController;
Loading