Skip to content
Merged
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
18 changes: 16 additions & 2 deletions src/apis/users/queries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { getUser } from '.';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getUser, updateUser } from '.';
import { UpdateUserForm } from './types';

export const useGetUser = () => {
return useQuery({
queryKey: ['user'],
queryFn: () => getUser(),
});
};

export const useUpdateUser = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (params: UpdateUserForm) => {
return updateUser(params);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
},
});
};
19 changes: 9 additions & 10 deletions src/apis/users/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,19 @@ export type SignupFailResponse = FailResponse;

export type SignupResponse = Promise<SignupSuccessResponse | SignupFailResponse>;

type ProfileImageUrl = string | URL | null;
const profileImageUrlSchema = z
.instanceof(File)
.refine((file) => ['image/jpeg', 'image/jpg', 'image/png', 'image/ico'].includes(file.type), {
message: PROFILEEDIT_FORM_ERROR_MESSAGE.IMAGE.TYPE,
})
.refine((file) => file.size < 2 * 1024 * 1024, { message: `2${PROFILEEDIT_FORM_ERROR_MESSAGE.IMAGE.SIZE}` });

export const updateUserFormSchema = z.object({
nickname: z.string().max(PROFILEEDIT_FORM_VALID_LENGTH.NICKNAME.MAX, PROFILEEDIT_FORM_ERROR_MESSAGE.NICKNAME.MAX),
profileImageUrl: z.string(),
profileImageUrl: z.union([z.string().url(), profileImageUrlSchema]).optional().nullable(),
});

export type UpdateUserForm = Omit<z.infer<typeof updateUserFormSchema>, 'profileImageUrl'> & {
profileImageUrl: ProfileImageUrl;
};

const profileImageUrlSchema = z.instanceof(File).refine((file) => ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml', 'image/ico'].includes(file.type), {
message: '지원되지 않는 이미지 파일입니다.',
});
export type UpdateUserForm = z.infer<typeof updateUserFormSchema>;

