Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 회원가입 api 연결 및 동작 처리 구현 #181

Merged
merged 10 commits into from
Sep 23, 2024
7 changes: 4 additions & 3 deletions src/api/email.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { END_POINT } from '@/constants/api';
import { ServerResponse } from '@/types/api';
import { MemberForm } from '@/types/member';
import { axiosInstance } from './interceptor';

export const postEmailVerify = async (
value: Pick<MemberForm, 'email' | 'verificationCode'>,
) => {
const { data } = await axiosInstance.post<string>(
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.EMAIL_VERIFY}`,
value,
);
Expand All @@ -14,15 +15,15 @@ export const postEmailVerify = async (
};

export const postEmailRequest = async (email: string) => {
const { data } = await axiosInstance.post<string>(
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.EMAIL_REQUEST}`,
email,
);
return data;
};

export const getFindPw = async (email: string) => {
const { data } = await axiosInstance.post<string>(
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.FIND_PW}`,
email,
);
Expand Down
42 changes: 18 additions & 24 deletions src/api/member.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { END_POINT } from '@/constants/api';
import { ServerResponse } from '@/types/api';
import { Member, MemberForm } from '@/types/member';
import { axiosInstance } from './interceptor';

const URL = process.env.API_URL;

export const fetchMembers = async (): Promise<Member[]> => {
const response = await fetch(`${URL}/members`);
const result = (await response.json()) as Member[];
return result;
};

export const fetchMember = async (memberId: number): Promise<Member> => {
const response = await fetch(`${URL}/members/${memberId}`);
const result = (await response.json()) as Member;
return result;
};

export const getMember = async (memberId: number) => {
const { data } = await axiosInstance.get<Member>(
`${END_POINT.MEMBER(memberId)}`,
Expand All @@ -31,9 +18,9 @@ export const getMemberProfile = async (memberId: number) => {
};

export const postMember = async (
form: Pick<MemberForm, 'nickname' | 'email' | 'password'>,
form: Pick<MemberForm, 'nickname' | 'email' | 'password' | 'role'>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role 매개변수의 역할이 혹시 유저와 어드민 이렇게 이후에 나누어질 구분에 대한 것인지 궁금합니당🧐

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원 가입 api에서 body값으로 role이라는 값을 넣어주어야 해서 추가해주었습니다! 아마 말씀하신 것처럼 차후에 유저와 어드민을 구분하기 위해서 기능을 미리 만들어 두신 게 아닐까 싶네요🤔

) => {
const { data } = await axiosInstance.post<string>(
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.SIGN_UP}`,
form,
);
Expand All @@ -47,15 +34,15 @@ export const deleteMember = async (
data: form,
};

const { data } = await axiosInstance.delete<string>(
const { data } = await axiosInstance.delete<ServerResponse>(
`${END_POINT.ALL_MEMBERS}`,
params,
);
return data;
};

