Skip to content

Commit c11c627

Browse files
author
jyn
committed
Merge branch 'dev' of https://github.com/codeit-team6/nomadia into dev
2 parents 3ec3010 + 1e6be4f commit c11c627

File tree

10 files changed

+4047
-1547
lines changed

10 files changed

+4047
-1547
lines changed

pnpm-lock.yaml

Lines changed: 3407 additions & 1208 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/images/icons/no-alarm.svg

Lines changed: 3 additions & 5 deletions
Loading
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
'use client';
2+
import { ChevronDown } from 'lucide-react';
3+
import Image from 'next/image';
4+
import React, { useEffect, useMemo, useState } from 'react';
5+
6+
import { getActListApi } from '@/features/activities/libs/api/getActListApi';
7+
import { getReservationsByMonthApi } from '@/features/activities/libs/api/getReserveMonthApi';
8+
import { useAuthStore } from '@/features/auth/stores/useAuthStore';
9+
import CalendarWithReservations from '@/shared/components/calendar/components/calendar-with-reservations';
10+
import { MonthReservations } from '@/shared/components/calendar/libs/types/data';
11+
import Dropdown from '@/shared/components/dropdown';
12+
import AdaptiveModal from '@/shared/components/modal/components/adaptive-modal/adaptive-modal';
13+
import ContentReservation from '@/shared/components/modal/components/adaptive-modal/content-reservation';
14+
import EmptyReservation from '@/shared/components/modal/components/adaptive-modal/empty-reservation';
15+
import { useCalendarStore } from '@/shared/libs/stores/useCalendarStore';
16+
import { useModalStore } from '@/shared/libs/stores/useModalStore';
17+
import { Activity } from '@/shared/types/activity';
18+
19+
const ReserveCalendarPage = () => {
20+
const [reservationArray, setReservationArray] = useState<MonthReservations[]>(
21+
[],
22+
);
23+
const { user } = useAuthStore();
24+
const userId = user?.id;
25+
const [activities, setActivities] = useState<Activity[]>([]);
26+
const [selectedActivityTitle, setSelectedActivityTitle] =
27+
useState<string>('내가 등록한 체험 선택');
28+
const [selectedActivityId, setSelectedActivityId] = useState<string | null>(
29+
null,
30+
);
31+
const [shouldFetch, setShouldFetch] = useState(false);
32+
const [selectedDate, setSelectedDate] = useState<string | null>(null);
33+
const { setModalType, appearModal, disappearModal, isDesktop } =
34+
useModalStore();
35+
const { month, setYear, setMonth } = useCalendarStore();
36+
37+
useEffect(() => {
38+
if (selectedActivityId) {
39+
setModalType('custom');
40+
if (isDesktop) {
41+
appearModal();
42+
}
43+
} else {
44+
disappearModal();
45+
}
46+
}, [
47+
selectedActivityId,
48+
isDesktop,
49+
setModalType,
50+
appearModal,
51+
disappearModal,
52+
]);
53+
54+
const handleDropdownOpen = () => {
55+
setShouldFetch(true);
56+
};
57+
58+
const handleDateClick = (dateStr: string) => {
59+
setSelectedDate(dateStr);
60+
setModalType('custom');
61+
appearModal();
62+
};
63+
64+
useEffect(() => {
65+
if (!shouldFetch || !userId) return;
66+
67+
(async () => {
68+
try {
69+
const data = await getActListApi();
70+
const myActivities = (data.activities || []).filter(
71+
(activity) => activity.userId === Number(userId),
72+
);
73+
setActivities(myActivities);
74+
} catch (error) {
75+
console.error('내 체험 리스트 조회 실패', error);
76+
}
77+
setShouldFetch(false);
78+
})();
79+
}, [shouldFetch, userId]);
80+
81+
const handleSelectActivity = async (id: string | number, title: string) => {
82+
setSelectedActivityTitle(title);
83+
const activityId = String(id);
84+
setSelectedActivityId(activityId);
85+
setSelectedDate(null);
86+
87+
try {
88+
const currentYear = new Date().getFullYear();
89+
const monthsToFetch = 12;
90+
let earliestDate: Date | null = null;
91+
let allReservations: MonthReservations[] = [];
92+
93+
for (let i = 0; i < monthsToFetch; i++) {
94+
const fetchYear = currentYear + Math.floor((month + i) / 12);
95+
const fetchMonth = (month + i) % 12;
96+
97+
const reservations = await getReservationsByMonthApi({
98+
activityId,
99+
year: fetchYear,
100+
month: fetchMonth + 1,
101+
});
102+
103+
allReservations = [...allReservations, ...reservations];
104+
105+
for (const r of reservations) {
106+
const resDate = new Date(r.date);
107+
if (!earliestDate || resDate < earliestDate) {
108+
earliestDate = resDate;
109+
}
110+
}
111+
112+
if (earliestDate) break;
113+
}
114+
115+
setReservationArray(allReservations);
116+
117+
if (earliestDate) {
118+
setYear(earliestDate.getFullYear());
119+
setMonth(earliestDate.getMonth());
120+
}
121+
} catch (error) {
122+
console.error('예약 데이터 조회 실패', error);
123+
setReservationArray([]);
124+
}
125+
};
126+
127+
const selectedReservationsOfDate = useMemo(() => {
128+
if (!selectedDate) return [];
129+
return reservationArray.filter((res) => res.date === selectedDate);
130+
}, [selectedDate, reservationArray]);
131+
132+
return (
133+
<div className="flex flex-col items-start">
134+
<div className="mb-[2.4rem] w-full">
135+
<h2 className="txt-18-bold text-gray-950">예약 현황</h2>
136+
<p className="txt-14-medium mt-1 text-gray-500">
137+
내 체험의 내역들을 한 눈에 확인할 수 있습니다.
138+
</p>
139+
140+
<div className="mt-[1.8rem] md:w-[47.6rem] lg:w-[64rem]">
141+
<Dropdown
142+
trigger={
143+
<div
144+
role="button"
145+
tabIndex={0}
146+
onClick={handleDropdownOpen}
147+
onKeyDown={(e) => {
148+
if (e.key === 'Enter' || e.key === ' ') {
149+
handleDropdownOpen();
150+
}
151+
}}
152+
className="shadow-experience-card flex h-[5.4rem] w-full cursor-pointer items-center justify-end rounded-[1rem] border border-gray-100 bg-white pr-[2.6rem] text-[1.4rem] text-gray-950 md:text-[1.6rem]"
153+
>
154+
<span className="mr-auto pl-[2.6rem]">
155+
{selectedActivityTitle}
156+
</span>
157+
<ChevronDown className="size-[2rem]" />
158+
</div>
159+
}
160+
dropdownClassName="w-full bg-white shadow-experience-card rounded-[1rem] border border-gray-100 px-[2.6rem]"
161+
>
162+
{(close) => (
163+
<ul className="py-3">
164+
{activities.length === 0 && <p>등록한 체험이 없습니다.</p>}
165+
{activities.map((act) => (
166+
<button
167+
key={act.id}
168+
className="block w-full cursor-pointer py-2 text-left hover:bg-gray-100"
169+
onClick={() => {
170+
handleSelectActivity(act.id, act.title);
171+
close();
172+
}}
173+
>
174+
{act.title}
175+
</button>
176+
))}
177+
</ul>
178+
)}
179+
</Dropdown>
180+
</div>
181+
</div>
182+
183+
<div className="flex gap-8">
184+
<div className="md:w-[47.6rem] lg:w-[64rem]">
185+
{selectedActivityId ? (
186+
<CalendarWithReservations
187+
reservationArray={reservationArray}
188+
calendarWidth="w-full md:rounded-[2rem] border border-gray-50 shadow-experience-card"
189+
dayOfWeekStyle="w-[5.35rem] md:w-[6.8rem] lg:w-[9.143rem]"
190+
onCellClick={handleDateClick}
191+
/>
192+
) : (
193+
<div className="mt-24 flex flex-col items-center justify-center">
194+
<Image
195+
src="/images/sad-laptop.svg"
196+
alt="Sad laptop"
197+
width={246}
198+
height={200}
199+
/>
200+
<p className="mt-14 text-[1.8rem] font-medium text-gray-600">
201+
등록한 체험을 선택해주세요.
202+
</p>
203+
</div>
204+
)}
205+
</div>
206+
207+
{selectedActivityId && (
208+
<AdaptiveModal extraClassName="w-[37.5rem] h-[50.8rem] md:w-[74.4rem] md:h-[39.7rem] lg:w-[34rem] lg:h-[51.9rem] border border-gray-50 shadow-experience-card">
209+
{selectedDate ? (
210+
<div className="p-4">
211+
{selectedReservationsOfDate &&
212+
selectedReservationsOfDate.length > 0 ? (
213+
<ContentReservation
214+
// scheduleId={selectedActivityId}
215+
// dateString={selectedDate}
216+
// reservations={selectedReservationsOfDate}
217+
/>
218+
) : (
219+
<EmptyReservation />
220+
)}
221+
</div>
222+
) : (
223+
<div className="txt-12-medium p-4 text-center text-gray-950">
224+
날짜를 선택하면 예약 목록이 표시됩니다.
225+
</div>
226+
)}
227+
</AdaptiveModal>
228+
)}
229+
</div>
230+
</div>
231+
);
232+
};
233+
234+
export default ReserveCalendarPage;
Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
11
'use client';
22

