-
Notifications
You must be signed in to change notification settings - Fork 2
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/#480 프로필 편집 페이지에서 닉네임 변경 기능 추가 #490
base: main
Are you sure you want to change the base?
Changes from all commits
301595a
a05b47b
e0c1a31
8bdd8ee
364656e
0e540b4
7ba5a04
a72d926
b21a2e4
6ea7b54
2f33079
7bb0169
9a88c5b
c1772fd
eacc7e0
b51f83f
ba945b9
6588a85
d7384ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Modal isOpen={isOpen} closeModal={closeModal}> | ||
<ModalContent>{`닉네임 변경 시 다시 로그인을 해야합니다.\n닉네임을 변경하시겠습니까?`}</ModalContent> | ||
<Spacing direction="vertical" size={10} /> | ||
<NicknameContent>{`변경 후 닉네임: ${nickname}`}</NicknameContent> | ||
|
||
<Spacing direction={'vertical'} size={16} /> | ||
|
||
<ButtonContainer> | ||
<CancelButton onClick={closeModal} type="button"> | ||
취소 | ||
</CancelButton> | ||
<ConfirmButton type="button" onClick={onSubmitNickname}> | ||
변경 | ||
</ConfirmButton> | ||
</ButtonContainer> | ||
</Modal> | ||
); | ||
}; | ||
|
||
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%; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const MIN_LENGTH_NICKNAME = 2; | ||
export const MAX_LENGTH_NICKNAME = 20; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로젝트 정책상 로그인 하지 않으면 닉네임을 변경할 수 없으니, |
||
// 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<HTMLInputElement> = (event) => { | ||
const currentNickname = event.currentTarget.value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import fetcher from '@/shared/remotes'; | ||
|
||
export const updateNickname = (memberId: number, nickname: string) => { | ||
return fetcher(`/members/${memberId}/nickname`, 'PATCH', { | ||
nickname, | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 훅 분리 해주신 부분 너무 좋습니다~ |
||
const { handleWithdrawal } = useWithdrawal(); | ||
|
||
const { | ||
isOpen: isWithdrawalModalOpen, | ||
openModal: openWithdrawalModal, | ||
closeModal: closeWithdrawalModal, | ||
} = useModal(); | ||
|
||
const { | ||
isOpen: isNicknameModalOpen, | ||
openModal: openNicknameModal, | ||
closeModal: closeNicknameModal, | ||
} = useModal(); | ||
|
||
return ( | ||
<Container> | ||
<Title>프로필 수정</Title> | ||
<Spacing direction={'vertical'} size={16} /> | ||
<Spacing direction={'vertical'} size={100} /> | ||
<Avatar src={shookshook} /> | ||
<Label htmlFor="nickname">닉네임</Label> | ||
<Spacing direction={'vertical'} size={4} /> | ||
<Input id="nickname" value={user.nickname} disabled /> | ||
<NicknameInput | ||
id="nickname" | ||
value={nicknameEntered} | ||
onChange={handleChangeNickname} | ||
autoComplete="off" | ||
/> | ||
<Spacing direction={'vertical'} size={8} /> | ||
{hasError && <BottomError>{nicknameErrorMessage}</BottomError>} | ||
<Spacing direction={'vertical'} size={16} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 위치를 나타내는 것 보다 어떤 역할인지 나타내주는건 어떤가요? |
||
<Label htmlFor="introduction">소개</Label> | ||
<Spacing direction={'vertical'} size={4} /> | ||
<TextArea id="introduction" value={''} disabled maxLength={100} /> | ||
<Spacing direction={'vertical'} size={16} /> | ||
<WithdrawalButton onClick={openModal}>회원 탈퇴</WithdrawalButton> | ||
<SubmitButton disabled>제출</SubmitButton> | ||
<WithdrawalModal isOpen={isOpen} closeModal={closeModal} onWithdraw={handleWithdrawal} /> | ||
<WithdrawalButton onClick={openWithdrawalModal}>회원 탈퇴</WithdrawalButton> | ||
<SubmitButton onClick={openNicknameModal} disabled={hasError}> | ||
변경 하기 | ||
</SubmitButton> | ||
<WithdrawalModal | ||
isOpen={isWithdrawalModalOpen} | ||
closeModal={closeWithdrawalModal} | ||
onWithdraw={handleWithdrawal} | ||
/> | ||
<NicknameChangingModal | ||
isOpen={isNicknameModalOpen} | ||
closeModal={closeNicknameModal} | ||
onSubmitNickname={submitNicknameChanged} | ||
nickname={nicknameEntered} | ||
/> | ||
</Container> | ||
); | ||
}; | ||
|
@@ -55,7 +74,10 @@ const Container = styled.div` | |
flex-direction: column; | ||
|
||
width: 100%; | ||
min-width: 300px; | ||
max-width: 400px; | ||
height: calc(100vh - ${({ theme: { headerHeight } }) => headerHeight.desktop}); | ||
margin: auto 0; | ||
padding-top: ${({ theme: { headerHeight } }) => headerHeight.desktop}; | ||
|
||
@media (max-width: ${({ theme }) => theme.breakPoints.xs}) { | ||
|
@@ -83,46 +105,58 @@ const Avatar = styled.img` | |
`; | ||
|
||
const Label = styled.label` | ||
font-size: 16px; | ||
margin-top: 16px; | ||
font-size: 18px; | ||
font-weight: 700; | ||
`; | ||
|
||
const disabledStyle = css<{ disabled: boolean }>` | ||
color: ${({ disabled, theme }) => (disabled ? theme.color.black400 : theme.color.black)}; | ||
background-color: ${({ disabled, theme }) => | ||
disabled ? theme.color.disabledBackground : theme.color.white}; | ||
`; | ||
|
||
const Input = styled.input<{ disabled: boolean }>` | ||
${disabledStyle}; | ||
padding: 0 8px; | ||
font-size: 16px; | ||
`; | ||
|
||
const TextArea = styled.textarea<{ disabled: boolean }>` | ||
${disabledStyle}; | ||
resize: none; | ||
`; | ||
|
||
const WithdrawalButton = styled.button` | ||
color: ${({ theme }) => theme.color.disabled}; | ||
text-decoration: underline; | ||
`; | ||
|
||
const SubmitButton = styled.button<{ disabled: boolean }>` | ||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; | ||
|
||
const SubmitButton = styled.button` | ||
position: absolute; | ||
bottom: 0; | ||
|
||
align-self: flex-end; | ||
|
||
width: 100%; | ||
height: 36px; | ||
padding: 11px 20px; | ||
|
||
font-size: 18px; | ||
font-weight: 700; | ||
|
||
${disabledStyle}; | ||
background-color: ${({ theme }) => theme.color.primary}; | ||
border: none; | ||
border-radius: 10px; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 글로벌 스타일 확인해주세용 |
||
|
||
&:disabled { | ||
color: ${({ theme }) => theme.color.disabled}; | ||
background-color: ${({ theme }) => theme.color.disabledBackground}; | ||
} | ||
`; | ||
|
||
const NicknameInput = styled.input` | ||
padding: 0 8px; | ||
|
||
font-size: 18px; | ||
line-height: 2.4; | ||
color: ${({ theme }) => theme.color.black}; | ||
|
||
border: none; | ||
border-radius: 6px; | ||
outline: none; | ||
box-shadow: 0 0 0 1px inset ${({ theme }) => theme.color.black200}; | ||
|
||
transition: box-shadow 0.3s ease; | ||
|
||
&:focus { | ||
box-shadow: 0 0 0 2px inset ${({ theme }) => theme.color.primary}; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 개인적인 의견으로는 input을 입력하다가 정책상 금지된 행동을 했을때, |
||
`; | ||
|
||
const BottomError = styled.p` | ||
font-size: 14px; | ||
color: ${({ theme }) => theme.color.error}; | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우코가 만든 Login, withdraw, NicknameChangingModal 들은 공통점이 있어요.
모두 사용자의 의도를 묻는
contents
와취소시 모달 닫기
,수락시 특정 함수를 실행
하는Confirm
이 목적인 모달이라는거죠.이걸 고려해서 ConfirmModal이라는 공통 컴포넌트를 만들 수 있을것 같아요.
이후엔 사용자에게 질문하려고 할 때 마다 매번 위와 같은 xxx모달 컴포넌트를 만들지 않아도 되겠네요😀