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}}
-
-
-
-
- 회원 탈퇴
- 제출
-
+ 회원 탈퇴
+
+ 변경 하기
+
+
+
);
};
@@ -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;
+
+ &: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};
+ }
+`;
+
+const BottomError = styled.p`
+ font-size: 14px;
+ color: ${({ theme }) => theme.color.error};
`;
diff --git a/frontend/src/shared/styles/GlobalStyles.ts b/frontend/src/shared/styles/GlobalStyles.ts
index fbce5564..ebb6ad73 100644
--- a/frontend/src/shared/styles/GlobalStyles.ts
+++ b/frontend/src/shared/styles/GlobalStyles.ts
@@ -51,6 +51,10 @@ const GlobalStyles = createGlobalStyle`
cursor: pointer;
background: none;
border: 0;
+
+ &:disabled {
+ cursor: not-allowed;
+ }
}
a {
cursor: pointer;