Skip to content

Commit

Permalink
feat/#50: 자녀 초대하기
Browse files Browse the repository at this point in the history
닉네임을 이용한 자식 검색 가능
 - 존재하지 않는 닉네임이라면 멘트로 알려줌

자녀 초대
 - 검색 결과 존재하는 자녀라면 초대가 가능
 - 이후 manage 페이지로 이동하여 초대 확인 가능
  • Loading branch information
yh-project committed Aug 27, 2024
1 parent 534dc41 commit 3c5e6d4
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 10 deletions.
5 changes: 3 additions & 2 deletions ssh-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ function App() {
location.pathname === '/signup' ||
location.pathname === '/mypage' ||
location.pathname === '/manage' ||
location.pathname === '/quiz/solve'
location.pathname === '/quiz/solve' ||
location.pathname === '/request'
) && <NavigationBar />}
<div
className={`${size === 'M' || size === 'T' ? (location.pathname === '/login' || location.pathname === '/signup' || location.pathname === '/mypage' || location.pathname === '/manage' ? '!min-h-full' : 'pb-[4rem]') : 'pb-0'} BODY-LAYOUT h-[calc(100%-3.5rem)] desktop:flex-1 relative w-full flex justify-center`}
className={`${size === 'M' || size === 'T' ? (location.pathname === '/login' || location.pathname === '/signup' || location.pathname === '/mypage' || location.pathname === '/manage' || location.pathname === '/quiz/solve' || location.pathname === '/request' ? '!min-h-full' : 'pb-[4rem]') : 'pb-0'} BODY-LAYOUT h-[calc(100%-3.5rem)] desktop:flex-1 relative w-full flex justify-center`}
>
<Outlet />
</div>
Expand Down
8 changes: 8 additions & 0 deletions ssh-web/src/apis/userApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ export const deleteMyChild = (nickname: string) => {
export const deleteMyWaitingChild = (nickname: string) => {
return api.patch('/api/parents/children/waiting', { nickname: nickname });
};

export const findChildByNickname = (nickname: string) => {
return api.post('/api/children', { nickname: nickname });
};

export const requestChild = (nickname: string) => {
return api.post('/api/parents/children/request', { nickname: nickname });
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { IChild } from '../../../interfaces/userInterface';

export interface MascotCardProps extends React.ComponentProps<'div'> {
childInfo: IChild;
isWaiting: boolean;
isWaiting?: boolean;
withTrash?: boolean;
children?: ReactNode;
classNameStyles?: string;
}
15 changes: 9 additions & 6 deletions ssh-web/src/components/molecules/MascotCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
export const MascotCard = ({
childInfo,
isWaiting = false,
withTrash = true,
children,
classNameStyles,
}: MascotCardProps) => {
Expand Down Expand Up @@ -52,12 +53,14 @@ export const MascotCard = ({
</Typography>
</div>
</div>
<Icon
color="danger"
classNameStyles="absolute right-4 top-4 mob:right-2 mob:top-2"
>
<HiTrash onClick={() => mutate(childInfo.nickname)} />
</Icon>
{withTrash && (
<Icon
color="danger"
classNameStyles="absolute right-4 top-4 mob:right-2 mob:top-2"
>
<HiTrash onClick={() => mutate(childInfo.nickname)} />
</Icon>
)}
</div>
);
};
44 changes: 44 additions & 0 deletions ssh-web/src/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,4 +591,48 @@ mock.onPatch('/api/parents/children/waiting').reply((config) => {
});
});

mock.onPost('/api/children').reply((config) => {
const target = JSON.parse(config.data).nickname;

return target === '귀요미요하'
? new Promise((resolve) => {
setTimeout(() => {
resolve([
200,
{
name: '김다운',
nickname: '귀욤다운',
birthday: '1999-06-30',
gender: Math.floor(Math.random() * 2) ? 'MALE' : 'FEMALE',
},
]);
}, 500);
})
: new Promise((resolve) => {
setTimeout(() => {
resolve([
404,
{
code: 'U003',
description: '존재하지 않는 사용자',
},
]);
}, 500);
});
});

mock.onPost('/api/parents/children/request').reply((config) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
202,
{
code: null,
description: '자식 신청 성공',
},
]);
}, 500);
});
});

