Skip to content

Commit cf560d0

Browse files
authored
Merge pull request #1608 from 42organization/Feat/#1596-calendar-admin-api
[Feat] 캘린더 어드민 페이지 제작
2 parents 15153ab + 57fa0b3 commit cf560d0

32 files changed

+1892
-1
lines changed

components/admin/SideNav.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FaAngleDown, FaAngleRight } from 'react-icons/fa';
55
import { IoPeople } from 'react-icons/io5';
66
import { RiPingPongFill } from 'react-icons/ri';
77
import SideNavAgenda from 'components/admin/agenda/SideNavAgenda';
8+
import SideNavCalendar from 'components/admin/calendar/SideNavCalendar';
89
import SideNavParty from 'components/admin/takgu/SideNavParty';
910
import SideNavTakgu from 'components/admin/takgu/SideNavTakgu';
1011
import styles from 'styles/admin/SideNav.module.scss';
@@ -13,6 +14,7 @@ export default function SideNav() {
1314
const [isTakguOpen, setTakguOpen] = useState(false);
1415
const [isPartyOpen, setPartyOpen] = useState(false);
1516
const [isAgendaOpen, setAgendaOpen] = useState(false);
17+
const [isCalendarOpen, setCalendarOpen] = useState(false);
1618

1719
const presentPath = useRouter().asPath;
1820
useEffect(() => {
@@ -27,6 +29,9 @@ export default function SideNav() {
2729
case presentPath.includes('/admin/agenda'):
2830
setAgendaOpen(true);
2931
break;
32+
case presentPath.includes('/admin/calendar'):
33+
setCalendarOpen(true);
34+
break;
3035
}
3136
}, [presentPath]);
3237

@@ -76,6 +81,21 @@ export default function SideNav() {
7681
<SideNavAgenda />
7782
</div>
7883
)}
84+
{/* ------------------------------------- */}
85+
<div
86+
className={styles.menuItem}
87+
onClick={() => setCalendarOpen(!isCalendarOpen)}
88+
>
89+
<BsAwardFill />
90+
<span className={styles.menuName}>Calendar 관리</span>
91+
{isCalendarOpen ? <FaAngleDown /> : <FaAngleRight />}
92+
</div>
93+
94+
{isCalendarOpen && (
95+
<div className={styles.subMenu}>
96+
<SideNavCalendar />
97+
</div>
98+
)}
7999
</div>
80100
);
81101
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import styles from 'styles/admin/calendar/CalendarHeader.module.scss';
2+
3+
interface CalendarHeaderProps {
4+
title: string;
5+
}
6+
7+
export const CalendarHeader = ({ title }: CalendarHeaderProps) => {
8+
return (
9+
<div className={styles.container}>
10+
<div className={styles.title}>{title}</div>
11+
</div>
12+
);
13+
};
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { useState } from 'react';
2+
import {
3+
Paper,
4+
Table,
5+
TableBody,
6+
TableCell,
7+
TableContainer,
8+
TableHead,
9+
TableRow,
10+
Box,
11+
Tooltip,
12+
} from '@mui/material';
13+
import { AdminSchedule } from 'types/calendar/scheduleTypes';
14+
import {
15+
CalendarStatus,
16+
CalendarClassification,
17+
} from 'constants/calendar/calendarConstants';
18+
import PageNation from 'components/Pagination';
19+
import { useAdminCalendarDelete } from 'hooks/calendar/admin/useAdminCalendarDelete';
20+
import styles from 'styles/admin/calendar/CalendarTable.module.scss';
21+
import { NoContent } from '../agenda/utils';
22+
23+
interface CalendarTableProps {
24+
data: AdminSchedule[];
25+
}
26+
27+
export const CalendarTable = ({ data }: CalendarTableProps) => {
28+
const [currentPage, setCurrentPage] = useState<number>(1);
29+
const itemsPerPage = 10;
30+
const totalPage = Math.ceil(data.length / itemsPerPage);
31+
const { deleteCalendar } = useAdminCalendarDelete();
32+
33+
const paginatedData = data.slice(
34+
(currentPage - 1) * itemsPerPage,
35+
currentPage * itemsPerPage
36+
);
37+
38+
const calendarTableFormat = {
39+
columns: [
40+
'ID',
41+
'상태',
42+
'분류',
43+
'작성자',
44+
'제목',
45+
'시작 시간',
46+
'종료 시간',
47+
'기타',
48+
],
49+
};
50+
51+
const classificationMap: Record<string, string> = {
52+
JOB_NOTICE: 'JOB',
53+
PRIVATE_SCHEDULE: 'PRIVATE',
54+
EVENT: 'EVENT',
55+
};
56+
57+
const handleDelete = async (id: number) => {
58+
const confirmDelete = window.confirm('정말 삭제하시겠습니까?');
59+
if (!confirmDelete) return;
60+
61+
const success = await deleteCalendar(id);
62+
if (success) {
63+
window.location.reload();
64+
}
65+
};
66+
67+
return (
68+
<div className={styles.container}>
69+
<TableContainer className={styles.tableContainer} component={Paper}>
70+
<Table className={styles.table} aria-label='calendar table'>
71+
{/* Table Header */}
72+
<TableHead className={styles.tableHead}>
73+
<TableRow>
74+
{calendarTableFormat.columns.map((name) => (
75+
<TableCell
76+
key={name}
77+
align={name === 'ID' || name === '상태' ? 'center' : 'left'}
78+
sx={{ whiteSpace: 'nowrap' }}
79+
>
80+
{name}
81+
</TableCell>
82+
))}
83+
</TableRow>
84+
</TableHead>
85+
86+
{/* Table Body */}
87+
{data && data.length > 0 ? (
88+
<TableBody>
89+
{paginatedData.map((row) => (
90+
<TableRow key={row.id} className={styles.tableRow}>
91+
<TableCell className={styles.tableCell} align='center'>
92+
{row.id}
93+
</TableCell>
94+
95+
<TableCell className={styles.tableCell} align='center'>
96+
<Tooltip
97+
title={
98+
row.status === CalendarStatus.DEACTIVATE
99+
? '비활성화'
100+
: row.status === CalendarStatus.ACTIVATE
101+
? '활성화'
102+
: '삭제'
103+
}
104+
placement='top'
105+
arrow
106+
>
107+
<Box
108+
sx={{
109+
width: 12,
110+
height: 12,
111+
borderRadius: '50%',
112+
backgroundColor:
113+
row.status === CalendarStatus.DEACTIVATE
114+
? 'orange'
115+
: row.status === CalendarStatus.ACTIVATE
116+
? 'green'
117+
: 'red',
118+
display: 'inline-block',
119+
transition: 'opacity 0.3s ease',
120+
'&:hover': {
121+
opacity: 0.6,
122+
},
123+
}}
124+
/>
125+
</Tooltip>
126+
</TableCell>
127+
128+
<TableCell className={styles.tableCell}>
129+
{classificationMap[row.classification] ||
130+
row.classification}
131+
</TableCell>
132+
133+
<TableCell className={styles.tableCell}>
134+
{row.author}
135+
</TableCell>
136+
137+
<TableCell
138+
className={styles.tableCell}
139+
sx={{
140+
maxWidth: '300px',
141+
whiteSpace: 'nowrap',
142+
overflow: 'hidden',
143+
textOverflow: 'ellipsis',
144+
}}
145+
>
146+
{row.title}
147+
</TableCell>
148+
149+
<TableCell className={styles.tableCell}>
150+
{new Date(row.startTime).toLocaleString('ko-KR', {
151+
year: 'numeric',
152+
month: '2-digit',
153+
day: '2-digit',
154+
hour: '2-digit',
155+
minute: '2-digit',
156+
hour12: false,
157+
})}
158+
</TableCell>
159+
160+
<TableCell className={styles.tableCell}>
161+
{new Date(row.endTime).toLocaleString('ko-KR', {
162+
year: 'numeric',
163+
month: '2-digit',
164+
day: '2-digit',
165+
hour: '2-digit',
166+
minute: '2-digit',
167+
hour12: false,
168+
})}
169+
</TableCell>
170+
171+
<TableCell className={styles.etcTableCell}>
172+
<button className={`${styles.btn} ${styles.detail}`}>
173+
자세히
174+
</button>
175+
{row.classification !== CalendarClassification.PRIVATE && (
176+
<button className={`${styles.btn} ${styles.modify}`}>
177+
수정
178+
</button>
179+
)}
180+
<button
181+
onClick={() => handleDelete(row.id)}
182+
className={`${styles.btn} ${styles.delete}`}
183+
>
184+
삭제
185+
</button>
186+
</TableCell>
187+
</TableRow>
188+
))}
189+
</TableBody>
190+
) : (
191+
<TableBody>
192+
<NoContent
193+
col={9}
194+
content={
195+
'조회 결과가 없습니다. 조건을 변경하거나 새로운 일정을 추가해보세요.'
196+
}
197+
/>
198+
</TableBody>
199+
)}
200+
</Table>
201+
</TableContainer>
202+
203+
<div className={styles.pageNationContainer}>
204+
<PageNation
205+
curPage={currentPage}
206+
totalPages={totalPage}
207+
pageChangeHandler={(pageNumber: number) => {
208+
setCurrentPage(pageNumber);
209+
}}
210+
/>
211+
</div>
212+
</div>
213+
);
214+
};

0 commit comments

Comments
 (0)