Skip to content

Commit 2434cf7

Browse files
authored
Merge pull request #103 from Stack-Knowledge/feature/approveList
[Admin] approve list
2 parents e3262b3 + 67e053b commit 2434cf7

File tree

18 files changed

+229
-8
lines changed

18 files changed

+229
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from './useGetApprovedList';
12
export * from './useGetScoringList';
23
export * from './useGetSolveDetail';
4+
export * from './usePatchApprovalStatus';
35
export * from './usePostScoringResult';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
3+
import { userQueryKeys, userUrl, get } from 'api/common';
4+
5+
import type { UseQueryOptions } from '@tanstack/react-query';
6+
import type { ApprovalStatusType } from 'types';
7+
8+
export const useGetApprovedList = (
9+
options?: UseQueryOptions<ApprovalStatusType[]>
10+
) =>
11+
useQuery<ApprovalStatusType[]>(
12+
userQueryKeys.getApprovedList(),
13+
() => get(userUrl.approvedList()),
14+
options
15+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
3+
import { userQueryKeys, userUrl, patch } from 'api/common';
4+
5+
interface ApprovedStatus {
6+
approveStatus: 'REJECT' | 'APPROVED';
7+
}
8+
9+
export const usePatchApprovalStatus = (userId: string) =>
10+
useMutation<void, Error, ApprovedStatus>(
11+
userQueryKeys.patchApprovedStatus(userId),
12+
(data) => patch(userUrl.approvedStatus(userId), data)
13+
);

packages/api/common/src/hooks/auth/usePostLoginCode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useMutation } from '@tanstack/react-query';
22

3-
import { post, authQueryKeys, authUrl } from 'api/common';
3+
import { authQueryKeys, authUrl, post } from 'api/common';
44

55
import type { TokenResponseLoginType } from 'types';
66

packages/api/common/src/libs/queryKeys.ts

+2
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ export const userQueryKeys = {
3434
getScoringList: () => ['user', 'scoring'],
3535
postScoringResult: (solveId: string) => ['user', 'scoring', solveId],
3636
getSolveDetail: (solveId: string) => ['user', 'solve', solveId],
37+
getApprovedList: () => ['user', 'list', 'approval'],
38+
patchApprovedStatus: (userId: string) => ['user', 'approved', userId],
3739
};

packages/api/common/src/libs/urlController.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@ export const userUrl = {
3131
scoring: () => '/user/scoring',
3232
scoringResult: (solveId: string) => `/user/scoring/${solveId}`,
3333
solveDetail: (solveId: string) => `/user/scoring/${solveId}`,
34+
approvedList: () => '/user/teacher',
35+
approvedStatus: (userId: string) => `/user/${userId}`,
3436
};

packages/common/src/components/Header/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const Header: React.FC<HeaderProps> = ({ role }) => {
5353
toast.success('로그아웃 되었습니다.');
5454
}
5555

56-
if (isError) toast.success('로그아웃에 실패했습니다.');
56+
if (isError) toast.error('로그아웃에 실패했습니다.');
5757