// ========== 사용자 도메인 ==========
2 changes: 1 addition & 1 deletion ssh-web/src/pages/Information/Manage/ManageFetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const ManageFetch = () => {
color="dark"
classNameStyles="absolute desktop:left-36 tabletB:left-20 mob:left-8"
>
<HiChevronLeft />
<HiChevronLeft onClick={() => nav('/mypage')} />
</Icon>
<Typography weight="bold" size="xl" color="dark">
자녀 정보
Expand Down
126 changes: 126 additions & 0 deletions ssh-web/src/pages/Information/Request/RequestFetch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import { containerStyles, contentStyles } from './styles';
import {
findChildByNickname,
getUserInfo,
requestChild,
} from '../../../apis/userApi';
import { Mascot } from '../../../components/molecules/Mascot';
import { infoBoxStyles } from '../Manage/styles';
import { Icon } from '../../../components/atoms/Icon';
import { HiChevronLeft } from 'react-icons/hi2';
import { useNavigate } from 'react-router-dom';
import { Typography } from '../../../components/atoms/Typography';
import { AvatarWithLabel } from '../../../components/molecules/AvatarWithLabel';
import { getImgSrc } from '../../../utils/userUtil';
import { HiMagnifyingGlass } from 'react-icons/hi2';
import TextField from '../../../components/atoms/TextField';
import { IChild } from '../../../interfaces/userInterface';
import { MascotCard } from '../../../components/molecules/MascotCard';
import { Button } from '../../../components/atoms/Button';
import { showToast } from '../../../utils/toastUtil';

export const RequestFetch = () => {
const userinfoQuery = useSuspenseQuery({
queryKey: ['userinfo'],
queryFn: async () => await getUserInfo(),
});

if (userinfoQuery.error && !userinfoQuery.isFetching) {
throw userinfoQuery.error;
}

const nav = useNavigate();
const [nickname, setNickname] = useState<string>('');
const [child, setChild] = useState<IChild | null>(null);
const onChangeNickname = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(() => e.target.value);
},
[nickname],
);

const { mutate } = useMutation({
mutationFn: async (nickname: string) => await requestChild(nickname),
onSuccess: (res) => {
showToast('success', res.data.description);
nav('/manage');
},
onError: () => showToast('error', '자식 신청에 실패했습니다'),
});

return (
<div className={containerStyles()}>
<Mascot
nickname={userinfoQuery.data.data.nickname}
ment="자녀 정보가 궁금하시군요!!"
classNameStyles="tablet:hidden"
/>
<div className={contentStyles()}>
<div className={infoBoxStyles()}>
<div className="flex justify-center w-full">
<Icon
color="dark"
classNameStyles="absolute desktop:left-36 tabletB:left-20 mob:left-8"
>
<HiChevronLeft onClick={() => nav('/manage')} />
</Icon>
<Typography weight="bold" size="xl" color="dark">
자녀 초대하기
</Typography>
</div>
<AvatarWithLabel
imageUrl={getImgSrc(userinfoQuery.data.data.gender, 'PARENT')}
label={userinfoQuery.data.data.nickname}
altText="avatarwithlabel"
size="2xl"
classNameStyles="mt-4"
/>
<div className="relative w-full mt-4">
<TextField label="닉네임" onChange={onChangeNickname} fullWidth />
<Icon
color="dark"
size="sm"
classNameStyles="absolute top-4 right-4"
>
<HiMagnifyingGlass
onClick={async () => {
await findChildByNickname(nickname)
.then((res) => setChild(res.data))
.catch(() => setChild(null));
}}
/>
</Icon>
</div>
<div
className={`w-full mt-4 ${child ? '' : 'flex justify-center items-center p-8'}`}
>
{child ? (
<MascotCard
key={child.nickname}
childInfo={child}
withTrash={false}
/>
) : (
'자녀가 존재하지 않습니다'
)}
</div>
</div>
<div className="w-full desktop:px-36 tabletB:px-20 mob:px-8 bottom-16">
<Button
fullWidth
onClick={() => {
if (child) mutate(child.nickname);
else showToast('error', '초대할 자녀를 검색해주세요');
}}
>
<Typography weight="bold" size="sm" color="light">
초대하기
</Typography>
</Button>
</div>
</div>
</div>
);
};
13 changes: 13 additions & 0 deletions ssh-web/src/pages/Information/Request/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { RequestFetch } from './RequestFetch';

export const Request = () => {
return (
<ErrorBoundary fallback={<>에러</>}>
<Suspense fallback={<>로딩중</>}>
<RequestFetch />
</Suspense>
</ErrorBoundary>
);
};
13 changes: 13 additions & 0 deletions ssh-web/src/pages/Information/Request/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { tv } from 'tailwind-variants';

export const containerStyles = tv({
base: 'flex items-center justify-center w-full h-auto tablet:flex-col',
});

export const contentStyles = tv({
base: 'flex flex-col items-center justify-between gap-y-4 bg-white desktop:w-[48rem] desktop:h-[48rem] desktop:rounded-lg desktop:py-12 tablet:w-full tablet:h-full tablet:py-8',
});

export const infoBoxStyles = tv({
base: 'relative w-full desktop:px-36 tabletB:px-20 mob:px-8',
});
5 changes: 5 additions & 0 deletions ssh-web/src/utils/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { QuizSolving } from '../pages/QuizSolving';
import { Mission } from '../pages/Mission';
import { Information } from '../pages/Information';
import { Manage } from '../pages/Information/Manage';
import { Request } from '../pages/Information/Request';

export const PathNames: IPathNames = {
HOME: {
Expand Down Expand Up @@ -77,6 +78,10 @@ export const router = createBrowserRouter([
path: '/manage',
element: <Manage />,
},
{
path: '/request',
element: <Request />,
},
],
},
]);

0 comments on commit 3c5e6d4

Please sign in to comment.