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 all 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,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%;
`;
2 changes: 2 additions & 0 deletions frontend/src/features/member/constants/nickname.ts
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;
61 changes: 61 additions & 0 deletions frontend/src/features/member/hooks/useNickname.ts
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();
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이 조금 더 의미있게 변할 것 같아요.

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

Choose a reason for hiding this comment

The 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;
28 changes: 28 additions & 0 deletions frontend/src/features/member/hooks/useWithdrawal.ts
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;
2 changes: 1 addition & 1 deletion frontend/src/features/member/remotes/member.ts
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');
};
7 changes: 7 additions & 0 deletions frontend/src/features/member/remotes/nickname.ts
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,
});
};
140 changes: 87 additions & 53 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 />
<NicknameInput
id="nickname"
value={nicknameEntered}
onChange={handleChangeNickname}
autoComplete="off"
/>
<Spacing direction={'vertical'} size={8} />
{hasError && <BottomError>{nicknameErrorMessage}</BottomError>}
<Spacing direction={'vertical'} size={16} />
Copy link
Collaborator

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

The 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};
}
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 {
cursor: pointer;
Expand Down
Loading