Skip to content

Commit

Permalink
feat: 프로필 닉네임 & 자기소개 수정 기능을 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
wokbjso authored Aug 28, 2024
2 parents 460a23a + d0ed8e8 commit 5efba10
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 20 deletions.
18 changes: 18 additions & 0 deletions app/api/member/patch/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';

import { fetchData } from '@/apis/fetch-data';
import { ProfileTextEditResponse } from '@/features/profile';

export async function PATCH(request: NextRequest) {
const body = (await request.json()) as Promise<{
nickname?: string;
introduction?: string;
}>;
const data = await fetchData<ProfileTextEditResponse>(
`/member`,
'PATCH',
body,
);

return NextResponse.json(data);
}
2 changes: 2 additions & 0 deletions components/molecules/text-field/form-text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const FormTextField = forwardRef<HTMLInputElement, FormTextFieldProps>(
placeholder,
unit,
className,
maxLength,
wrapperClassName,
absoluteClassName,
subTextClassName,
Expand Down Expand Up @@ -74,6 +75,7 @@ export const FormTextField = forwardRef<HTMLInputElement, FormTextFieldProps>(
name={name}
type={inputType}
placeholder={placeholder}
maxLength={maxLength}
onFocus={() => handlers.onChangeFocus(true)}
onBlur={() => handlers.onChangeFocus(false)}
onChange={handleInputChange}
Expand Down
1 change: 1 addition & 0 deletions features/profile/apis/dto/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './profile-image-presign';
export * from './profile-image-url-done';
export * from './profile-text-edit';
4 changes: 1 addition & 3 deletions features/profile/apis/dto/profile-image-presign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@ interface PresignedImageProps {
presignedUrl: string;
}

export interface ProfileImagePresignedResponse extends Response {
data: PresignedImageProps;
}
export type ProfileImagePresignedResponse = Response<PresignedImageProps>;
6 changes: 5 additions & 1 deletion features/profile/apis/dto/profile-image-url-done.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { Response } from '@/apis';

export interface ProfileImageUrlDoneResponse extends Response {}
export type ProfileImageUrlDoneResponse = Response<{
introduction: string;
memberId: number;
nickname: string;
}>;
7 changes: 7 additions & 0 deletions features/profile/apis/dto/profile-text-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Response } from '@/apis';

export type ProfileTextEditResponse = Response<{
memberId: number;
nickname: string;
introduction: string;
}>;
1 change: 1 addition & 0 deletions features/profile/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './dto';
export * from './fetch-profile-data';
export * from './use-get-profile-image-presigned-url';
export * from './use-profile-image-url-done';
export * from './use-profile-text-edit';
37 changes: 37 additions & 0 deletions features/profile/apis/use-profile-text-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';

import { ProfileTextEditResponse } from './dto';

async function profileTextEdit(data: {
nickname?: string;
introduction?: string;
}): Promise<ProfileTextEditResponse> {
const res = await fetch('/api/member/patch', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return res.json();
}

export function useProfileTextEdit(id?: number) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: profileTextEdit,
onSuccess: async () => {
await queryClient.refetchQueries({
queryKey: ['currentMember'],
});
await queryClient.refetchQueries({
queryKey: ['profileData', String(id)],
});
},
});
}
96 changes: 88 additions & 8 deletions features/profile/components/organisms/profile-edit-form.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,129 @@
'use client';

import { useRouter } from 'next/navigation';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';

import { useImagePresignUrl } from '@/apis';
import { Button } from '@/components/atoms';
import { useCurrentMemberInfo, useToast } from '@/hooks';
import { css } from '@/styled-system/css';
import { flex } from '@/styled-system/patterns';
import { getBlobData } from '@/utils';

import {
useGetProfileImagePresignedUrl,
useProfileImageUrlDone,
useProfileTextEdit,
} from '../../apis';
import { useProfileEditForm } from '../../hooks';
import { useProfileData, useProfileEditForm } from '../../hooks';
import { ProfileEditImageSection } from './profile-edit-image-section';
import { ProfileEditTextInfoSection } from './profile-edit-text-info-section';

interface ProfileEditFormProps {
nickname?: string;
introduce?: string;
nickname: string;
introduction: string;
}

