Skip to content
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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
301595a
feat: useMutation promise 객체 반환하도록 수정
ukkodeveloper Sep 29, 2023
a05b47b
feat: nickname 변경 api 구현
ukkodeveloper Sep 29, 2023
e0c1a31
feat: 프로필 변경 페이지에서 nickname 변경 핸들러 구현
ukkodeveloper Sep 29, 2023
8bdd8ee
refactor: 회원 탈퇴 modal 관련 네이밍 변경
ukkodeveloper Sep 29, 2023
364656e
feat: nickname 변경 확인 modal 구현
ukkodeveloper Sep 29, 2023
0e540b4
refactor: 함수명 및 이벤트 핸들러 로직 수정
ukkodeveloper Sep 30, 2023
7ba5a04
design: button disabled 시에 cursor not-allowed
ukkodeveloper Oct 3, 2023
a72d926
feat: 변경할 닉네임 유효성 검사 추가
ukkodeveloper Oct 3, 2023
b21a2e4
design: 프로필 수정 페이지 디자인 변경
ukkodeveloper Oct 3, 2023
6ea7b54
feat: 닉네임 변경 시 로그아웃 후 로그인 페이지로 이동
ukkodeveloper Oct 3, 2023
2f33079
fix: mutateData response 반환하지 않도록 수정
ukkodeveloper Oct 3, 2023
7bb0169
refactor: 프로필 변경 페이지 내 로직 훅으로 분리
ukkodeveloper Oct 3, 2023
9a88c5b
fix: 닉네임 최대 글자 수 20자로 수정
ukkodeveloper Oct 3, 2023
c1772fd
feat: nickname 20자 넘어갈 경우 입력되지 않도록 변경
ukkodeveloper Oct 18, 2023
eacc7e0
design: 전반적인 디자인 스타일 피드백 반영
ukkodeveloper Oct 19, 2023
b51f83f
feat: nickname 변경 api 개선
ukkodeveloper Oct 19, 2023
ba945b9
feat: nickname 삭제 api
ukkodeveloper Oct 19, 2023
6588a85
Merge branch 'main' into feat/#480
ukkodeveloper Oct 19, 2023
d7384ff
refactor: 닉네임 min length, max length 상수화
ukkodeveloper Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Collaborator

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모달 컴포넌트를 만들지 않아도 되겠네요😀

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 변경되는 닉네임이 잘 부각되지 않아 보인다고 생각하는데,
대략 이런 식으로 조금 부각시켜 보여주면 어떨까요?
꼭 제안과 같은 방법이 아니더라도, 질문과 사용자가 원하는 정보를 분리해서 보여주면 좋을 것 같습니다.


image

image

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}>
<>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 프래그먼트는 없어도 되겠네요~
withdrawalModal에도 동일한 부분에 같은 프라그 먼트가 있으니 삭제해 주세요.

<ModalContent>{`닉네임 변경 시 다시 로그인을 해야합니다.\n닉네임을 ${nickname}로 바꾸겠습니까?`}</ModalContent>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시멘틱 + 웹접근성 측면에서
div 안에 바로 문장 내용을 보여주는 것 보단, 별도의 태그로 내용물을 분리하는게 좋을 것 같아요.

<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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 없어도 아무런 변화가 없던데, 위 속성은 어떤 역할을 하나요?


font-size: 16px;
line-height: 200%;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 font size에 맞춰 배율로 계산하기 편한 숫자를 쓰는건 어떨까요?
line-height: 2

color: ${({ theme }) => theme.color.subText};
white-space: pre-line;
`;

const Button = styled.button`
cursor: pointer;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointer는 글로벌 스타일에 있습니다. 삭제해도 되겠네요.


height: 36px;

color: ${({ theme: { color } }) => color.white};

border: none;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