export const createProfileImageFormSchema = z.object({
image: profileImageUrlSchema,
Expand All @@ -66,7 +65,7 @@ export interface CreateProfileImageForm {
}

export const profileImageUrlResponseSchema = z.object({
profileImageUrl: z.union([z.string(), z.instanceof(URL)]),
profileImageUrl: z.string().url(),
});

export type ProfileImageUrlResponse = z.infer<typeof profileImageUrlResponseSchema>;
7 changes: 7 additions & 0 deletions src/app/(after-login)/mypage/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function loading() {
return (
<div className='p-10'>
<div className='py-6 text-md text-gray-40'>내정보를 불러오는 중입니다.</div>
</div>
);
}
29 changes: 23 additions & 6 deletions src/app/(after-login)/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { redirect } from 'next/navigation';
import { User, userSchema } from '@/apis/users/types';
import PasswordEdit from '@/components/profile/PasswordEdit';
import ProfileEdit from '@/components/profile/ProfileEdit';
import GoBackLink from '@/components/ui/Link/GoBackLink';
import axiosServerHelper from '@/utils/network/axiosServerHelper';
import { safeResponse } from '@/utils/network/safeResponse';

export default function MyPage() {
return (
<div className='mx-10 flex max-w-[670px] flex-col'>
{/* TODO: 돌아가기 컴포넌트 연동 */}
export default async function MyPage() {
const response = await axiosServerHelper<User>('/users/me');
const userData = safeResponse(response.data, userSchema);

if (!userData) {
redirect('/login');
}

<ProfileEdit />
return (
<div className='p-10'>
<div className='mb-8'>
<GoBackLink href='/mydashboard' />
</div>
<div className='grid w-full max-w-[620px] gap-4'>
{/* 프로필 수정 */}
<ProfileEdit user={userData} />

<PasswordEdit />
{/* 비밀번호 수정 */}
<PasswordEdit />
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/dashboard-header/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Profile() {
) : (
data && (
<div ref={menuRef} className='group relative flex cursor-pointer items-center gap-3 leading-none' onClick={() => setIsMenuOpen((prev) => !prev)}>
<Avatar email={data.email} className='transition-shadow group-hover:shadow-sm group-hover:shadow-slate-400' />
<Avatar email={data.email} profileImageUrl={data.profileImageUrl} className='transition-shadow group-hover:shadow-sm group-hover:shadow-slate-400' />
<span className='hidden font-medium md:block'>{data.nickname}</span>

{isMenuOpen && (
Expand Down
73 changes: 50 additions & 23 deletions src/components/profile/PasswordEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
'use client';

import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { Input } from '@/components/ui/Field/Input';
import { zodResolver } from '@hookform/resolvers/zod';
import { isAxiosError } from 'axios';
import { Input } from '@/components/ui/Field/Input';
import { passwordSchema, PutPasswordFormData } from '@/apis/auth/types';
import { logout, putPassword } from '@/apis/auth';
import useAlert from '@/hooks/useAlert';
import { isError } from 'es-toolkit/compat';
import { isAxiosError } from 'axios';
import SubmitButton from '@/components/auth/SubmitButton';
import { useRouter } from 'next/navigation';
import { Card, CardTitle } from '@/components//ui/Card/Card';
import Button from '@/components/ui/Button/Button';
import { getErrorMessage } from '@/utils/errorMessage';

export default function PasswordEdit() {
const {
register,
handleSubmit,
reset,
formState: { errors, isValid, isSubmitting },
formState: { errors, isValid, isDirty, isSubmitting },
} = useForm({
resolver: zodResolver(passwordSchema),
mode: 'onBlur',
Expand All @@ -29,35 +31,60 @@ export default function PasswordEdit() {

const alert = useAlert();
const router = useRouter();
const queryClient = useQueryClient();

const onSubmit = async (putPasswordFormData: PutPasswordFormData) => {
try {
await putPassword(putPasswordFormData);
alert('비밀번호가 변경되었습니다!');
reset();
} catch (error) {
if (isAxiosError(error)) {
if (error?.status === 401) {
await alert('세션이 만료되어 로그인 페이지로 이동합니다.');
await logout();
router.replace('/login');
return;
}

alert(error.response?.data?.message ?? '알 수 없는 오류가 발생했습니다.');
} else alert(isError(error) ? error.message : String(error));
if (isAxiosError(error) && error?.status === 401) {
await alert('세션이 만료되어 로그인 페이지로 이동합니다.');
await logout();
queryClient.invalidateQueries();
router.replace('/login');
} else {
const message = getErrorMessage(error);
alert(message);
}
}
};

const isDisabled = !isDirty || !isValid || isSubmitting;

return (
<div className='flex flex-col gap-4 rounded-2xl bg-white p-6'>
<h2 className='text-2lg font-bold text-gray-70 md:text-2xl'>비밀번호 변경</h2>
<Card>
<CardTitle>비밀번호 변경</CardTitle>
<form onSubmit={handleSubmit(onSubmit)} className='grid gap-6'>
<Input label='현재 비밀번호' placeholder='비밀번호 입력' error={errors.password?.message} required {...register('password')} type='password' />
<Input label='새 비밀번호' placeholder='새 비밀번호 입력' error={errors.newPassword?.message} required {...register('newPassword')} type='password' />
<Input label='새 비밀번호 확인' placeholder='새 비밀번호 입력' error={errors.newPasswordConfirm?.message} required {...register('newPasswordConfirm')} type='password' />
<SubmitButton isValid={isValid} isSubmitting={isSubmitting} text='변경' />
<Input //
type='password'
label='현재 비밀번호'
placeholder='비밀번호 입력'
error={errors.password?.message}
required
{...register('password')}
/>
<Input //
type='password'
label='새 비밀번호'
placeholder='새 비밀번호 입력'
error={errors.newPassword?.message}
required
{...register('newPassword')}
/>
<Input //
type='password'
label='새 비밀번호 확인'
placeholder='새 비밀번호 입력'
error={errors.newPasswordConfirm?.message}
required
{...register('newPasswordConfirm')}
/>
<Button type='submit' disabled={isDisabled}>
변경
</Button>
</form>
</div>
</Card>
);
}
Loading