diff --git a/src/apis/user.ts b/src/apis/user.ts index 677fdf2..bd4e01c 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -8,6 +8,7 @@ import { LoginResponse, MyInfoResponse, UpdateMyInfoRequest, + UpdatePasswordRequest, } from '@/models/user'; import { AlarmResponse, @@ -62,3 +63,7 @@ export const getMyInfoAPI = () => { export const updateMyInfoAPI = (data: UpdateMyInfoRequest) => { return axios.put(PREFIX + '/myPage/setInfo', data); }; + +export const updatePasswordAPI = (data: UpdatePasswordRequest) => { + return axios.put(PREFIX + '/myPage/updatePassword', data); +}; diff --git a/src/components/ProfilePage/Account/Account.tsx b/src/components/ProfilePage/Account/Account.tsx index 78a091d..4175d1a 100644 --- a/src/components/ProfilePage/Account/Account.tsx +++ b/src/components/ProfilePage/Account/Account.tsx @@ -254,7 +254,7 @@ const Account = () => {
{userInfo?.email}
- + )} diff --git a/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx b/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx index 96697ca..6b3f0f7 100644 --- a/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx +++ b/src/components/ProfilePage/Account/ChangePassword/ChangePassword.tsx @@ -2,7 +2,11 @@ import useBoolean from '@/hooks/useBoolean'; import ChangePasswordModal from './ChangePasswordModal'; -const ChangePassword = () => { +type Props = { + onRefresh: () => void; +}; + +const ChangePassword = ({ onRefresh }: Props) => { const [isOpen, , open, close] = useBoolean(false); return ( @@ -18,7 +22,7 @@ const ChangePassword = () => { - {isOpen && } + {isOpen && } ); }; diff --git a/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx index 9d09526..8444d89 100644 --- a/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx +++ b/src/components/ProfilePage/Account/ChangePassword/ChangePasswordModal.tsx @@ -1,11 +1,160 @@ +import { useMemo, useState } from 'react'; + +import CloseIcon from '@/assets/icons/close.svg?react'; +import TransformationIcon from '@/assets/icons/transformation.svg?react'; + import { Modal } from '@/components/common'; +import { ModalBox } from '@/styles/ProfilePage'; + +import { ValidatePassword, validatePassword } from '@/utils/validation'; +import { updatePasswordAPI } from '@/apis/user'; + type Props = { onClose: () => void; + onRefresh: () => void; }; -const ChangePasswordModal = ({ onClose }: Props) => { - return asdf; +const ChangePasswordModal = ({ onClose, onRefresh }: Props) => { + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const [validateNewPassword, setValidateNewPassword] = + useState(null); + const [validateConfirmPassword, setValidateConfirmPassword] = useState(true); + + const isDisabledSubmitButton = useMemo(() => { + return !( + oldPassword !== '' && + validateNewPassword?.ALPHA_UPPER && + validateNewPassword?.LENGTH && + validateNewPassword?.NUMBER && + validateNewPassword?.SPECIAL_CHAR && + validateConfirmPassword + ); + }, [oldPassword, validateNewPassword, validateConfirmPassword]); + + const handleChangeNewPassword: React.ChangeEventHandler = ({ + target: { value }, + }) => { + setNewPassword(value); + setValidateNewPassword(validatePassword(value)); + setValidateConfirmPassword(value === newPassword); + }; + + const handleChangeConfirmPassword: React.ChangeEventHandler< + HTMLInputElement + > = ({ target: { value } }) => { + setConfirmPassword(value); + setValidateConfirmPassword(value === newPassword); + }; + + const getHelpStyle = (test: undefined | boolean) => { + if (test === undefined) return ''; + return test ? 'active' : 'error'; + }; + + const handleClickSubmitButton = async () => { + try { + await updatePasswordAPI({ + old_password: oldPassword, + new_password: newPassword, + confirm_password: confirmPassword, + }); + } catch (e) { + console.error(e); + } + + onClose(); + onRefresh(); + }; + + return ( + + +
+
+ +
+ +
+ + +

비밀번호 변경

+
+
+ +
+
+ 기존 비밀번호 + + setOldPassword(e.target.value)} + /> +
+ +
+ 비밀번호 + + + +
+ + *8자 이상으로 입력 + + + + *대문자 사용 + + + + *숫자 사용 + + + + *특수문자 사용 + +
+
+ +
+ 비밀번호 재입력 + + + +
+ {validateConfirmPassword ? ( + 비밀번호 확인을 위해 다시 한 번 입력해주세요 + ) : ( + + 비밀번호가 일치하지 않아요 다시 입력해주세요 + + )} +
+
+ + +
+
+
+ ); }; export default ChangePasswordModal; diff --git a/src/components/ProfilePage/LogoutModal/LogoutModal.tsx b/src/components/ProfilePage/LogoutModal/LogoutModal.tsx index e3ff822..a7a076e 100644 --- a/src/components/ProfilePage/LogoutModal/LogoutModal.tsx +++ b/src/components/ProfilePage/LogoutModal/LogoutModal.tsx @@ -41,7 +41,9 @@ const LogoutModal = ({ onClose }: Props) => { - + ); diff --git a/src/models/user.ts b/src/models/user.ts index 29f0670..496b5d1 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -39,3 +39,9 @@ export interface UpdateMyInfoRequest { nick_name: string; gender: string; } + +export interface UpdatePasswordRequest { + old_password: string; + new_password: string; + confirm_password: string; +} diff --git a/src/styles/ProfilePage.ts b/src/styles/ProfilePage.ts index 4fce258..6946976 100644 --- a/src/styles/ProfilePage.ts +++ b/src/styles/ProfilePage.ts @@ -250,9 +250,55 @@ export const ModalBox = styled.div` ${(props) => props.theme.typography.Body1}; } } + + & > .input-group { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + + & .input-title { + color: ${(props) => props.theme.color.gray400}; + ${(props) => props.theme.typography.Body1}; + } + + & .input-help { + display: flex; + gap: 4px; + padding-left: 16px; + + & > span { + color: ${(props) => props.theme.color.gray300}; + transition: 0.1s; + ${(props) => props.theme.typography.Body3}; + + &.active { + color: ${(props) => props.theme.color.gray500}; + } + + &.error { + color: ${(props) => props.theme.color.red}; + } + } + } + + & input { + padding: 15px 20px; + width: 100%; + height: 56px; + border-radius: 12px; + border: 1.5px solid ${(props) => props.theme.color.gray200}; + outline: none; + transition: 0.1s; + + &:focus { + border: 2px solid ${(props) => props.theme.color.gray500}; + } + } + } } - & > button { + & button.submit { width: 100%; height: 58px; background-color: ${(props) => props.theme.color.gray500}; @@ -261,6 +307,12 @@ export const ModalBox = styled.div` color: white; cursor: pointer; outline: none; + transition: 0.1s; ${(props) => props.theme.typography.Body1}; + + &:disabled { + background-color: ${(props) => props.theme.color.gray100}; + color: ${(props) => props.theme.color.gray300}; + } } `; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index f59dd6c..3759f83 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,2 +1,20 @@ +export type ValidatePassword = { + ALPHA_UPPER: boolean; + ALPHA_LOWER: boolean; + SPECIAL_CHAR: boolean; + NUMBER: boolean; + LENGTH: boolean; +}; + export const validateNickname = (nickname: string) => - !/^[a-zA-Z가-힣\s]{1,7}$/g.test(nickname); + !/^[a-zA-Z가-힣\d\s]{1,7}$/g.test(nickname); + +export const validatePassword = (password: string): ValidatePassword => { + return { + ALPHA_UPPER: /[A-Z]/g.test(password), + ALPHA_LOWER: /[a-z]/g.test(password), + SPECIAL_CHAR: /[#?!@$%^&*-]/g.test(password), + NUMBER: /\d/g.test(password), + LENGTH: 8 <= password.length && password.length <= 13, + }; +};