Skip to content

Commit

Permalink
Merge pull request #77 from KGU-C-Lab/hotfix/#76
Browse files Browse the repository at this point in the history
프로필 수정 시 비밀번호 초기화 개선
  • Loading branch information
gwansikk authored Mar 25, 2024
2 parents 14cc033 + 896ad28 commit 4d8b5cf
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 90 deletions.
12 changes: 3 additions & 9 deletions apps/member/src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface PatchUserInfoArgs {
body: ProfileData;
multipartFile: FormData | null;
}

// 내 정보
export const getMyProfile = async () => {
const { data } = await server.get<BaseResponse<ProfileData>>({
Expand All @@ -24,21 +25,14 @@ export const patchUserInfo = async ({
body,
multipartFile,
}: PatchUserInfoArgs) => {
let userInfoData;
if (multipartFile) {
const data = await postUploadedFileProfileImage(multipartFile);

userInfoData = {
...body,
imageUrl: data.fileUrl,
};
} else {
userInfoData = body;
body['imageUrl'] = data.fileUrl;
}

const { data } = await server.patch<ProfileData, BaseResponse<string>>({
url: END_POINT.MY_INFO_EDIT(id),
body: userInfoData,
body,
});

return data;
Expand Down
163 changes: 86 additions & 77 deletions apps/member/src/components/my/ProfileSection/ProfileSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent, useState } from 'react';
import { ChangeEvent, useCallback, useState } from 'react';
import { Button } from '@clab/design-system';
import Image from '@components/common/Image/Image';
import Section from '@components/common/Section/Section';
Expand All @@ -17,51 +17,45 @@ interface ProfileSectionProps {
data: ProfileData;
}

interface ProfileType extends ProfileData {
password: string;
passwordCheck: string;
}

const ProfileSection = ({ data }: ProfileSectionProps) => {
const setIsLoggedIn = useSetIsLoggedInStore();
const { userInfoMutate } = useUserInfoMutation();
const { openModal } = useModal();
const { openModal, updateModal } = useModal();
const toast = useToast();

const [profileImage, setProfileImage] = useState<string>(data.imageUrl);
const [isEdit, setIsEdit] = useState<boolean>(false);
const [inputs, setInputs] = useState<ProfileType>({
...data,
const [inputs, setInputs] = useState<ProfileData>(data);
const [newPassword, setNewPassword] = useState({
password: '',
passwordCheck: '',
});

const onClickEdit = () => {
setIsEdit((prev) => {
if (prev) {
const formData = new FormData();
const file = document.getElementById('imageUrl') as HTMLInputElement;
if (file.files?.length) formData.append(FORM_DATA_KEY, file.files[0]);
let newPassword = undefined;
if (inputs.password === inputs.passwordCheck) {
newPassword = inputs.passwordCheck;
} else {
toast({
state: 'error',
message: '비밀번호 변경이 일치하지 않습니다. 다시 확인해주세요.',
});
return prev;
}
userInfoMutate({
id: data.id,
body: {
...inputs,
password: newPassword,
},
multipartFile: file.files?.length ? formData : null,
});
}
return !prev;
const onClickChangePassword = () => {
openModal({
title: '비밀번호 변경',
content: (
<div className="space-y-4">
<Input
id="새로운 비밀번호"
label="새로운 비밀번호"
type="password"
name="password"
onChange={handlePasswordChange}
/>
<Input
id="비밀번호 확인"
label="비밀번호 확인"
type="password"
name="passwordCheck"
onChange={handlePasswordChange}
/>
</div>
),
accept: {
text: '변경하기',
onClick: onClickChangePasswordAccept,
},
});
};

Expand All @@ -79,34 +73,64 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
});
};

const handleInputsChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleIsEditClick = () => {
setIsEdit((prev) => {
if (prev) {
const formData = new FormData();
const file = document.getElementById('imageUrl') as HTMLInputElement;
if (file.files?.length) formData.append(FORM_DATA_KEY, file.files[0]);

userInfoMutate({
id: data.id,
body: inputs,
multipartFile: file.files?.length ? formData : null,
});
}
return !prev;
});
};

const handleInputsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setInputs((prev) => ({ ...prev, [name]: value }));
}, []);

const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewPassword((prev) => ({ ...prev, [name]: value }));
updateModal();
};

const onChangeProfileImage = (e: ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === 2) {
setProfileImage(reader.result as string);
}
};
if (e.target.files && e.target.files.length > 0) {
reader.readAsDataURL(e.target.files[0]);
const onClickChangePasswordAccept = () => {
if (newPassword.password !== newPassword.passwordCheck) {
return toast({
state: 'error',
message: '비밀번호가 일치하지 않습니다.',
});
}
userInfoMutate({
id: data.id,
body: { ...inputs, password: newPassword.password },
multipartFile: null,
});
};

const {
name,
id,
interests,
contact,
email,
address,
password,
passwordCheck,
githubUrl,
} = inputs;
const onChangeProfileImage = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === 2) {
setProfileImage(reader.result as string);
}
};
if (e.target.files && e.target.files.length > 0) {
reader.readAsDataURL(e.target.files[0]);
}
},
[],
);