//Todo: 한줄 소개 현재 글자 수 세는 UI 추가
export function ProfileEditForm() {
const router = useRouter();

const methods = useForm<ProfileEditFormProps>({
defaultValues: {},
});
const { toast } = useToast();

const { imageFile, defaultProfileIndex, handlers } = useProfileEditForm();

const { data: currrentMemberData } = useCurrentMemberInfo();
const { data: profileData } = useProfileData(currrentMemberData?.data.id);
const { mutateAsync: getProfileImagePresignedUrl } =
useGetProfileImagePresignedUrl();
const { mutateAsync: imagePresign } = useImagePresignUrl();
const { mutateAsync: profileImageUrlDone } = useProfileImageUrlDone();
const { mutateAsync: profileTextEdit } = useProfileTextEdit(
currrentMemberData?.data.id,
);

const handleProfileImageEditSuccess = (
hasTextEditData: boolean,
memberId: number,
) => {
if (!hasTextEditData) {
handlers.onChangeIsLoading(false);
toast('프로필이 수정되었어요.');
router.push(`/profile/${memberId}`);
}
};

//Todo: 성공 처리 구체화
const handleProfileTextEditSuccess = (memberId: number) => {
handlers.onChangeIsLoading(false);
toast('프로필이 수정되었어요.');
router.push(`/profile/${memberId}`);
};

//Todo: 에러 처리 구체화
const handleProfileEditError = () => {
alert('프로필 수정 중 오류가 발생하였습니다.');
handlers.onChangeIsLoading(false);
return;
};

//Todo: 닉네임 & 소개 수정 api 연결
//Todo: 각 상황에 맞는 이미지 api 연결(디폴트 캐릭터 프로필 & 직접 선택 프로필)
const handleProfileNicknameBlank = () => {
toast('닉네임을 설정해주세요.', { type: 'error', delay: 1000 });
return;
};

const extractModifiedData = (data: ProfileEditFormProps) => {
const modifiedData: Partial<ProfileEditFormProps> = { ...data };

if (data.nickname?.trim() === profileData?.nickname.trim()) {
delete modifiedData.nickname;
}
if (data.introduction?.trim() === profileData?.introduction.trim()) {
delete modifiedData.introduction;
}

return modifiedData;
};

//Todo: 기본 프로필 api 처리
//Todo: 에러 처리
//Todo: 헤더의 저장버튼 클릭 시에도 수정 로직 수행
//Todo: 이전 프로필 정보 화면에 반영
const onSubmit: SubmitHandler<ProfileEditFormProps> = async (data) => {
console.log(data);
const hasTextEditData = Boolean(
data.nickname?.trim() !== profileData?.nickname.trim() ||
data?.introduction?.trim() !== profileData?.introduction,
);
//사용자가 직접 선택한 사진이 있을 때
if (imageFile) {
const { data } = await getProfileImagePresignedUrl(imageFile.name);
await imagePresign({
presignedUrl: data.presignedUrl,
file: getBlobData(imageFile),
});
await profileImageUrlDone(data.imageName);
const profileImageUrlDoneRes = await profileImageUrlDone(data.imageName);
if (profileImageUrlDoneRes.status === 200)
handleProfileImageEditSuccess(
hasTextEditData,
profileImageUrlDoneRes.data.memberId,
);
else handleProfileEditError();
}
//닉네임 or 자기소개를 수정할 때
if (hasTextEditData) {
//닉네임이 비었을 때
if (data.nickname === '') handleProfileNicknameBlank();
else {
const profileTextEditRes = await profileTextEdit(
extractModifiedData(data),
);
if (profileTextEditRes.status === 200)
handleProfileTextEditSuccess(profileTextEditRes.data.memberId);
else handleProfileEditError();
}
}
};

Expand All @@ -64,7 +142,9 @@ export function ProfileEditForm() {
<ProfileEditTextInfoSection
nickNameLabel="닉네임"
nickNameSubText="14자까지 입력할 수 있어요"
introducePlaceholder="한 줄 소개를 입력해주세요 (수린이 1년차 / 접영 드릴 연습중)"
introductionPlaceholder="한 줄 소개를 입력해주세요 (수린이 1년차 / 접영 드릴 연습중)"
currentNickname={profileData?.nickname.trim()}
currentIntroduction={profileData?.introduction.trim()}
/>
<div className={buttonStyles.layout}>
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';

import { FormTextArea, FormTextField } from '@/components/molecules';
Expand All @@ -8,16 +9,26 @@ import { css } from '@/styled-system/css';
interface ProfileEditTextInfoSectionProps {
nickNameLabel: string;
nickNameSubText: string;
introducePlaceholder: string;
introductionPlaceholder: string;
currentNickname?: string;
currentIntroduction?: string;
}

//Todo: 한줄 소개 현재 글자 수 세는 UI 추가
export function ProfileEditTextInfoSection({
nickNameLabel,
nickNameSubText,
introducePlaceholder,
introductionPlaceholder,
currentNickname,
currentIntroduction,
}: ProfileEditTextInfoSectionProps) {
const { register, control } = useFormContext();
const { register, control, setValue } = useFormContext();
useEffect(() => {
if (currentNickname) setValue('nickname', currentNickname);
if (currentIntroduction) setValue('introduction', currentIntroduction);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentNickname, currentIntroduction]);

return (
<>
<FormTextField
Expand All @@ -34,8 +45,8 @@ export function ProfileEditTextInfoSection({
wrapperClassName={css({ marginBottom: '24px' })}
/>
<FormTextArea
{...register('introduce')}
placeholder={introducePlaceholder}
{...register('introduction')}
placeholder={introductionPlaceholder}
/>
</>
);
Expand Down
7 changes: 4 additions & 3 deletions features/profile/hooks/use-profile-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { ProfileProps } from '@/features/profile';

import { fetchProfileData } from '../apis/fetch-profile-data';

export const useProfileData = (id: number) => {
export const useProfileData = (id?: number) => {
return useQuery<ProfileProps['data']>({
queryKey: ['profileData', id],
queryKey: ['profileData', String(id)],
queryFn: () =>
fetchProfileData(id).then((data) => {
fetchProfileData(Number(id)).then((data) => {
return data.data;
}),
enabled: !!id,
staleTime: Infinity,
});
};
7 changes: 7 additions & 0 deletions features/profile/hooks/use-profile-edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from 'react';
import { ProfileIndexType } from '@/public/images/default-profile';

export function useProfileEditForm() {
const [isLoading, setIsLoading] = useState(false);
const [imageFile, setImageFile] = useState<File>();
const [defaultProfileIndex, setDefaultProfileIndex] =
useState<ProfileIndexType>(0);
Expand All @@ -13,14 +14,20 @@ export function useProfileEditForm() {
setImageFile(file);
};

const onChangeIsLoading = (isLoading: boolean) => {
setIsLoading(isLoading);
};

const onChangeDefaultProfileIndex = (index: ProfileIndexType) => {
setDefaultProfileIndex(index);
};

return {
isLoading,
imageFile,
defaultProfileIndex,
handlers: {
onChangeIsLoading,
onChangeImageFile,
onChangeDefaultProfileIndex,
},
Expand Down

0 comments on commit 5efba10

Please sign in to comment.