export const putMemberImage = async (profilePhoto: string) => {
const { data } = await axiosInstance.put<string>(
const { data } = await axiosInstance.put<ServerResponse>(
`${END_POINT.MEMBER_IMAGE}`,
profilePhoto,
{
Expand All @@ -68,7 +55,7 @@ export const putMemberImage = async (profilePhoto: string) => {
};

export const putMemberNickname = async (nickname: string) => {
const { data } = await axiosInstance.put<string>(
const { data } = await axiosInstance.put<ServerResponse>(
`${END_POINT.MEMBER_NICKNAME}`,
nickname,
{
Expand All @@ -81,7 +68,7 @@ export const putMemberNickname = async (nickname: string) => {
};

export const putMemberPw = async (pw: string) => {
const { data } = await axiosInstance.put<string>(
const { data } = await axiosInstance.put<ServerResponse>(
`${END_POINT.MEMBER_PASSWORD}`,
pw,
{
Expand All @@ -96,22 +83,29 @@ export const putMemberPw = async (pw: string) => {
export const postLogin = async (
form: Pick<MemberForm, 'email' | 'password'>,
) => {
const { data } = await axiosInstance.post<string>(`${END_POINT.LOGIN}`, form);
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.LOGIN}`,
form,
);
return data;
};

export const postLogout = async () => {
const { data } = await axiosInstance.post<string>(`${END_POINT.LOGOUT}`);
const { data } = await axiosInstance.post<ServerResponse>(
`${END_POINT.LOGOUT}`,
);
return data;
};

export const getRefreshToken = async () => {
const { data } = await axiosInstance.get<string>(`${END_POINT.REFRESH}`);
const { data } = await axiosInstance.get<ServerResponse>(
`${END_POINT.REFRESH}`,
);
return data;
};

export const getNicknameVerify = async (nickname: string) => {
const { data } = await axiosInstance.get<string>(
const { data } = await axiosInstance.get<ServerResponse>(
`${END_POINT.NICKNAME_VERIFY}`,
{
params: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ export const profileImageText = (isError: boolean) =>
css(typo.Comment.Regular, {
color: isError ? color.RED : color.GY[1],
});

export const defaultProfileModalcenterStyling = css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '1000',
});
27 changes: 23 additions & 4 deletions src/components/molecules/InputProfileImage/InputProfileImage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import Label from '@/components/atoms/Label/Label';
import ProfileSignUp from '@/components/atoms/ProfileSetting/ProfileSetting';
import ProfileSetting from '@/components/atoms/ProfileSetting/ProfileSetting';
import {
InputProfileImageProps,
useCheckProfileImage,
} from '@/hooks/common/inputsUserInfo/useCheckProfileImage';
import React, { useState } from 'react';
import DefaultProfileModal from '../DefaultProfileModal/DefaultProfileModal';
import {
defaultProfileModalcenterStyling,
profileDefaultText,
profileImageSelectContainer,
profileImageText,
Expand All @@ -18,10 +20,20 @@ const InputProfileImage = ({
}: InputProfileImageProps) => {
const { imageSrc, isError, getRootProps, handleDefaultImage } =
useCheckProfileImage({ setProfilePhoto, imgSrc });

const [defaultProfileModalOpen, setDefaultProfileModalOpen] =
useState<boolean>(false);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useState의 경우 기본 타입 추정 값이 false로 알고 있는데, lint 관련 설정 때문에 까지 추가 하신 것인지 궁금합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 모달 동작 부분을 하나의 함수로 처리해도 좋을 것 같다는 의견도 제시봅시다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint 관련 설정보다는 명시적으로 초기화해주는 것이 좋아 위와 같이 코드를 작성했습니다 :)
useState<boolean>() 이렇게 해도 되나 싶어서 방금 해보았더니, false로 초기값을 주지 않는다면 undefined값으로 초기화되어 에러가 발생하네요🥲

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 모달 동작 부분을 하나의 함수로 처리해도 좋을 것 같다 를 제가 잘 이해하지 못해서 혹시 예시를 들어주실 수 있을까요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 의도는 이미지가 선택된 후 자동으로 모달 창이 닫도록 동시에 처리 하는게 어떨까 하는 코멘트였는데, 이러한 의도로 모달 open 상태 관리 + 이미지 선택 부분을 하나의 함수로 처리해도 좋다는 의견을 제시하였습니다.
다만, 현재 모달에 닫기 버튼이 따로 출력시키기로 하였고, 해당 동작 둘을 동시에 처리하는 것은 기획 의도에도 맞지 않아 제 코멘트 스스로 반려 하도록 하겠습니닷 😢

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+) useState 관련해서는 희선님의 의도가 더 적절하신 거 같네요! 오히려 제가 하나 더 배워갑니다! 확인해주셔서 정말 감사드려요 👍

const handleSelectDefaultImage = (selectedImage: string | null) => {
if (selectedImage) {
handleDefaultImage(selectedImage);
}
setDefaultProfileModalOpen(false);
};
return (
<div css={profileImageSelectContainer}>
<Label>프로필 사진(선택)</Label>
<ProfileSignUp
<ProfileSetting
isSetting={!!imageSrc}
src={imageSrc}
{...getRootProps()}
Expand All @@ -30,14 +42,21 @@ const InputProfileImage = ({
<button
type="button"
css={profileDefaultText}
onClick={handleDefaultImage}
onClick={() => setDefaultProfileModalOpen(true)}
>
기본 이미지로 프로필 설정하기
</button>
<span css={profileImageText(isError)}>
3MB 이하의 사진만 가능합니다.
</span>
</div>
<div css={defaultProfileModalcenterStyling}>
<DefaultProfileModal
isOpen={defaultProfileModalOpen}
onSelect={handleSelectDefaultImage}
onClose={() => setDefaultProfileModalOpen(false)}
/>
</div>
</div>
);
};
Expand Down
6 changes: 6 additions & 0 deletions src/components/molecules/InputPw/InputPw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const InputPw = ({ value, onChange, onSuccessChange }: InputPwProps) => {
const { inputRef, isError, errorMessage, handleVerify } =
useCheckPassword(value);

useEffect(() => {
if (isError) {
handleVerify();
}
}, [value]);

WonJuneKim marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
if (value && onSuccessChange) {
onSuccessChange('password', !isError);
Expand Down
6 changes: 6 additions & 0 deletions src/components/molecules/InputPwCheck/InputPwCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const InputPwCheck = ({
const { inputRef, isError, errorMessage, handleVerify } =
useCheckPasswordCheck({ value, pw });

useEffect(() => {
if (isError) {
handleVerify();
}
}, [value]);

useEffect(() => {
if (value && onSuccessChange) {
onSuccessChange('passwordCheck', !isError);
Expand Down
32 changes: 32 additions & 0 deletions src/constants/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const DEFAULT_PROFILE_URL = {
OCTOPUS: {
CLIENT: '/octopus-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/2c132b00-e37a-4f71-bbf2-e064bd181915_octopus_profile.jpg',
WonJuneKim marked this conversation as resolved.
Show resolved Hide resolved
},
JELLYFISH: {
CLIENT: '/jellyfish-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/8fcc5073-d977-4b91-a4d1-e242e3eb2efa_jellyfilsh_profile.jpg',
},
RAY: {
CLIENT: '/ray-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/0c6c471d-420a-4632-ae9b-20be5a52097c_ray_profile.jpg',
},
EEL: {
CLIENT: '/eel-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/873fad2f-1cf5-4429-9cd0-626656b894d3_eel_profile.jpg',
},
TURTLE: {
CLIENT: '/turtle-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/4239508d-f9b3-4a3e-91be-3777024ea05a_turtle_profile.jpg',
},
RABBIT: {
CLIENT: '/rabbit-profile.png',
SERVER:
'https://picko-image.s3.ap-northeast-2.amazonaws.com/member/68f736cf-0f65-4f12-bb44-4f54c448eddc_rabbit_profile.jpg',
},
};
8 changes: 1 addition & 7 deletions src/hooks/api/member/useSignUpMutation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { useNavigate } from 'react-router-dom';
import { AxiosErrorResponse } from '@/api/interceptor';
import { postMember } from '@/api/member';
import { useMutation } from '@tanstack/react-query';
import { PATH } from '@/constants/path';

export const useSignUpMutation = () => {
const navigate = useNavigate();
return useMutation({
mutationFn: postMember,
onSuccess: () => {
navigate(`/${PATH.LOGIN}`);
alert('회원가입에 성공했습니다😀');
},
onSuccess: () => {},
onError: (err: AxiosErrorResponse) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원가입 후 로그인페이지로 이동하도록 로직이 설정되어있다고 하셨는데 제거하신 이유가 따로 있을까용?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원가입 성공 시 로직은 아래 파일로 이동해두었습니다!

// hooks\signup\useSignupForm.ts
const { mutate: signup } = useSignUpMutation();

const handleSubmit = (e: ChangeEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (isAllTrue(successForm)) {
      const newForm = createNewForm(form);
      signup(newForm, {
        onSuccess: () => {
          setSignupSuccess(true);
          setTimeout(() => {
            navigate(`/${PATH.LOGIN}`);
          }, 2000);
        },
      });
    } else {
      focus(e);
    }
  };

위와 같이 옮겨둔 이유는 회원 가입 성공 토스트 메시지 창이 2초간 띄워진 이후로 로그인 페이지로 이동해야 했기 때문입니당

const [signupSuccess, setSignupSuccess] = useState<boolean>(false);

signupSuccess 변수를 통해서 토스트 메시지 창을 띄울지 말지 여부를 제어하는 데 이 로직을 useSignUpMutation 훅에 넣기에는 해당 훅의 관심사에 맞지 않다고 판단했습니다🤔 이 훅은 다른 컴포넌트를 직접 제어하지 않는 것이 원칙이라고 생각해서 회원가입 로직을 전체적으로 관리하는 useSignupForm 훅으로 옮겨두었습니다!

console.error(err);
},
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/common/inputsUserInfo/useCheckEmail.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getFindPw, postEmailRequest } from '@/api/email';
import { AxiosErrorResponse } from '@/api/interceptor';
import { HTTP_STATUS_CODE } from '@/constants/api';
import { ERROR, SUCCESS } from '@/constants/message';
import { PATH } from '@/constants/path';
import { isEmptyString } from '@/utils/validator';
import { useMutation } from '@tanstack/react-query';
import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ERROR, SUCCESS } from '../../../constants/message';
import { isEmptyString } from '../../../utils/validator';

export const useCheckEmail = (type: string, value: string) => {
const inputRef = useRef<HTMLInputElement | null>(null);
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/common/inputsUserInfo/useCheckNickname.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AxiosErrorResponse } from '@/api/interceptor';
import { getNicknameVerify } from '@/api/member';
import { HTTP_STATUS_CODE } from '@/constants/api';
import { ERROR, SUCCESS } from '@/constants/message';
import { isEmptyString } from '@/utils/validator';
import { useMutation } from '@tanstack/react-query';
import { useRef, useState } from 'react';
import { ERROR, SUCCESS } from '../../../constants/message';
import { isEmptyString } from '../../../utils/validator';

export const useCheckNickname = (value: string) => {
const inputRef = useRef<HTMLInputElement | null>(null);
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/common/inputsUserInfo/useCheckPassword.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INPUT_LIMIT } from '@/constants/input';
import { ERROR } from '@/constants/message';
import { isEmptyString } from '@/utils/validator';
import { useRef, useState } from 'react';
import { ERROR } from '../../../constants/message';
import { isEmptyString } from '../../../utils/validator';

export const useCheckPassword = (value: string) => {
const inputRef = useRef<HTMLInputElement | null>(null);
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/common/inputsUserInfo/useCheckPasswordCheck.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ERROR } from '@/constants/message';
import { isEmptyString } from '@/utils/validator';
import { useRef, useState } from 'react';
import { isEmptyString } from '../../../utils/validator';
import { ERROR } from '../../../constants/message';

interface CheckPwChkProps {
value: string;
Expand Down
Loading
Loading