Skip to content

Commit

Permalink
feat: 비밀번호 변경 API 연결 (#196)
Browse files Browse the repository at this point in the history
* feat: 비밀번호 변경 api 구현

* feat: 비밀번호 변경 api 로그인 정보 및 유저 정보 확인

* feat: 변경 성공 시 새로운 accessToken 쿠키 설정

* refactor: 이전 비밀번호와 같은 비밀번호 입력 시 새로운 에러메세지 출력

* fix: 비밀번호 확인 시 에러메세지 표시되지 않는 버그 수정

* refactor: state 줄이고 if return 문으로 구조 변경

* fix: package-lock.json 제거

* refactor: newPasswordForm 컴포넌트 react-hook-form으로 리팩토링

* refactor: 비밀번호 확인 유효성 검사 로직 수정 및 api 호출부 함수 분리

* refactor : mutation으로 api 호출 후 동작 위임, 맞춰서 PostChangePassword api 수정

* fix: import 오류 fix, 비밀번호 정규식 통일

* fix: api error throw 삭제

* fix: 사용하지 않는 import 삭제

* fix: userState 사용하는 로직 삭제
  • Loading branch information
jonique98 authored Jun 23, 2024
1 parent ec959f0 commit 04b905e
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 87 deletions.
2 changes: 1 addition & 1 deletion src/home/apis/getUserPasswordMatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AxiosError } from 'axios';

import { authClient } from '@/apis';
import { AuthErrorData } from '@/home/types/Auth.type';
import { GetPasswordResponse } from '@/home/types/GetPassword.type';
import { GetPasswordResponse } from '@/home/types/password.type';
import { api } from '@/service/TokenService';

