Skip to content

Commit 21c0918

Browse files
authored
Feat/50/dashboard edit (#97)
* โœจ feat : ๊ณต์šฉ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ž‘์—… * โœจ feat : ํŽ˜์ด์ง€๋„ค์ด์…˜ + ์นด์šดํ„ฐ ์กฐํ•ฉ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ * โœจ feat : ๋Œ€์‹œ๋ณด๋“œ ์ˆ˜์ • - ์ดˆ๋Œ€๋‚ด์—ญ ์ปดํฌ๋„ŒํŠธ ์ž‘์—… * โ™ป๏ธ refactor : ์ดˆ๋Œ€๋ฐ›์€ ๋Œ€์‹œ๋ณด๋“œ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ๊ต์ฒด * โšก๏ธ performance : react query retry ์˜ต์…˜ ๋„๊ธฐ * โ™ป๏ธ refactor : ๋Œ€์‹œ๋ณด๋“œ ์ˆ˜์ • - ๋’ค๋กœ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ ๊ต์ฒด
1 parent 9a59321 commit 21c0918

7 files changed

Lines changed: 249 additions & 54 deletions

File tree

โ€Žsrc/app/(after-login)/dashboard/[id]/edit/page.tsxโ€Ž

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getErrorMessage } from '@/utils/errorMessage';
88
import DetailModify from '@/components/dashboard/DetailModify';
99
import DetailMembers from '@/components/dashboard/DetailMembers';
1010
import DetailInvited from '@/components/dashboard/DetailInvited';
11-
import Link from 'next/link';
11+
import GoBackLink from '@/components/ui/Link/GoBackLink';
1212

