diff --git a/frontend/src/features/member/components/NicknameChangingModal.tsx b/frontend/src/features/member/components/NicknameChangingModal.tsx new file mode 100644 index 00000000..bd543ca4 --- /dev/null +++ b/frontend/src/features/member/components/NicknameChangingModal.tsx @@ -0,0 +1,73 @@ +import styled from 'styled-components'; +import Modal from '@/shared/components/Modal/Modal'; +import Spacing from '@/shared/components/Spacing'; + +interface NicknameChangingModalProps { + isOpen: boolean; + nickname: string | undefined; + closeModal: () => void; + onSubmitNickname: () => void; +} + +const NicknameChangingModal = ({ + isOpen, + nickname, + closeModal, + onSubmitNickname, +}: NicknameChangingModalProps) => { + if (!nickname) return; + + return ( + + {`닉네임 변경 시 다시 로그인을 해야합니다.\n닉네임을 변경하시겠습니까?`} + + {`변경 후 닉네임: ${nickname}`} + + + + + + 취소 + + + 변경 + + + + ); +}; + +export default NicknameChangingModal; + +const ModalContent = styled.div` + font-size: 16px; + line-height: 1.8; + color: ${({ theme }) => theme.color.subText}; + white-space: pre-line; +`; + +const NicknameContent = styled.div` + color: ${({ theme }) => theme.color.white}; +`; + +const Button = styled.button` + height: 36px; + color: ${({ theme: { color } }) => color.white}; + border-radius: 10px; +`; + +const ConfirmButton = styled(Button)` + flex: 1; + background-color: ${({ theme: { color } }) => color.primary}; +`; + +const CancelButton = styled(Button)` + flex: 1; + background-color: ${({ theme: { color } }) => color.secondary}; +`; + +const ButtonContainer = styled.div` + display: flex; + gap: 16px; + width: 100%; +`; diff --git a/frontend/src/features/member/constants/nickname.ts b/frontend/src/features/member/constants/nickname.ts new file mode 100644 index 00000000..4cd4a5d4 --- /dev/null +++ b/frontend/src/features/member/constants/nickname.ts @@ -0,0 +1,2 @@ +export const MIN_LENGTH_NICKNAME = 2; +export const MAX_LENGTH_NICKNAME = 20; diff --git a/frontend/src/features/member/hooks/useNickname.ts b/frontend/src/features/member/hooks/useNickname.ts new file mode 100644 index 00000000..cd0d1ef6 --- /dev/null +++ b/frontend/src/features/member/hooks/useNickname.ts @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import { MAX_LENGTH_NICKNAME, MIN_LENGTH_NICKNAME } from '@/features/member/constants/nickname'; +import { updateNickname } from '@/features/member/remotes/nickname'; +import ROUTE_PATH from '@/shared/constants/path'; +import { useMutation } from '@/shared/hooks/useMutation'; + +const useNickname = () => { + const { user, logout } = useAuthContext(); + // TODO: 피드백 반영하여 error throw. 그러나 error 핸들링 반드시 필요 + if (!user) { + throw new Error('현재 user 로그인된 정보가 없습니다.'); + } + + const [nicknameEntered, setNicknameEntered] = useState(user.nickname); + + const [nicknameErrorMessage, setNicknameErrorMessage] = + useState('이전과 다른 닉네임으로 변경해주세요.'); + + const { mutateData: changeNickname } = useMutation(() => + updateNickname(user.memberId, nicknameEntered) + ); + + const navigate = useNavigate(); + + const hasError = nicknameErrorMessage.length !== 0; + const handleChangeNickname: React.ChangeEventHandler = (event) => { + const currentNickname = event.currentTarget.value; + + if (currentNickname.length > MAX_LENGTH_NICKNAME) { + setNicknameErrorMessage('2글자 이상 20글자 이하 문자만 가능합니다.'); + return; + } else if (currentNickname.length < MIN_LENGTH_NICKNAME) { + setNicknameErrorMessage('2글자 이상 20글자 이하 문자만 가능합니다.'); + } else if (currentNickname === user?.nickname) { + setNicknameErrorMessage('이전과 다른 닉네임으로 변경해주세요.'); + } else { + setNicknameErrorMessage(''); + } + + setNicknameEntered(currentNickname); + }; + + const submitNicknameChanged = async () => { + await changeNickname(); + logout(); + navigate(ROUTE_PATH.LOGIN); + }; + + return { + nicknameEntered, + nicknameErrorMessage, + hasError, + handleChangeNickname, + submitNicknameChanged, + setNicknameErrorMessage, + }; +}; + +export default useNickname; diff --git a/frontend/src/features/member/hooks/useWithdrawal.ts b/frontend/src/features/member/hooks/useWithdrawal.ts new file mode 100644 index 00000000..6b6a8977 --- /dev/null +++ b/frontend/src/features/member/hooks/useWithdrawal.ts @@ -0,0 +1,28 @@ +import { useNavigate } from 'react-router-dom'; +import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import { deleteMember } from '@/features/member/remotes/member'; +import ROUTE_PATH from '@/shared/constants/path'; +import { useMutation } from '@/shared/hooks/useMutation'; + +const useWithdrawal = () => { + const navigate = useNavigate(); + const { user, logout } = useAuthContext(); + // TODO: 피드백 반영하여 error throw. 그러나 error 핸들링 반드시 필요 + if (!user) { + throw new Error('현재 user 로그인된 정보가 없습니다.'); + } + + const { mutateData: withdrawMember } = useMutation(() => deleteMember(user.memberId)); + + const handleWithdrawal = async () => { + await withdrawMember(); + logout(); + navigate(ROUTE_PATH.ROOT); + }; + + return { + handleWithdrawal, + }; +}; + +export default useWithdrawal; diff --git a/frontend/src/features/member/remotes/member.ts b/frontend/src/features/member/remotes/member.ts index b9df67e0..a3e4cec0 100644 --- a/frontend/src/features/member/remotes/member.ts +++ b/frontend/src/features/member/remotes/member.ts @@ -1,5 +1,5 @@ import fetcher from '@/shared/remotes'; -export const deleteMember = (memberId: number | undefined) => () => { +export const deleteMember = (memberId: number) => { return fetcher(`/members/${memberId}`, 'DELETE'); }; diff --git a/frontend/src/features/member/remotes/nickname.ts b/frontend/src/features/member/remotes/nickname.ts new file mode 100644 index 00000000..a900c289 --- /dev/null +++ b/frontend/src/features/member/remotes/nickname.ts @@ -0,0 +1,7 @@ +import fetcher from '@/shared/remotes'; + +export const updateNickname = (memberId: number, nickname: string) => { + return fetcher(`/members/${memberId}/nickname`, 'PATCH', { + nickname, + }); +}; diff --git a/frontend/src/pages/EditProfilePage.tsx b/frontend/src/pages/EditProfilePage.tsx index 4191e8e0..f6a4d1c7 100644 --- a/frontend/src/pages/EditProfilePage.tsx +++ b/frontend/src/pages/EditProfilePage.tsx @@ -1,47 +1,66 @@ -import { useNavigate } from 'react-router-dom'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import shookshook from '@/assets/icon/shookshook.svg'; -import { useAuthContext } from '@/features/auth/components/AuthProvider'; +import NicknameChangingModal from '@/features/member/components/NicknameChangingModal'; import WithdrawalModal from '@/features/member/components/WithdrawalModal'; -import { deleteMember } from '@/features/member/remotes/member'; +import useNickname from '@/features/member/hooks/useNickname'; +import useWithdrawal from '@/features/member/hooks/useWithdrawal'; import useModal from '@/shared/components/Modal/hooks/useModal'; import Spacing from '@/shared/components/Spacing'; -import ROUTE_PATH from '@/shared/constants/path'; -import { useMutation } from '@/shared/hooks/useMutation'; const EditProfilePage = () => { - const { user, logout } = useAuthContext(); - const { isOpen, openModal, closeModal } = useModal(); - const { mutateData } = useMutation(deleteMember(user?.memberId)); - const navigate = useNavigate(); - - if (!user) { - navigate(ROUTE_PATH.LOGIN); - return; - } - - const handleWithdrawal = async () => { - await mutateData(); - logout(); - navigate(ROUTE_PATH.ROOT); - }; + const { + nicknameEntered, + nicknameErrorMessage, + hasError, + handleChangeNickname, + submitNicknameChanged, + } = useNickname(); + + const { handleWithdrawal } = useWithdrawal(); + + const { + isOpen: isWithdrawalModalOpen, + openModal: openWithdrawalModal, + closeModal: closeWithdrawalModal, + } = useModal(); + + const { + isOpen: isNicknameModalOpen, + openModal: openNicknameModal, + closeModal: closeNicknameModal, + } = useModal(); return ( 프로필 수정 - + - + + + {hasError && {nicknameErrorMessage}} - - -