const { name, id, interests, contact, email, address, githubUrl } = inputs;

return (
<Section>
Expand All @@ -115,10 +139,15 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
<Button
color={isEdit ? 'green' : 'orange'}
size="sm"
onClick={onClickEdit}
onClick={handleIsEditClick}
>
{isEdit ? '저장' : '수정'}
</Button>
{isEdit && (
<Button size="sm" onClick={onClickChangePassword}>
비빌번호 변경
</Button>
)}
<Button color="red" size="sm" onClick={onClickLogout}>
로그아웃
</Button>
Expand Down Expand Up @@ -204,26 +233,6 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
onChange={handleInputsChange}
/>
</div>
<Input
id="비밀번호 변경"
label="비밀번호 변경"
type="password"
name="password"
value={password}
placeholder={password}
disabled={!isEdit}
onChange={handleInputsChange}
/>
<Input
id="비밀번호 변경 확인"
label="비밀번호 변경 확인"
type="password"
name="passwordCheck"
value={passwordCheck}
placeholder={passwordCheck}
disabled={!isEdit}
onChange={handleInputsChange}
/>
</div>
</Section.Body>
</Section>
Expand Down
2 changes: 1 addition & 1 deletion apps/member/src/constants/message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const ERROR_MESSAGE = {
default: '⚠️ 뭔가 잘못됐어요..',
DEFAULT: '오류가 발생했어요. 잠시후에 다시 시도해주세요.',
} as const;

export const COMMUNITY_MESSAGE = {
Expand Down
22 changes: 19 additions & 3 deletions apps/member/src/hooks/common/useModal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useSetModalStore } from '@store/modal';
import { now } from '@utils/date';

interface OpenModalProps {
key?: string;
title?: string;
content: React.ReactNode;
accept?: {
Expand All @@ -15,14 +17,18 @@ interface OpenModalProps {

const useModal = () => {
const setModal = useSetModalStore();

/**
* open modal
*/
const openModal = ({
key = now().toString(),
title = 'C-Lab PLAY',
content,
accept,
cancel,
}: OpenModalProps) => {
setModal({
key,
isOpen: true,
title,
content,
Expand All @@ -33,12 +39,22 @@ const useModal = () => {
},
});
};

/**
* close modal
*/
const closeModal = () => {
setModal((prev) => ({ ...prev, isOpen: false }));
};
/**
* update modal
* - when you need to update modal content
* - ex) when you need to update modal content after user input
*/
const updateModal = () => {
setModal((prev) => ({ ...prev, key: now().toString() }));
};

return { openModal, closeModal };
return { openModal, closeModal, updateModal };
};

export default useModal;
7 changes: 7 additions & 0 deletions apps/member/src/hooks/queries/useUserInfoMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { patchUserInfo } from '@api/member';
import { QUERY_KEY } from '@constants/key';
import useToast from '@hooks/common/useToast';
import { ERROR_MESSAGE } from '@constants/message';

/**
* 회원의 정보를 수정합니다.
*/
export const useUserInfoMutation = () => {
const queryClient = useQueryClient();
const toast = useToast();
Expand All @@ -13,6 +17,9 @@ export const useUserInfoMutation = () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.MY_PROFILE] });
toast({ state: 'success', message: '프로필이 수정되었습니다.' });
},
onError: () => {
toast({ state: 'error', message: ERROR_MESSAGE.DEFAULT });
},
});

return { userInfoMutate: userInfoMutation.mutate };
Expand Down
2 changes: 2 additions & 0 deletions apps/member/src/store/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from 'recoil';

interface ModalState {
key: string;
isOpen: boolean;
title: string;
content: React.ReactNode;
Expand All @@ -23,6 +24,7 @@ interface ModalState {
export const modalState = atom<ModalState>({
key: ATOM_KEY.MODAL,
default: {
key: '',
isOpen: false,
title: '',
content: null,
Expand Down

0 comments on commit 4d8b5cf

Please sign in to comment.