interface getPasswordProps {
Expand Down
23 changes: 23 additions & 0 deletions src/home/apis/postChangePassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { authClient } from '@/apis';
import { SessionTokenType } from '@/home/types/password.type';

import { PostAuthSignInData } from '../types/Auth.type';

interface changePasswordProps {
email: string;
newPassword: string;
sessionToken: SessionTokenType;
}

export const postChangePassword = async ({
email,
sessionToken,
newPassword,
}: changePasswordProps): Promise<PostAuthSignInData> => {
const res = await authClient.post('/auth/change-password', {
email: email,
newPassword: newPassword,
sessionToken: sessionToken.sessionToken,
});
return res.data;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BoxButton, PasswordTextField } from '@yourssu/design-system-react';

import { useCurrentPasswordForm } from '@/home/components/ChangePasswordContents/CurrentPasswordForm/useCurrentPasswordForm';
import { SessionTokenType } from '@/home/types/GetPassword.type';
import { SessionTokenType } from '@/home/types/password.type';

import {
StyledInputContainer,
Expand All @@ -14,6 +14,7 @@ import {
interface CurrentPasswordFormProps {
onConfirm: () => void;
setSessionToken: ({ sessionToken }: SessionTokenType) => void;
setPreviousPassword: (password: string) => void;
}

export const CurrentPasswordForm = (props: CurrentPasswordFormProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import { useRecoilValue } from 'recoil';

import { getUserPasswordMatch } from '@/home/apis/getUserPasswordMatch';
import { LogInState } from '@/home/recoil/LogInState';
import { SessionTokenType } from '@/home/types/GetPassword.type';
import { SessionTokenType } from '@/home/types/password.type';

interface CurrentPasswordFormProps {
onConfirm: () => void;
setSessionToken: ({ sessionToken }: SessionTokenType) => void;
setPreviousPassword: (password: string) => void;
}

export const useCurrentPasswordForm = ({
onConfirm,
setSessionToken,
setPreviousPassword,
}: CurrentPasswordFormProps) => {
const [currentPassword, setCurrentPassword] = useState('');
const [isError, setIsError] = useState(false);
Expand All @@ -23,7 +25,6 @@ export const useCurrentPasswordForm = ({

const checkCurrentPassword = async () => {
if (!isLoggedIn) {
alert('로그인이 필요합니다.');
navigate('/Login');
return;
}
Expand All @@ -35,6 +36,7 @@ export const useCurrentPasswordForm = ({
if (data) {
setIsError(false);
setSessionToken(data as SessionTokenType);
setPreviousPassword(currentPassword);
onConfirm();
} else if (error) setIsError(true);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BoxButton, PasswordTextField } from '@yourssu/design-system-react';

import { useNewPasswordForm } from '@/home/components/ChangePasswordContents/NewPasswordForm/useNewPasswordForm';
import { SessionTokenType } from '@/home/types/GetPassword.type';
import { NewPasswordFormProps } from '@/home/types/password.type';

import {
StyledInputContainer,
Expand All @@ -12,25 +12,11 @@ import {
StyledInputAnimation,
} from './NewPasswordForm.style';

interface NewPasswordFormProps {
sessionToken: SessionTokenType;
}
export const NewPasswordForm = (props: NewPasswordFormProps) => {
const { isFirstRender, register, passwordValidate, errors, onSubmit } = useNewPasswordForm(props);

export const NewPasswordForm = ({ sessionToken }: NewPasswordFormProps) => {
const {
newPassword,
newPasswordCheck,
isNewPasswordError,
isNewPasswordCheckError,
isFirstRender,
validationAttempted,
setNewPasswordCheck,
handleNewPasswordChange,
handleSubmit,
} = useNewPasswordForm(sessionToken);

const isNewPasswordFieldNegative = !isFirstRender && isNewPasswordError;
const isRepeatPasswordFieldNegative = isNewPasswordCheckError && validationAttempted;
const isInvalidPassword = !isFirstRender && !!errors.newPassword;
const isValidPassword = !isFirstRender && !errors.newPassword;

return (
<StyledBoxContainer>
Expand All @@ -39,24 +25,18 @@ export const NewPasswordForm = ({ sessionToken }: NewPasswordFormProps) => {
<StyledInputTitle>새로운 비밀번호를 입력해주세요.</StyledInputTitle>
<PasswordTextField
placeholder="숫자, 영문자, 특수문자 조합으로 8자 이상 입력해주세요"
value={newPassword}
onChange={(e) => handleNewPasswordChange(e.target.value)}
isNegative={isNewPasswordFieldNegative}
helperLabel={
isNewPasswordFieldNegative
? '숫자, 영문자, 특수문자 조합으로 8자 이상 입력해주세요'
: ''
}
{...register('newPassword', {
validate: passwordValidate,
})}
isNegative={isInvalidPassword}
helperLabel={isInvalidPassword ? (errors.newPassword?.message as string) : ''}
/>
<StyledInputAnimation
className={!isNewPasswordError && newPassword.length >= 8 ? 'active' : ''}
>
<StyledInputAnimation className={isValidPassword ? 'active' : ''}>
<StyledInputTitle>비밀번호를 한번 더 입력해주세요.</StyledInputTitle>
<PasswordTextField
value={newPasswordCheck}
onChange={(e) => setNewPasswordCheck(e.target.value)}
isNegative={isRepeatPasswordFieldNegative}
helperLabel={isRepeatPasswordFieldNegative ? '비밀번호가 일치하지 않습니다.' : ''}
{...register('newPasswordCheck')}
isNegative={!!errors.newPasswordCheck}
helperLabel={errors.newPasswordCheck?.message as string | undefined}
/>
</StyledInputAnimation>
</StyledInputContainer>
Expand All @@ -65,8 +45,8 @@ export const NewPasswordForm = ({ sessionToken }: NewPasswordFormProps) => {
rounding={8}
size="large"
variant="filled"
onClick={handleSubmit}
disabled={isFirstRender || isNewPasswordError}
onClick={onSubmit}
disabled={!isValidPassword}
>
변경하기
</BoxButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,69 +1,81 @@
import { useState, useEffect } from 'react';

import { hasNumberAndEnglishWithSymbols } from '@yourssu/utils';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';

import { SessionTokenType } from '@/home/types/GetPassword.type';
import { api } from '@/service/TokenService';
import { useGetUserData } from '@/home/hooks/useGetUserData';
import { usePostChangePassword } from '@/home/hooks/usePostChangePassword';
import { LogInState } from '@/home/recoil/LogInState';
import { NewPasswordFormProps } from '@/home/types/password.type';

export const useNewPasswordForm = (sessionToken: SessionTokenType) => {
const [newPassword, setNewPassword] = useState('');
const [newPasswordCheck, setNewPasswordCheck] = useState('');
const [isNewPasswordError, setIsNewPasswordError] = useState(false);
const [isNewPasswordCheckError, setIsNewPasswordCheckError] = useState(false);
const [isFirstRender, setIsFirstRender] = useState(true);
const [validationAttempted, setValidationAttempted] = useState(false);
export const useNewPasswordForm = (props: NewPasswordFormProps) => {
const { sessionToken, previousPassword } = props;
const [isFirstRender, setIsFirstRender] = useState<boolean>(true);
const {
register,
watch,
formState: { errors },
getValues,
setValue,
setError,
} = useForm({
mode: 'onChange',
});

const isLoggedIn = useRecoilValue(LogInState);
const { data: currentUser } = useGetUserData();
const navigate = useNavigate();
const postChangePassword = usePostChangePassword();

const newPassword = watch('newPassword');

const regexp = new RegExp('^(?=.*[a-zA-Z])(?=.*[0-9]).{8,}$');
useEffect(() => {
setIsFirstRender(!newPassword || newPassword.length < 8);
}, [newPassword]);

const handleNewPasswordChange = (password: string) => {
setNewPassword(password);
if (password.length >= 8) {
setIsFirstRender(false);
setIsNewPasswordError(!regexp.test(password));
} else {
setIsNewPasswordError(true);
const passwordValidate = (newPassword: string) => {
if (newPassword === previousPassword) {
setValue('newPasswordCheck', '');
return '현재 비밀번호와 다른 비밀번호를 입력해주세요.';
}

if (hasNumberAndEnglishWithSymbols(newPassword) && newPassword.length <= 100) return true;

setValue('newPasswordCheck', '');
return '숫자, 영문자, 특수문자 조합으로 8자 이상 입력해주세요.';
};

const handleSubmit = () => {
if (sessionToken === null) {
const onSubmit = () => {
const newPasswordCheck = getValues('newPasswordCheck');

if (newPassword !== newPasswordCheck) {
setError('newPasswordCheck', {
type: 'manual',
message: '비밀번호가 일치하지 않습니다.',
});
return;
}

setValidationAttempted(true);
const isValid = regexp.test(newPassword);
if (isValid && newPassword === newPasswordCheck) {
const accessToken = api.getAccessToken();
if (!accessToken) {
alert('로그인이 필요합니다.');
navigate('/Login');
return;
}
setIsNewPasswordCheckError(false);
if (!isLoggedIn || !currentUser) {
navigate('/Login');
return;
}

if (!isValid) setIsNewPasswordError(true);
if (newPassword !== newPasswordCheck) setIsNewPasswordCheckError(true);
postChangePassword.mutate({
email: currentUser.email,
newPassword,
sessionToken,
});
};

useEffect(() => {
if (newPassword.length < 8) {
setIsNewPasswordCheckError(false);
setNewPasswordCheck('');
setValidationAttempted(false);
}
}, [newPassword]);

return {
newPassword,
newPasswordCheck,
isNewPasswordError,
isNewPasswordCheckError,
isFirstRender,
validationAttempted,
setNewPasswordCheck,
handleNewPasswordChange,
handleSubmit,
register,
onSubmit,
passwordValidate,
errors,
};
};
21 changes: 21 additions & 0 deletions src/home/hooks/usePostChangePassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';

import { postChangePassword } from '@/home/apis/postChangePassword';
import { api } from '@/service/TokenService';

export const usePostChangePassword = () => {
const navigate = useNavigate();

return useMutation({
mutationFn: postChangePassword,
onSuccess: (data) => {
api.setAccessToken(data.accessToken, data.accessTokenExpiredIn);
api.setRefreshToken(data.refreshToken, data.refreshTokenExpiredIn);
navigate('/mypage');
},
onError: () => {
navigate('/Login');
},
});
};
7 changes: 6 additions & 1 deletion src/home/pages/ChangePassword/ChangePassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ChangePasswordFunnelStepsType = '현재비밀번호입력' | '새비밀번
export const ChangePassword = () => {
const [Funnel, setStep] = useFunnel<ChangePasswordFunnelStepsType>('현재비밀번호입력');
const [sessionToken, setSessionToken] = useState<SessionTokenType | null>(null);
const [previousPassword, setPreviousPassword] = useState<string>('');

return (
<ChangePasswordFrame>
Expand All @@ -19,10 +20,14 @@ export const ChangePassword = () => {
<CurrentPasswordForm
onConfirm={() => setStep('새비밀번호입력')}
setSessionToken={setSessionToken}
setPreviousPassword={setPreviousPassword}
/>
</Funnel.Step>
<Funnel.Step name="새비밀번호입력">
<NewPasswordForm sessionToken={sessionToken as SessionTokenType} />
<NewPasswordForm
sessionToken={sessionToken as SessionTokenType}
previousPassword={previousPassword}
/>
</Funnel.Step>
</Funnel>
</ChangePasswordFrame>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export interface GetPasswordResponse {
data?: SessionTokenType;
error?: AxiosError<AuthErrorData>;
}

export interface NewPasswordFormProps {
sessionToken: SessionTokenType;
previousPassword: string;
}

0 comments on commit 04b905e

Please sign in to comment.