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: 내 정보 수정 페이지 UI 및 API 연동 구현 #924

Merged
merged 2 commits into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions frontend/src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export interface QueryMemberSuccess {
organization: string | null;
}

export interface PutMemberParams {
userName: string;
emoji: string;
}

export const queryMember: QueryFunction<AxiosResponse<QueryMemberSuccess>> = () => {
return api.get('/members/me');
};

export const putMember = ({ userName, emoji }: PutMemberParams) => {
return api.put('/members/me', {
userName,
emoji,
});
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import useEmojiList from 'hooks/query/useEmojiList';
import * as Styled from './EmojiSelector.styles';

interface EmojiSelectorProps {
initialEmoji?: string;
onSelect?: (emoji: string) => void;
}

const EmojiSelector = ({ onSelect }: EmojiSelectorProps): JSX.Element => {
const EmojiSelector = ({ initialEmoji, onSelect }: EmojiSelectorProps): JSX.Element => {
const emojiListQuery = useEmojiList();

const [selectedEmoji, setSelectedEmoji] = useState<string | null>(initialEmoji ?? null);

const emojiList = useMemo(
() => emojiListQuery.data?.data.emojis ?? [],
[emojiListQuery.data?.data.emojis]
);

const handleSelect = (emoji: string) => {
setSelectedEmoji(emoji);
onSelect?.(emoji);
};

Expand All @@ -28,6 +32,7 @@ const EmojiSelector = ({ onSelect }: EmojiSelectorProps): JSX.Element => {
type="radio"
name="emoji"
id={emoji.name}
checked={selectedEmoji === emoji.name}
onChange={() => handleSelect(emoji.name)}
/>
<Styled.EmojiCode>{emoji.code}</Styled.EmojiCode>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/MemberInfo/MemberInfo.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const InfoContainer = styled.div`
flex-direction: column;

& > * {
margin-bottom: 12px;
margin-bottom: 6px;

::last-of-type {
margin-bottom: 0;
Expand All @@ -37,13 +37,15 @@ export const InfoContainer = styled.div`

export const NameTextContainer = styled.div`
font-size: 1.5rem;
display: flex;
align-items: center;
`;

export const NameText = styled.span`
font-weight: bold;
margin-right: 0.375rem;
`;

export const OranizationTextContainer = styled.div`
export const OrganizationTextContainer = styled.div`
font-size: 0.875rem;
`;
11 changes: 9 additions & 2 deletions frontend/src/components/MemberInfo/MemberInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { theme } from 'App.styles';
import { ReactComponent as CaretIcon } from 'assets/svg/caret-right.svg';
import PATH from 'constants/path';
import useMember from 'hooks/query/useMember';
import * as Styled from './MemberInfo.styled';

Expand All @@ -13,11 +17,14 @@ const MemberInfo = (): JSX.Element => {
<Styled.InfoContainer>
<Styled.NameTextContainer>
<Styled.NameText>{data?.data.userName}</Styled.NameText>님
<Link to={PATH.MANAGER_PROFILE_EDIT}>
<CaretIcon width={36} height={36} fill={theme.gray[400]} />
</Link>
</Styled.NameTextContainer>
{data?.data.organization && (
<Styled.OranizationTextContainer>
<Styled.OrganizationTextContainer>
Comment on lines -18 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

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

오타가 있었군요 ㅋㅋㅋㅋ

{data?.data.organization}
</Styled.OranizationTextContainer>
</Styled.OrganizationTextContainer>
)}
</Styled.InfoContainer>
</Styled.Container>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const MESSAGE = {
LOGIN: {
UNEXPECTED_ERROR: '로그인에 문제가 발생했습니다. 잠시 후에 다시 시도해주세요.',
},
MEMBER: {
EDIT_PROFILE_UNEXPECTED_ERROR:
'내 정보를 수정하는데 문제가 발생했습니다. 잠시 후에 다시 시도해주세요.',
},
RESERVATION: {
CREATE: '예약하기',
EDIT: '예약 수정하기',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const PATH = {
MANAGER_SOCIAL_JOIN: '/join/social',
MANAGER_GITHUB_OAUTH_REDIRECT: '/login/oauth/github',
MANAGER_GOOGLE_OAUTH_REDIRECT: '/login/oauth/google',
MANAGER_PROFILE_EDIT: '/profile/edit',
MANAGER_MAP_DETAIL: '/map/:mapId',
MANAGER_MAP_LIST: '/map/list',
MANAGER_MAP_CREATE: '/map/create',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/constants/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
import GuestMain from 'pages/GuestMain/GuestMain';
import GuestMapContainer from 'pages/GuestMap/GuestMapContainer';
import ManagerMapList from 'pages/ManagerMapList/ManagerMapList';
import ManagerProfileEdit from 'pages/ManagerProfileEdit/ManagerProfileEdit';
import PATH from './path';

const GuestMap = React.lazy(() => import('pages/GuestMap/GuestMap'));
Expand Down Expand Up @@ -85,6 +86,11 @@ export const PRIVATE_ROUTES: PrivateRoute[] = [
component: <ManagerMapList />,
redirectPath: PATH.LOGIN,
},
{
path: PATH.MANAGER_PROFILE_EDIT,
component: <ManagerProfileEdit />,
redirectPath: PATH.LOGIN,
},
{
path: PATH.MANAGER_RESERVATION,
component: <ManagerReservation />,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/ManagerJoin/units/JoinForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import React, { FormEventHandler, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { queryValidateEmail, queryValidateUserName } from 'api/join';
import Button from 'components/Button/Button';
import EmojiSelector from 'components/EmojiSelector/EmojiSelector';
import Input from 'components/Input/Input';
import MANAGER from 'constants/manager';
import MESSAGE from 'constants/message';
import REGEXP from 'constants/regexp';
import useInputs from 'hooks/useInputs';
import { ErrorResponse } from 'types/response';
import { JoinParams } from '../ManagerJoin';
import EmojiSelector from './EmojiSelector';
import * as Styled from './JoinForm.styles';

interface Form {
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import styled from 'styled-components';
import { FORM_MAX_WIDTH } from 'constants/style';

export const Container = styled.div`
width: 100%;
max-width: ${FORM_MAX_WIDTH};
margin: 0 auto;
`;

export const PageTitle = styled.h2`
font-size: 1.5rem;
font-weight: 400;
text-align: center;
margin: 2.125rem auto;
`;

export const PasswordChangeLinkMessage = styled.p`
margin: 1rem 0;
text-align: center;
font-size: 0.75rem;
color: ${({ theme }) => theme.gray[500]};

a {
color: ${({ theme }) => theme.primary[400]};
text-decoration: none;
margin-left: 0.375rem;

&:hover {
font-weight: 700;
}
}
`;
48 changes: 48 additions & 0 deletions frontend/src/pages/ManagerProfileEdit/ManagerProfileEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AxiosError } from 'axios';
import React from 'react';
import { useMutation } from 'react-query';
import { Link, useHistory } from 'react-router-dom';
import { putMember } from 'api/member';
import Header from 'components/Header/Header';
import Layout from 'components/Layout/Layout';
import MESSAGE from 'constants/message';
import PATH from 'constants/path';
import { ErrorResponse } from 'types/response';
import * as Styled from './ManagerProfileEdit.styles';
import ProfileEditForm from './units/ProfileEditForm';

const ManagerProfileEdit = () => {
const history = useHistory();

const editProfile = useMutation(putMember, {
onSuccess: () => {
history.push(PATH.MANAGER_MAP_LIST);
},

onError: (error: AxiosError<ErrorResponse>) => {
alert(error?.response?.data.message ?? MESSAGE.MEMBER.EDIT_PROFILE_UNEXPECTED_ERROR);
},
});

const handleSubmit = ({ userName, emoji }: { userName: string; emoji: string }) => {
editProfile.mutate({ userName, emoji });
};

return (
<>
<Header />
<Layout>
<Styled.Container>
<Styled.PageTitle>내 정보 수정</Styled.PageTitle>
<ProfileEditForm onSubmit={handleSubmit} />
<Styled.PasswordChangeLinkMessage>
비밀번호를 변경하고 싶으신가요?
Copy link
Collaborator

@yujo11 yujo11 Mar 12, 2023

Choose a reason for hiding this comment

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

네니요

<Link to="/">변경하기</Link>
</Styled.PasswordChangeLinkMessage>
</Styled.Container>
</Layout>
</>
);
};

export default ManagerProfileEdit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styled from 'styled-components';

export const Form = styled.form`
margin: 3.75rem 0 1rem;
`;

export const InputWrapper = styled.div`
margin-bottom: 3rem;
`;
109 changes: 109 additions & 0 deletions frontend/src/pages/ManagerProfileEdit/units/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { queryValidateUserName } from 'api/join';
import Button from 'components/Button/Button';
import EmojiSelector from 'components/EmojiSelector/EmojiSelector';
import Input from 'components/Input/Input';
import MANAGER from 'constants/manager';
import MESSAGE from 'constants/message';
import useMember from 'hooks/query/useMember';
import useInputs from 'hooks/useInputs';
import { ErrorResponse } from 'types/response';
import * as Styled from './ProfileEditForm.styles';

interface ProfileEditFormProps {
onSubmit: ({ userName, emoji }: { userName: string; emoji: string }) => void;
}

const ProfileEditForm = ({ onSubmit }: ProfileEditFormProps) => {
const member = useMember();
const initialUserName = member.data?.data.userName;
const initialEmoji = member.data?.data.emoji.name;

const [emoji, setEmoji] = useState<string>('');
const [{ userName }, onChangeForm, setInputs] = useInputs<{ userName: string }>({
userName: '',
});

const [userNameMessage, setUserNameMessage] = useState('');

const checkValidateUserName = useQuery(
['checkValidateUserName', userName],
queryValidateUserName,
{
enabled: false,
retry: false,

onSuccess: () => {
setUserNameMessage(MESSAGE.JOIN.VALID_USERNAME);
},

onError: (error: AxiosError<ErrorResponse>) => {
setUserNameMessage(
error.response?.data.message ?? MESSAGE.JOIN.CHECK_USERNAME_UNEXPECTED_ERROR
);
},
}
);

const handleSelectEmoji = (emoji: string) => {
setEmoji(emoji);
};

const handleChangeUserName = (event: React.ChangeEvent<HTMLInputElement>) => {
onChangeForm(event);
setUserNameMessage('');
};

const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();

onSubmit({ userName, emoji });
};

const isSubmitButtonDisabled = !(emoji && userName);

useEffect(() => {
if (!initialUserName) return;

setInputs((prevInputs) => ({
...prevInputs,
userName: initialUserName,
}));
}, [initialUserName, setInputs]);

useEffect(() => {
if (!initialEmoji) return;

setEmoji(initialEmoji);
}, [initialEmoji]);

return (
<Styled.Form onSubmit={handleSubmit}>
{initialEmoji && <EmojiSelector initialEmoji={initialEmoji} onSelect={handleSelectEmoji} />}

{initialUserName && (
<Styled.InputWrapper>
<Input
type="text"
label="이름"
name="userName"
minLength={MANAGER.USERNAME.MIN_LENGTH}
maxLength={MANAGER.USERNAME.MAX_LENGTH}
value={userName}
onChange={handleChangeUserName}
message={userNameMessage}
status={checkValidateUserName.isSuccess ? 'success' : 'error'}
required
/>
</Styled.InputWrapper>
)}
<Button variant="primary" size="large" fullWidth disabled={isSubmitButtonDisabled}>
수정하기
</Button>
</Styled.Form>
);
};

export default ProfileEditForm;
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { AxiosError } from 'axios';
import { FormEventHandler, useState } from 'react';
import { useQuery } from 'react-query';
import { queryValidateUserName } from 'api/join';
import EmojiSelector from 'components/EmojiSelector/EmojiSelector';
import Input from 'components/Input/Input';
import SocialJoinButton from 'components/SocialAuthButton/SocialJoinButton';
import MANAGER from 'constants/manager';
import MESSAGE from 'constants/message';
import useInput from 'hooks/useInput';
import EmojiSelector from 'pages/ManagerJoin/units/EmojiSelector';
import { ErrorResponse } from 'types/response';
import { SocialJoinParams } from '../ManagerSocialJoin';
import * as Styled from './SocialJoinForm.styles';
Expand Down