글로벌 스타일에 button의 border : 0 이 정의되어 있습니다.
위 속성은 삭제하고, 글로벌 스타일에 border:none을 바꾸는게 좋겠네요.

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%;
`;
50 changes: 50 additions & 0 deletions frontend/src/features/member/hooks/useNickname.ts
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();
Copy link
Collaborator

@Creative-Lee Creative-Lee Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로젝트 정책상 로그인 하지 않으면 닉네임을 변경할 수 없으니,
user 객체가 null이라면, useNickname 훅도 실행하지 않고 return하는 분기가 있어도 될것 같아요.
현재는 updateNickname remote 함수의 인자가 둘다 undefined type일수도 있는데,
변경된다면, remote 함수의 type이 조금 더 의미있게 변할 것 같아요.


const [nicknameEntered, setNicknameEntered] = useState(user?.nickname);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 이 memo가 어떤 이유로 필요했는지, 맥락을 몰라서 이해가 잘 되지않네요.
아래 useMutation에 인자로 화살표 함수를 넣는대신, remote 함수에 커링을 사용하고,
remote 함수를 useMemo로 메모이제이션 한 이유가 있나요?

);
const { mutateData: changeNickname } = useMutation(mutateNickname);
const navigate = useNavigate();

const hasError = nicknameErrorMessage.length !== 0;
const handleChangeNickname: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const currentNickname = event.currentTarget.value;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value만 뽑아 쓴다면 구조분해 해서 가져와도 될 것 같습니다.

setNicknameEntered(currentNickname);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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('');
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
23 changes: 23 additions & 0 deletions frontend/src/features/member/hooks/useWithdrawal.ts
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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 이 로직을 보고, deleteMember가 바로 실행되는 로직이여서 당황했습니다.
알고보니, updateNickname, deleteMember remote 함수에 커링을 적용하셨더라고요.

프로젝트 전반적으로 remote함수는 커링이 아닌 함수로 작성되고 있었기 때문에, 혼동이 생길 수 있을 것 같아요.

💬 프로젝트에선 mutation 성격의 api콜 + remote 함수의 인자가 필요한 경우,
화살표 함수를 적용하는 방식으로 구현이 되어있는데, 일관성을 위해 통일해 보는건 어떨까요?


const handleWithdrawal = async () => {
await withdrawMember();
logout();
navigate(ROUTE_PATH.ROOT);
};

return {
handleWithdrawal,
};
};

export default useWithdrawal;
8 changes: 8 additions & 0 deletions frontend/src/features/member/remotes/nickname.ts
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) => () => {
Copy link
Collaborator

@Creative-Lee Creative-Lee Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 링크용

return fetcher(`/members/${memberId}/nickname`, 'PATCH', {
nickname,
});
};
141 changes: 89 additions & 52 deletions frontend/src/pages/EditProfilePage.tsx
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"
/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 maxlength를 활용해봐도 좋을 것 같아요

<Spacing direction={'vertical'} size={8} />
{hasError && <BottomError>{nicknameErrorMessage}</BottomError>}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
);
};
Expand All @@ -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}) {
Expand Down Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line-height 단위를 % or 숫자 하나로 통일하는게 좋겠네요.


${disabledStyle};
background-color: ${({ theme }) => theme.color.primary};
border: none;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 개인적인 의견으로는 input을 입력하다가 정책상 금지된 행동을 했을때,
붉은 계열의 테두리가 생기는게 일반적이라고 생각해요.
focus만 했는데, 붉은 계열의 테두리가 생겨서 '내가 뭘 잘못했나?' 생각할수도 있을것 같네요.
어떻게 생각하세요?

}
`;

const BottomError = styled.p`
font-size: 14px;
color: ${({ theme }) => theme.color.error};
`;
4 changes: 4 additions & 0 deletions frontend/src/shared/styles/GlobalStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const GlobalStyles = createGlobalStyle`
cursor: pointer;
background: none;
border: 0;

&:disabled {
cursor: not-allowed;
}
}
a {
text-decoration: none;
Expand Down