Skip to content

Commit ba7dd69

Browse files
authored
[EC-75] FE/fat: 회의실 예약 API 연동 (#144)
* [EC-75] chore: WIP * [EC-75] chore: WIP * [EC-75] chore: WIP * [EC-75] chore: WIP * [EC-75] feat: 전체 조회, 생성 API 연동 * [EC-75] feat: 취소 API 연동 * [EC-75] chore: 타임존
1 parent 92c7df6 commit ba7dd69

File tree

5 files changed

+166
-55
lines changed

5 files changed

+166
-55
lines changed

client/src/api/instance/apiInstance.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,13 @@ const apiInstance = axios.create({
99
const NO_TOKEN_REQUIRED = ['/api/auth/login', '/api/auth/signup', '/api/auth/refresh', '/'];
1010

1111
apiInstance.interceptors.request.use((config) => {
12-
// console.log(config);
1312
const accessToken = store.getState().auth.accessToken;
1413
const isRequiredTokenUrl = !NO_TOKEN_REQUIRED.includes(config.url);
1514

1615
if (isRequiredTokenUrl) {
1716
config.headers['Authorization'] = `Bearer ${accessToken}`;
18-
// console.log(config.headers);
1917
}
2018

21-
// console.log(config);
2219

2320
return config;
2421
});

client/src/api/reservationApi.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import apiInstance from './instance/apiInstance';
22

33
export const reservationApi = {
4-
getReservations: async () => {
5-
const response = await apiInstance.get(`/campuses/1/meeting-rooms/reservations`);
4+
getReservations: async (campusId) => {
5+
// /api/campuses/{campusId}/meeting-rooms/reservations
6+
const response = await apiInstance.get(`/campuses/${campusId}/meeting-rooms/reservations`);
7+
return response;
8+
},
9+
10+
createReservation: async (campusId, requestBody) => {
11+
await apiInstance.post(`/campuses/${campusId}/meeting-rooms/reservations`, requestBody);
12+
return response;
13+
},
14+
15+
cancelReservation: async (campusId, meetingRoomReservationId) => {
16+
await apiInstance.delete(
17+
`/campuses/${campusId}/meeting-rooms/reservations/${meetingRoomReservationId}`,
18+
);
619
return response;
720
},
821
};

client/src/constants/sidebar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const sidebarList = {
44
STUDENT: [
55
{ name: '출석', icon: '/assets/calender-icon.png', path: URL_PATHS.STUDENT.ATTENDANCE.BASE },
66
{ name: '회의실 예약', icon: '/assets/alarm-icon.png', path: URL_PATHS.STUDENT.RESERVATION },
7-
{ name: '설정', icon: '/assets/alarm-icon.png', path: URL_PATHS.STUDENT.SETTING },
7+
{ name: '설정', icon: '/assets/setting-icon.png', path: URL_PATHS.STUDENT.SETTING },
88
],
99

1010
MIDDLE_ADMIN: [
Lines changed: 149 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,210 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import FullCalendar from '@fullcalendar/react';
3-
import timeGridPlugin from '@fullcalendar/timegrid';
3+
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
44
import interactionPlugin from '@fullcalendar/interaction';
5+
import { reservationApi } from '../../../api/reservationApi';
6+
import { useSelector } from 'react-redux';
57

6-
const DUMMY_RESERVATIONS = [
7-
{
8-
id: '1',
9-
title: '회의실 A',
10-
start: '2024-03-29T09:00:00',
11-
end: '2024-03-29T10:30:00',
12-
backgroundColor: '#4CAF50',
13-
borderColor: '#45a049',
14-
},
15-
{
16-
id: '2',
17-
title: '세미나실 B',
18-
start: '2024-03-29T13:00:00',
19-
end: '2024-03-29T14:30:00',
20-
backgroundColor: '#2196F3',
21-
borderColor: '#1976D2',
22-
},
23-
{
24-
id: '3',
25-
title: '교육장 C',
26-
start: '2024-03-29T16:00:00',
27-
end: '2024-03-29T17:00:00',
28-
backgroundColor: '#FF9800',
29-
borderColor: '#F57C00',
30-
},
31-
];
32-
33-
const ReservationDayTimeline = () => {
34-
const [events, setEvents] = useState(DUMMY_RESERVATIONS);
8+
const MeetingRoomSchedule = () => {
9+
const [resources, setResources] = useState([]);
10+
const [events, setEvents] = useState([]);
3511
const [selectedDate, setSelectedDate] = useState(new Date());
12+
const [loading, setLoading] = useState(true);
13+
const campusId = useSelector((state) => state.auth.user.campusId);
14+
const courseId = useSelector((state) => state.auth.user.courseId);
15+
console.log(campusId);
16+
useEffect(() => {
17+
if (!campusId) return;
18+
19+
const fetchData = async () => {
20+
try {
21+
const response = await reservationApi.getReservations(campusId);
22+
const meetingRooms = response.data.data.meetingRooms;
23+
24+
// 회의실 데이터 변환
25+
const resourcesData = meetingRooms.map((room) => ({
26+
id: room.meetingRoomId.toString(),
27+
roomName: room.meetingRoomName,
28+
}));
29+
30+
setResources(resourcesData);
31+
32+
// 이벤트(예약) 데이터 변환
33+
const eventsData = meetingRooms.flatMap((room) =>
34+
room.reservations.map((reservation) => ({
35+
id: `${room.meetingRoomId}-${reservation.reserverId}`,
36+
resourceId: room.meetingRoomId.toString(),
37+
title: `${reservation.reserverName} 예약`,
38+
start: formatDateTime(reservation.startDateTime),
39+
end: formatDateTime(reservation.endDateTime),
40+
extendedProps: {
41+
reserverId: reservation.reserverId,
42+
reserverName: reservation.reserverName,
43+
},
44+
backgroundColor: getRandomColor(),
45+
})),
46+
);
47+
48+
setEvents(eventsData);
49+
50+
// API 응답에서 첫 예약 날짜를 가져와 초기 날짜로 설정
51+
if (eventsData.length > 0) {
52+
setSelectedDate(new Date(eventsData[0].start));
53+
}
54+
} catch (error) {
55+
console.error('데이터 불러오기 실패:', error);
56+
} finally {
57+
setLoading(false);
58+
}
59+
};
60+
61+
fetchData();
62+
}, [campusId]);
3663

3764
// 날짜 선택 시 처리
3865
const handleDateSet = (dateInfo) => {
3966
setSelectedDate(dateInfo.view.currentStart);
4067
};
4168

42-
// 타임슬롯 클릭 핸들러
43-
const handleTimeSlotSelect = (selectInfo) => {
44-
const title = prompt('새 예약 이름을 입력하세요:');
69+
const formatToLocalDateTime = (isoString) => {
70+
return isoString.split('+')[0];
71+
};
72+
73+
const handleTimeSlotSelect = async (selectInfo) => {
74+
const confirmed = window.confirm('이 시간에 예약하시겠습니까?');
75+
if (!confirmed) return;
76+
77+
try {
78+
const requestBody = {
79+
startTime: formatToLocalDateTime(selectInfo.startStr),
80+
endTime: formatToLocalDateTime(selectInfo.endStr),
81+
meetingRoomId: selectInfo.resource.id,
82+
courseId: courseId,
83+
};
84+
85+
console.log(selectInfo.startStr);
86+
console.log(selectInfo.endStr);
87+
88+
const response = await reservationApi.createReservation(campusId, requestBody);
89+
console.log(response);
90+
91+
if (response.status === 201) {
92+
alert('예약이 완료되었습니다.');
93+
}
94+
} catch (error) {
95+
console.error('예약 중 오류', error);
96+
}
97+
4598
if (title) {
4699
const newEvent = {
47-
id: String(events.length + 1),
48-
title,
100+
id: `new-${Date.now()}`,
101+
resourceId: selectInfo.resource.id,
102+
title: `${title} 예약`,
49103
start: selectInfo.startStr,
50104
end: selectInfo.endStr,
51-
backgroundColor: '#9C27B0',
52-
borderColor: '#7B1FA2',
105+
extendedProps: {
106+
reserverId: Date.now(),
107+
reserverName: title,
108+
},
109+
backgroundColor: getRandomColor(),
53110
};
54111

55112
setEvents([...events, newEvent]);
56113
}
57114
};
58115

59116
// 이벤트 클릭 핸들러
60-
const handleEventClick = (clickInfo) => {
117+
const handleEventClick = async (clickInfo) => {
118+
const event = clickInfo.event;
119+
const resourceId = event.getResources()[0].id;
120+
const resourceTitle = resources.find((r) => r.id === resourceId)?.title;
121+
const reserverName = event.extendedProps.reserverName;
122+
61123
const confirmed = window.confirm(`
62-
${clickInfo.event.title} 예약 정보
63-
시작: ${new Date(clickInfo.event.start).toLocaleTimeString()}
64-
종료: ${new Date(clickInfo.event.end).toLocaleTimeString()}
124+
예약 정보:
125+
예약자: ${reserverName}
126+
장소: ${resourceTitle}
127+
시작: ${formatDisplayTime(event.start)}
128+
종료: ${formatDisplayTime(event.end)}
65129
66130
삭제하시겠습니까?
67131
`);
68132

69133
if (confirmed) {
70-
setEvents(events.filter((event) => event.id !== clickInfo.event.id));
134+
setEvents(events.filter((e) => e.id !== event.id));
71135
}
136+
137+
console.log(resourceId);
138+
const response = await reservationApi.cancelReservation(campusId, resourceId);
139+
console.log(response);
140+
};
141+
142+
// 날짜 포맷 변환 함수 (API 형식 -> FullCalendar 형식)
143+
const formatDateTime = (dateTimeStr) => {
144+
return dateTimeStr.replace(' ', 'T');
72145
};
73146

147+
// 화면 표시용 시간 포맷
148+
const formatDisplayTime = (date) => {
149+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
150+
};
151+
152+
// 랜덤 색상 생성 함수
153+
const getRandomColor = () => {
154+
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#E91E63', '#795548', '#607D8B'];
155+
return colors[Math.floor(Math.random() * colors.length)];
156+
};
157+
158+
if (loading) {
159+
return <div className="p-4">데이터를 불러오는 중...</div>;
160+
}
161+
74162
return (
75163
<div className="p-4">
76164
<div className="mb-4">
77-
<h2 className="text-xl font-bold">{selectedDate.toLocaleDateString()} 예약 타임라인</h2>
165+
<h2 className="text-xl font-bold">{selectedDate.toLocaleDateString()} 회의실 예약 현황</h2>
78166
<button
79-
className="bg-blue-500 text-white px-3 py-1 rounded mt-2"
167+
className="bg-blue-500 text-white px-3 py-1 rounded mt-2 mr-2"
80168
onClick={() => setSelectedDate(new Date())}
81169
>
82170
오늘로 이동
83171
</button>
172+
<span className="text-sm text-gray-600">
173+
운영 시간: 09:00 - 22:00 (15분 단위로 예약 가능)
174+
</span>
84175
</div>
85176

86177
<FullCalendar
87-
plugins={[timeGridPlugin, interactionPlugin]}
88-
initialView="timeGridDay"
178+
plugins={[resourceTimeGridPlugin, interactionPlugin]}
179+
initialView="resourceTimeGridDay"
89180
headerToolbar={{
90181
left: 'prev,next',
91182
center: 'title',
92183
right: '',
93184
}}
185+
resources={resources}
94186
initialDate={selectedDate}
95187
events={events}
96-
slotMinTime="08:00:00"
188+
slotMinTime="09:00:00"
97189
slotMaxTime="22:00:00"
190+
slotDuration="00:15:00"
191+
slotLabelInterval="01:00:00"
192+
slotLabelFormat={{
193+
hour: '2-digit',
194+
minute: '2-digit',
195+
hour12: false,
196+
}}
98197
allDaySlot={false}
99198
selectable={true}
100199
selectMirror={true}
101200
select={handleTimeSlotSelect}
102201
eventClick={handleEventClick}
103202
datesSet={handleDateSet}
203+
snapDuration="00:15:00"
104204
height="auto"
105205
/>
106206
</div>
107207
);
108208
};
109209

110-
export default ReservationDayTimeline;
210+
export default MeetingRoomSchedule;

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ services:
6767
image: ${DOCKER_REGISTRY}/educheck-client-image:latest
6868
env_file:
6969
- .env
70+
- TZ=Asia/Seoul
7071
environment:
7172
- DOMAIN=${DOMAIN}
7273
volumes:

0 commit comments

Comments
 (0)