33
import Image from 'next/image';
4-
import { useRef } from 'react';
4+
import { useEffect, useRef } from 'react';
55

66
import { useNotifications } from '@/features/activities/libs/hooks/useNotifications';
7-
// import NotificationModal from '@/shared/components/modal/components/notification-modal';
8-
// import { useModalStore } from '@/shared/libs/stores/useModalStore';
7+
import NotificationModal from '@/shared/components/modal/components/notification-modal';
8+
import { useNotificationStore } from '@/shared/libs/stores/useNotificationStore';
99

1010
const NotificationButton = () => {
1111
const ref = useRef<HTMLDivElement>(null);
12-
13-
// const { isNotificationOpen, openNotification, closeNotification } =
14-
// useModalStore();
12+
const { isNotificationOpen, openNotification, closeNotification } =
13+
useNotificationStore();
1514

1615
const { data } = useNotifications();
1716
const hasNotifications =
1817
data?.pages?.some((page) => page.notifications.length > 0) ?? false;
1918

20-
// useEffect(() => {
21-
// const handler = (e: MouseEvent) => {
22-
// if (ref.current && !ref.current.contains(e.target as Node)) {
23-
// closeNotification();
24-
// }
25-
// };
26-
// document.addEventListener('mousedown', handler);
27-
// return () => document.removeEventListener('mousedown', handler);
28-
// }, [closeNotification]);
19+
// 바깥 클릭 시 모달 닫기
20+
useEffect(() => {
21+
const handler = (e: MouseEvent) => {
22+
if (ref.current && !ref.current.contains(e.target as Node)) {
23+
closeNotification();
24+
}
25+
};
26+
document.addEventListener('mousedown', handler);
27+
return () => document.removeEventListener('mousedown', handler);
28+
}, [closeNotification]);
2929

3030
return (
3131
<div className="relative" ref={ref}>
32-
<button
33-
// onClick={() =>
34-
// isNotificationOpen ? closeNotification() : openNotification()
35-
// }
36-
className="cursor-pointer rounded p-2"
37-
>
32+
<button className="cursor-pointer rounded p-2" onClick={openNotification}>
3833
<Image
3934
src={
4035
hasNotifications
@@ -46,11 +41,9 @@ const NotificationButton = () => {
4641
height={24}
4742
/>
4843
</button>
49-
{/* {isNotificationOpen && <NotificationModal />} */}
44+
{isNotificationOpen && <NotificationModal />}
5045
</div>
5146
);
5247
};
5348

5449
export default NotificationButton;
55-
56-
//주석처리 필요(?)

src/shared/components/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const Header = () => {
9797
handleLogout();
9898
close();
9999
}}
100-
className="hover:bg-sub block h-[5.5rem] w-full rounded-t-lg"
100+
className="hover:bg-sub block h-[5.5rem] w-full cursor-pointer rounded-t-lg"
101101
>
102102
로그아웃
103103
</button>

0 commit comments

Comments
 (0)