1313
export default function DashboardEditPage() {
1414
const router = useRouter();
@@ -29,9 +29,8 @@ export default function DashboardEditPage() {
2929

3030
return (
3131
<div className='p-10'>
32-
{/* TODO : ๋Œ์•„๊ฐ€๊ธฐ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ต์ฒด ํ•„์š” */}
3332
<div className='mb-8'>
34-
<Link href={`/dashboard/${id}`}>๋Œ์•„๊ฐ€๊ธฐ</Link>
33+
<GoBackLink href={`/dashboard/${id}`} />
3534
</div>
3635
<div className='grid w-full max-w-[620px] gap-4'>
3736
{/* ๋Œ€์‹œ๋ณด๋“œ ์ •๋ณด */}
@@ -44,7 +43,6 @@ export default function DashboardEditPage() {
4443
<DetailInvited />
4544

4645
{/* ๋Œ€์‹œ๋ณด๋“œ ์‚ญ์ œ */}
47-
{/* TODO : ๋Œ€์‹œ๋ณด๋“œ ๊ณต์šฉ ๋ฒ„ํŠผ ๊ต์ฒด ํ•„์š” */}
4846
<Button variant='outline' onClick={handleDelete}>
4947
๋Œ€์‹œ๋ณด๋“œ ์‚ญ์ œํ•˜๊ธฐ
5048
</Button>

โ€Žsrc/components/dashboard/DetailInvited.tsxโ€Ž

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,98 @@
1-
import { useRef } from 'react';
1+
import { useRef, useState } from 'react';
22
import Button from '@/components/ui/Button/Button';
33
import { Card, CardTitle } from '@/components/ui/Card/Card';
44
import { ModalHandle } from '@/components/ui/Modal/Modal';
55
import InviteDashboard from './InviteDashboard';
6+
import { useParams } from 'next/navigation';
7+
import { useDashboardInvitationsQuery, useDashboardMutation } from '@/apis/dashboards/queries';
8+
import { getErrorMessage } from '@/utils/errorMessage';
9+
import useAlert from '@/hooks/useAlert';
10+
import { Table, TableBody, TableCell, TableCol, TableColGroup, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table/Table';
11+
import PaginationWithCounter from '@/components/pagination/PaginationWithCounter';
12+
import { isAxiosError } from 'axios';
13+
14+
const PAGE_SIZE = 5;
615

716
export default function DetailInvited() {
17+
const { id } = useParams<{ id: string }>();
18+
const [page, setPage] = useState(1);
19+
const { data, error, isLoading } = useDashboardInvitationsQuery(Number(id), page, PAGE_SIZE);
20+
const { cancel } = useDashboardMutation();
21+
const alert = useAlert();
822
const inviteModalRef = useRef<ModalHandle | null>(null);
923

24+
const cancelInvite = async (invitationId: number) => {
25+
try {
26+
await cancel({ dashboardId: Number(id), invitationId });
27+
alert('์ดˆ๋Œ€๋ฅผ ์ทจ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค.');
28+
} catch (error) {
29+
const message = getErrorMessage(error);
30+
alert(message);
31+
}
32+
};
33+
const notAllowed = isAxiosError(error) && error.status === 403;
34+
1035
return (
1136
<Card>
1237
<CardTitle>
1338
์ดˆ๋Œ€๋‚ด์—ญ
1439
<div className='leading-none'>
15-
<Button size='sm' onClick={() => inviteModalRef.current?.open()}>
16-
์ดˆ๋Œ€ํ•˜๊ธฐ
17-
</Button>
40+
<PaginationWithCounter //
41+
totalCount={data?.totalCount || 0}
42+
page={page}
43+
setPage={setPage}
44+
pageSize={PAGE_SIZE}
45+
/>
1846
</div>
1947
</CardTitle>
20-
<div>๋Œ€์‹œ๋ณด๋“œ ์ดˆ๋Œ€๋‚ด์—ญ(์ž‘์—…ํ•„์š”)</div>
48+
<Table className='mb-4'>
49+
<TableColGroup>
50+
<TableCol />
51+
<TableCol className='w-24' />
52+
</TableColGroup>
53+
<TableHead>
54+
<TableRow>
55+
<TableHeadCell>์ด๋ฉ”์ผ</TableHeadCell>
56+
<TableHeadCell></TableHeadCell>
57+
</TableRow>
58+
</TableHead>
59+
<TableBody>
60+
{isLoading && (
61+
<TableRow>
62+
<TableCell colSpan={2}>
63+
<div className='p-4 text-center'>์ดˆ๋Œ€๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.</div>
64+
</TableCell>
65+
</TableRow>
66+
)}
67+
{notAllowed && (
68+
<TableRow>
69+
<TableCell colSpan={2}>
70+
<div className='p-4 text-center'>๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.</div>
71+
</TableCell>
72+
</TableRow>
73+
)}
74+
{data?.invitations.length === 0 && (
75+
<TableRow>
76+
<TableCell colSpan={2}>
77+
<div className='p-4 text-center'>์ดˆ๋Œ€ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค.</div>
78+
</TableCell>
79+
</TableRow>
80+
)}
81+
{data?.invitations.map((item) => (
82+
<TableRow key={item.id}>
83+
<TableCell>{item.invitee.email}</TableCell>
84+
<TableCell>
85+
<Button variant='outline' size='sm' onClick={() => cancelInvite(item.id)}>
86+
์ทจ์†Œ
87+
</Button>
88+
</TableCell>
89+
</TableRow>
90+
))}
91+
</TableBody>
92+
</Table>
93+
<Button className='w-full' onClick={() => inviteModalRef.current?.open()}>
94+
์ดˆ๋Œ€ํ•˜๊ธฐ
95+
</Button>
2196

2297
{/* ์ดˆ๋Œ€๋ชจ๋‹ฌ */}
2398
<InviteDashboard ref={inviteModalRef} />

โ€Žsrc/components/dashboard/MyDashboardList.tsxโ€Ž

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import { useState } from 'react';
44
import Link from 'next/link';
5-
import PaginationControls from '@/components/pagination/PaginationControls';
65
import MyDashboardCard from '@/components/dashboard/MyDashboardCard';
76
import { useDashboardsQuery } from '@/apis/dashboards/queries';
87
import DashboardButton from '../ui/Button/DashboardButton';
8+
import PaginationWithCounter from '../pagination/PaginationWithCounter';
99

1010
interface MyDashboardListProps {
1111
onAdd: () => void;
@@ -17,18 +17,6 @@ export default function MyDashboardList({ onAdd }: MyDashboardListProps) {
1717
const [page, setPage] = useState(1);
1818
const { data, isLoading } = useDashboardsQuery(page, ITEMS_PER_PAGE);
1919

20-
const totalCount = data?.totalCount || 0;
21-
const totalPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
22-
const hasNext = page < totalPage;
23-
const hasPrev = page > 1;
24-
25-
const handlePrev = () => {
26-
setPage((prev) => prev - 1);
27-
};
28-
const handleNext = () => {
29-
setPage((prev) => prev + 1);
30-
};
31-
3220
return (
3321
<div className='grid gap-3'>
3422
<ul className='grid-row-6 md:grid-row-3 relative grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3 lg:grid-rows-2'>
@@ -47,14 +35,12 @@ export default function MyDashboardList({ onAdd }: MyDashboardListProps) {
4735
))}
4836
</ul>
4937

50-
{totalCount > 0 && (
51-
<div className='flex items-center justify-end gap-4'>
52-
<span className='text-md text-gray-70'>
53-
{totalPage} ํŽ˜์ด์ง€์ค‘ {page}
54-
</span>
55-
<PaginationControls canGoPrev={hasPrev} canGoNext={hasNext} handlePrev={handlePrev} handleNext={handleNext} totalPages={totalCount} alwaysShow />
56-
</div>
57-
)}
38+
<PaginationWithCounter //
39+
totalCount={data?.totalCount || 0}
40+
page={page}
41+
setPage={setPage}
42+
pageSize={ITEMS_PER_PAGE}
43+
/>
5844
</div>
5945
);
6046
}

โ€Žsrc/components/dashboard/MyInvitedDashboardList.tsxโ€Ž

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import MyInvitedEmptyCard from './MyInvitedEmptyCard';
1010
import { SearchInput } from '../ui/Field';
1111
import useAlert from '@/hooks/useAlert';
1212
import { getErrorMessage } from '@/utils/errorMessage';
13+
import { Table, TableBody, TableCell, TableCol, TableColGroup, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table/Table';
1314

1415
export default function MyInvitedDashboardList() {
1516
const [searchKeyword, setSearchKeyword] = useState('');
@@ -47,42 +48,48 @@ export default function MyInvitedDashboardList() {
4748
{hasNoInvitations ? (
4849
<MyInvitedEmptyCard>์•„์ง ์ดˆ๋Œ€๋ฐ›์€ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์—†์–ด์š”.</MyInvitedEmptyCard>
4950
) : (
50-
<div className='grid gap-10'>
51+
<div className='grid gap-4 md:gap-10'>
5152
<SearchInput value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} placeholder='๊ฒ€์ƒ‰' />
5253
{hasNoSearchResults ? (
5354
<MyInvitedEmptyCard>๊ฒ€์ƒ‰๋œ ์ดˆ๋Œ€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</MyInvitedEmptyCard>
5455
) : (
55-
// TODO : ๋ฆฌ์ŠคํŠธ ํ…Œ์ด๋ธ” ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธํ™” ํ•„์š”
56-
<div className='flex flex-col gap-6'>
57-
<div className='px-8'>
58-
<div className='hidden md:block'>
59-
<div className='mb-4 grid grid-cols-[1fr_1fr_2fr] gap-5 text-lg text-gray-40 lg:pl-10'>
60-
<span>์ด๋ฆ„</span>
61-
<span>์ดˆ๋Œ€์ž</span>
62-
<span>์ˆ˜๋ฝ ์—ฌ๋ถ€</span>
63-
</div>
64-
<div className='flex flex-col gap-5'>
65-
{invitations.map((item) => (
66-
<div key={item.id} className='grid grid-cols-[1fr_1fr_2fr] items-center gap-5 border-b pb-5 text-lg text-gray-70 lg:pl-10'>
67-
<span>{item.dashboard.title}</span>
68-
<span>{item.inviter.nickname}</span>
69-
<div className='flex gap-2.5'>
56+
<>
57+
<Table responsive>
58+
<TableColGroup className='hidden md:table-column-group'>
59+
<TableCol />
60+
<TableCol className='w-[20%]' />
61+
<TableCol className='w-48' />
62+
</TableColGroup>
63+
<TableHead>
64+
<TableRow>
65+
<TableHeadCell>์ด๋ฆ„</TableHeadCell>
66+
<TableHeadCell>์ดˆ๋Œ€์ž</TableHeadCell>
67+
<TableHeadCell>์ˆ˜๋ฝ ์—ฌ๋ถ€</TableHeadCell>
68+
</TableRow>
69+
</TableHead>
70+
<TableBody>
71+
{invitations.map((item) => (
72+
<TableRow key={item.id}>
73+
<TableCell label='์ด๋ฆ„'>{item.dashboard.title}</TableCell>
74+
<TableCell label='์ดˆ๋Œ€์ž'>{item.inviter.nickname}</TableCell>
75+
<TableCell>
76+
<div className='flex w-full gap-2'>
7077
<Button size='sm' onClick={() => handleAccept({ id: item.id, flag: true })}>
7178
์ˆ˜๋ฝ
7279
</Button>
7380
<Button variant='outline' size='sm' onClick={() => handleAccept({ id: item.id, flag: false })}>
7481
๊ฑฐ์ ˆ
7582
</Button>
7683
</div>
77-
</div>
78-
))}
79-
</div>
80-
</div>
84+
</TableCell>
85+
</TableRow>
86+
))}
87+
</TableBody>
88+
</Table>
8189

82-
{/* ๊ณตํ†ต load more ํŠธ๋ฆฌ๊ฑฐ ์š”์†Œ */}
83-
<div ref={ref} className='h-8' />
84-
</div>
85-
</div>
90+
{/* ๊ณตํ†ต load more ํŠธ๋ฆฌ๊ฑฐ ์š”์†Œ */}
91+
<div ref={ref} className='h-2' />
92+
</>
8693
)}
8794
</div>
8895
)}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Dispatch, SetStateAction } from 'react';
2+
import PaginationControls from './PaginationControls';
3+
4+
interface PaginationWithCounterProps {
5+
totalCount: number;
6+
page: number;
7+
setPage: Dispatch<SetStateAction<number>>;
8+
pageSize: number;
9+
}
10+
11+
export default function PaginationWithCounter({ totalCount, page, setPage, pageSize }: PaginationWithCounterProps) {
12+
const totalPage = Math.ceil(totalCount / pageSize);
13+
const hasNext = page < totalPage;
14+
const hasPrev = page > 1;
15+
16+
const handlePrev = () => {
17+
setPage((prev) => prev - 1);
18+
};
19+
const handleNext = () => {
20+
setPage((prev) => prev + 1);
21+
};
22+
23+
return (
24+
<div className='flex items-center justify-end gap-4 font-normal'>
25+
{totalCount > 0 && (
26+
<span className='text-md text-gray-70'>
27+
{totalPage} ํŽ˜์ด์ง€์ค‘ {page}
28+
</span>
29+
)}
30+
<PaginationControls canGoPrev={hasPrev} canGoNext={hasNext} handlePrev={handlePrev} handleNext={handleNext} totalPages={totalPage} alwaysShow />
31+
</div>
32+
);
33+
}

โ€Žsrc/components/provider/QueryProvider.tsxโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const queryClient = new QueryClient({
77
defaultOptions: {
88
queries: {
99
staleTime: 60 * 1000, // 1๋ถ„
10+
retry: false,
1011
},
1112
},
1213
});

0 commit comments

Comments
ย (0)