5858
return (
5959
<S.HeaderWrapper>

packages/types/src/approvalStatus.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface ApprovalStatusType {
2+
name: string;
3+
createdAt: string; // LocalDateTime
4+
userId: string; // UUID
5+
}

packages/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './solveStatusType';
99
export * from './missionDetail';
1010
export * from './studentType';
1111
export * from './uploadProfile';
12+
export * from './approvalStatus';

projects/admin/src/PageContainer/MainPage/index.tsx

+31-3
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,58 @@
22

33
import { useRef, useState } from 'react';
44

5-
import { XIcon } from 'admin/assets';
6-
import { ApproveModalButton } from 'admin/components';
5+
import { toast } from 'react-toastify';
6+
7+
import { CapIcon, XIcon } from 'admin/assets';
8+
import { ModalItem, ApproveModalButton } from 'admin/components';
9+
10+
import { useGetApprovedList } from 'api/admin';
711

812
import { MainPage } from 'common';
913

1014
import * as S from './style';
1115

1216
const MainPageComponent = () => {
13-
const dialog = useRef<HTMLDialogElement>(null);
17+
const { data, refetch } = useGetApprovedList();
1418

19+
const dialog = useRef<HTMLDialogElement>(null);
1520
const [isOpen, setIsOpen] = useState<boolean>(false);
1621

1722
const handleModalOpen = () => {
1823
dialog.current?.showModal();
1924
setIsOpen(true);
2025
};
2126

27+
const handleSuccessApproved = (isAccepted: boolean) => {
28+
refetch();
29+
isAccepted
30+
? toast.success('수락되었습니다.')
31+
: toast.error('거절되었습니다.');
32+
};
33+
2234
return (
2335
<>
2436
<S.Modal ref={dialog} isOpen={isOpen}>
2537
<form method='dialog'>
2638
<S.ModalButton onClick={() => setIsOpen(false)}>
2739
<XIcon />
2840
</S.ModalButton>
41+
<S.ModalWrapper>
42+
{data && data.length > 0 ? (
43+
data.map((item) => (
44+
<ModalItem
45+
key={item.userId}
46+
teacherItem={item}
47+
onSuccessApproved={handleSuccessApproved}
48+
/>
49+
))
50+
) : (
51+
<S.ApprovedNone>
52+
<CapIcon />
53+
<span>대기중인 선생님이 없습니다..</span>
54+
</S.ApprovedNone>
55+
)}
56+
</S.ModalWrapper>
2957
</form>
3058
</S.Modal>
3159
<S.Wrapper onClick={handleModalOpen}>

projects/admin/src/PageContainer/MainPage/style.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,23 @@ const slideAndScaleAnimation = keyframes`
1010
}
1111
`;
1212

13+
export const ApprovedNone = styled.div`
14+
width: 15.875rem;
15+
height: 100%;
16+
flex-direction: column;
17+
display: flex;
18+
padding-top: 2.875rem;
19+
align-items: center;
20+
gap: 1.9756rem;
21+
22+
span {
23+
${({ theme }) => theme.typo.body1};
24+
color: ${({ theme }) => theme.color.gray['030']};
25+
}
26+
`;
27+
1328
export const Modal = styled.dialog<{ isOpen: boolean }>`
14-
width: 21.875rem;
29+
width: fit-content;
1530
height: 24.0625rem;
1631
border-radius: 1.25rem;
1732
background: #fbfbfb;
@@ -40,9 +55,18 @@ export const ModalButton = styled.button`
4055
outline: none;
4156
`;
4257

58+
export const ModalWrapper = styled.div`
59+
width: 100%;
60+
height: 24.0625rem;
61+
padding: 2rem 3.1875rem 2rem 2.5625rem;
62+
display: flex;
63+
flex-direction: column;
64+
gap: 1.625rem;
65+
`;
66+
4367
export const Wrapper = styled.div`
4468
position: absolute;
45-
right: 20vw;
46-
top: 75vh;
69+
right: 9vw;
70+
top: 88vh;
4771
z-index: 3;
4872
`;

projects/admin/src/assets/BarIcon.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const BarIcon = () => (
2+
<svg
3+
width='0.5rem'
4+
height='0.0625rem'
5+
viewBox='0 0 8 1'
6+
fill='none'
7+
xmlns='http://www.w3.org/2000/svg'
8+
>
9+
<rect width='8' height='1' rx='0.5' fill='black' />
10+
</svg>
11+
);
12+
13+
export default BarIcon;

projects/admin/src/assets/CapIcon.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const CapIcon = () => (
2+
<svg
3+
width='5.125rem'
4+
height='4.375rem'
5+
viewBox='0 0 82 70'
6+
fill='none'
7+
xmlns='http://www.w3.org/2000/svg'
8+
>
9+
<path
10+
d='M11.975 27.0511C10.9436 34.947 10.329 42.8918 10.1338 50.8524C20.9293 55.3618 31.2657 60.8998 41 67.3899C50.7356 60.8996 61.0732 55.3616 71.87 50.8524C71.6747 42.8918 71.0601 34.947 70.0288 27.0511M70.0288 27.0511C73.31 25.9486 76.6363 24.9286 79.9963 23.9986C67.7584 15.4196 54.6949 8.08321 41 2.09863C27.305 8.08447 14.2415 15.4221 2.00378 24.0024C5.35356 24.9272 8.67731 25.9439 11.9713 27.0511C21.9786 30.4157 31.6884 34.6076 41 39.5836C50.3103 34.6076 60.0226 30.4156 70.0288 27.0511ZM21.3125 45.2499C22.0585 45.2499 22.7738 44.9536 23.3013 44.4261C23.8287 43.8987 24.125 43.1833 24.125 42.4374C24.125 41.6915 23.8287 40.9761 23.3013 40.4486C22.7738 39.9212 22.0585 39.6249 21.3125 39.6249C20.5666 39.6249 19.8512 39.9212 19.3238 40.4486C18.7964 40.9761 18.5 41.6915 18.5 42.4374C18.5 43.1833 18.7964 43.8987 19.3238 44.4261C19.8512 44.9536 20.5666 45.2499 21.3125 45.2499ZM21.3125 45.2499V31.4686C27.6753 27.5136 34.2474 23.9059 41 20.6611M14.7238 63.9736C16.8162 61.8864 18.4754 59.4062 19.6061 56.6756C20.7369 53.945 21.3168 51.0178 21.3125 48.0624V42.4374'
11+
stroke='#D9D9D9'
12+
stroke-width='4'
13+
stroke-linecap='round'
14+
stroke-linejoin='round'
15+
/>
16+
</svg>
17+
);
18+
19+
export default CapIcon;

projects/admin/src/assets/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export { default as BarIcon } from './BarIcon';
2+
export { default as CapIcon } from './CapIcon';
13
export { default as XIcon } from './XIcon';

projects/admin/src/components/ModalItem/index.stories.tsx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import { BarIcon } from 'admin/assets';
4+
5+
import { usePatchApprovalStatus } from 'api/admin';
6+
7+
import * as S from './style';
8+
9+
import type { ApprovalStatusType } from 'types';
10+
11+
interface ModalItemProps {
12+
teacherItem: ApprovalStatusType;
13+
// eslint-disable-next-line no-unused-vars
14+
onSuccessApproved: (isAccepted: boolean) => void;
15+
}
16+
17+
const ModalItem: React.FC<ModalItemProps> = ({
18+
teacherItem,
19+
onSuccessApproved,
20+
}) => {
21+
const { userId, name, createdAt } = teacherItem;
22+
const { mutate } = usePatchApprovalStatus(userId);
23+
24+
const formatDate = (isoDate: string) =>
25+
isoDate.slice(0, 10).replaceAll('-', '.');
26+
27+
const handleApproval = (approveStatus: 'APPROVED' | 'REJECT') => {
28+
mutate({ approveStatus });
29+
onSuccessApproved(approveStatus === 'APPROVED');
30+
};
31+
32+
return (
33+
<S.ModalItem id={userId}>
34+
<S.TitleContainer>
35+
<S.Title>{name}</S.Title>
36+
<BarIcon />
37+
<S.Title>{formatDate(createdAt)}</S.Title>
38+
</S.TitleContainer>
39+
<S.ApprovedContainer>
40+
<S.ApprovedButton onClick={() => handleApproval('APPROVED')}>
41+
수락
42+
</S.ApprovedButton>
43+
<S.ApprovedButton onClick={() => handleApproval('REJECT')}>
44+
거절
45+
</S.ApprovedButton>
46+
</S.ApprovedContainer>
47+
</S.ModalItem>
48+
);
49+
};
50+
51+
export default ModalItem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import styled from '@emotion/styled';
2+
3+
export const ApprovedButton = styled.div`
4+
background-color: ${({ theme }) => theme.color.gray['020']};
5+
color: ${({ theme }) => theme.color.black};
6+
${({ theme }) => theme.typo.body3};
7+
display: flex;
8+
border-radius: 0.125rem;
9+
justify-content: center;
10+
align-items: center;
11+
width: 2.8125rem;
12+
height: 1.3125rem;
13+
cursor: pointer;
14+
15+
&:hover {
16+
background-color: ${({ theme }) => theme.color.primary};
17+
color: ${({ theme }) => theme.color.white};
18+
}
19+
`;
20+
21+
export const ApprovedContainer = styled.div`
22+
display: flex;
23+
margin-left: 20px;
24+
gap: 0.5rem;
25+
`;
26+
27+
export const ModalItem = styled.div`
28+
width: 100%;
29+
align-items: center;
30+
display: flex;
31+
`;
32+
33+
export const Title = styled.span`
34+
${({ theme }) => theme.typo.body2};
35+
color: ${({ theme }) => theme.color.black};
36+
`;
37+
38+
export const TitleContainer = styled.div`
39+
display: flex;
40+
align-items: center;
41+
gap: 0.375rem;
42+
width: 100%;
43+
`;

projects/admin/src/components/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { default as ApproveModalButton } from './ApproveModalButton';
22
export { default as GradingContainer } from './GradingCotainer';
33
export { default as MissionCarousel } from './MissionCarousel';
4+
export { default as ModalItem } from './ModalItem';
45
export { default as ShopCarousel } from './ShopCarousel';
56
export { default as ShopItemCard } from './ShopItemCard';
67
export { default as Timer } from './Timer';

0 commit comments

Comments
 (0)