-
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 13 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,74 @@ | ||
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}> | ||
<> | ||
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. 이 프래그먼트는 없어도 되겠네요~ |
||
<ModalContent>{`닉네임 변경 시 다시 로그인을 해야합니다.\n닉네임을 ${nickname}로 바꾸겠습니까?`}</ModalContent> | ||
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. 시멘틱 + 웹접근성 측면에서 |
||
<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` | ||
align-self: start; | ||
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. 💬 없어도 아무런 변화가 없던데, 위 속성은 어떤 역할을 하나요? |
||
|
||
font-size: 16px; | ||
line-height: 200%; | ||
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. 💬 font size에 맞춰 배율로 계산하기 편한 숫자를 쓰는건 어떨까요? |
||
color: ${({ theme }) => theme.color.subText}; | ||
white-space: pre-line; | ||
`; | ||
|
||
const Button = styled.button` | ||
cursor: pointer; | ||
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. pointer는 글로벌 스타일에 있습니다. 삭제해도 되겠네요. |
||
|
||
height: 36px; | ||
|
||
color: ${({ theme: { color } }) => color.white}; | ||
|
||
border: none; | ||
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. 글로벌 스타일에 button의 border : 0 이 정의되어 있습니다. |
||
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,50 @@ | ||
import { useMemo, useState } from 'react'; | ||
import { useNavigate } from 'react-router-dom'; | ||
import { useAuthContext } from '@/features/auth/components/AuthProvider'; | ||
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. 프로젝트 정책상 로그인 하지 않으면 닉네임을 변경할 수 없으니, |
||
|
||
const [nicknameEntered, setNicknameEntered] = useState(user?.nickname); | ||
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. 💬 Entered 라는 단어가 없어도 충분히 이해 된다고 생각해요. 우코는 어떻게 생각하세요? |
||
const [nicknameErrorMessage, setNicknameErrorMessage] = | ||
useState('이전과 다른 닉네임으로 변경해주세요.'); | ||
const mutateNickname = useMemo( | ||
() => updateNickname(user?.memberId, nicknameEntered), | ||
[nicknameEntered, user?.memberId] | ||
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. 💬 이 memo가 어떤 이유로 필요했는지, 맥락을 몰라서 이해가 잘 되지않네요. |
||
); | ||
const { mutateData: changeNickname } = useMutation(mutateNickname); | ||
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만 뽑아 쓴다면 구조분해 해서 가져와도 될 것 같습니다. |
||
setNicknameEntered(currentNickname); | ||
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. 💬 지금은 문제가 있어도 텍스트가 업데이트 되어서 다소 어색하다고 생각되어요. |
||
if (currentNickname.length < 2 || currentNickname.length > 20) { | ||
setNicknameErrorMessage('2글자 이상 20글자 이하 문자만 가능합니다.'); | ||
} else if (currentNickname === user?.nickname) { | ||
setNicknameErrorMessage('이전과 다른 닉네임으로 변경해주세요.'); | ||
} else { | ||
setNicknameErrorMessage(''); | ||
} | ||
}; | ||
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 isValidA = () => {}
const isValidB = () => {}
const isValidC = () => {}
const validateNickname = () => {
if(!isValidA) return 'A Error msg'
if(!validateB) return 'B Error msg'
if(!validateC) return 'C Error msg'
return ''
} |
||
|
||
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,23 @@ | ||
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(); | ||
const { mutateData: withdrawMember } = useMutation(deleteMember(user?.memberId)); | ||
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. 처음에 이 로직을 보고, deleteMember가 바로 실행되는 로직이여서 당황했습니다. 프로젝트 전반적으로 remote함수는 커링이 아닌 함수로 작성되고 있었기 때문에, 혼동이 생길 수 있을 것 같아요. 💬 프로젝트에선 mutation 성격의 api콜 + remote 함수의 인자가 필요한 경우, |
||
|
||
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 |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import fetcher from '@/shared/remotes'; | ||
|
||
export const updateNickname = | ||
(memberId: number | undefined, nickname: string | undefined) => () => { | ||
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. 💬 링크용 |
||
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 /> | ||
<Input | ||
id="nickname" | ||
value={nicknameEntered} | ||
onChange={handleChangeNickname} | ||
autoComplete="off" | ||
/> | ||
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. 💬 maxlength를 활용해봐도 좋을 것 같아요 |
||
<Spacing direction={'vertical'} size={8} /> | ||
{hasError && <BottomError>{nicknameErrorMessage}</BottomError>} | ||
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. 💬 위치를 나타내는 것 보다 어떤 역할인지 나타내주는건 어떤가요? |
||
<Spacing direction={'vertical'} size={16} /> | ||
<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: 800px; | ||
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,61 @@ 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}; | ||
font-size: 16px; | ||
padding: 0 8px; | ||
`; | ||
|
||
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` | ||
cursor: pointer; | ||
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. 글로벌 스타일에 있습니다~ |
||
|
||
position: absolute; | ||
bottom: 0; | ||
|
||
align-self: flex-end; | ||
|
||
width: 100%; | ||
height: 36px; | ||
padding: 11px 20px; | ||
|
||
font-size: 18px; | ||
font-weight: 700; | ||
line-height: 1.6; | ||
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. line-height 단위를 % or 숫자 하나로 통일하는게 좋겠네요. |
||
|
||
${disabledStyle}; | ||
background-color: ${({ theme }) => theme.color.primary}; | ||
border: none; | ||
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. 글로벌 스타일 확인해주세용 |
||
border-radius: 10px; | ||
|
||
&:disabled { | ||
color: ${({ theme }) => theme.color.disabled}; | ||
background-color: ${({ theme }) => theme.color.disabledBackground}; | ||
} | ||
`; | ||
|
||
const Input = 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모달 컴포넌트를 만들지 않아도 되겠네요😀