Skip to content

Commit 239afb3

Browse files
authored
Merge pull request #123 from geulDa/api/#114/event-api
🔗API & ✨Feat : 이벤트 페이지 api 연결
2 parents 4989c72 + a28d795 commit 239afb3

File tree

19 files changed

+555
-299
lines changed

19 files changed

+555
-299
lines changed

next.config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ const nextConfig: NextConfig = {
1919
images: {
2020
domains: [
2121
'geulda-ai-video-bucket.s3.ap-southeast-2.amazonaws.com',
22-
'example.com'
22+
'example.com',
23+
'www.bucheon.go.kr',
24+
'www.bcf.or.kr',
25+
],
26+
remotePatterns: [
27+
{ protocol: 'https', hostname: 'mblogthumb-phinf.pstatic.net' },
28+
{ protocol: 'https', hostname: 'blogfiles.pstatic.net' },
29+
{ protocol: 'https', hostname: 'postfiles.pstatic.net' },
2330
],
2431
remotePatterns: [
2532
{
@@ -28,7 +35,7 @@ const nextConfig: NextConfig = {
2835
},
2936
],
3037
},
31-
38+
3239
webpack: (config) => {
3340
const svgRule = config.module.rules.find(
3441
// @ts-ignore

src/pages/events/[id].tsx

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,145 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
14
import { Header, EventCard } from '@/shared/components';
25
import DateTag from '@/pages/events/components/DateTag';
36
import { cn } from '@/shared/lib';
4-
import { eventData } from '@/shared/constants/events/eventsData';
57
import Image from 'next/image';
68
import { useRouter } from 'next/router';
9+
import { useEventDetail } from '@/shared/hooks/events/useEventDetail';
10+
import { buildNextEventList } from '@/shared/utils/buildNextEventList';
11+
import type { RelatedEventOrEmpty } from '@/shared/types/eventtypes';
12+
13+
const isEmptyItem = (item: RelatedEventOrEmpty): item is { isEmpty: true } =>
14+
'isEmpty' in item;
715

816
const EventDetailPage = () => {
917
const router = useRouter();
10-
const { id } = router.query;
18+
const { id, date } = router.query;
19+
20+
const eventId = Number(id);
21+
const { data: eventDetail, isLoading, isError } = useEventDetail(eventId);
22+
23+
useEffect(() => {
24+
if (!isLoading && (isError || !eventDetail)) {
25+
router.replace('/events');
26+
}
27+
}, [isLoading, isError, eventDetail, router]);
1128

12-
const event = eventData.find((e) => e.id === Number(id));
13-
if (!event) return null;
29+
if (!eventId) return null;
30+
if (isError || !eventDetail) return null;
31+
if (!router.isReady) return null;
1432

15-
const { name, address, description, startDate, endDate, imageSrc } = event;
33+
const { title, body, address, startDate, endDate, imageUrl, nextEvents } =
34+
eventDetail;
1635

17-
return (
18-
<div
19-
className={cn(
20-
'relative w-full min-h-[100vh] overflow-auto',
21-
)}
22-
>
36+
const nextList = buildNextEventList(nextEvents);
37+
38+
return (
39+
<div className={cn('relative w-full min-h-[100vh] overflow-auto')}>
2340
<Header
2441
title='행사명'
25-
onClick={() => router.back()}
42+
onClick={() => router.push(`/events${date ? `?date=${date}` : ''}`)}
2643
className={cn('fixed top-0 left-0 right-0 z-50')}
2744
/>
2845

2946
<main
3047
className={cn(
3148
'flex flex-col items-center justify-start',
32-
'px-[2.4rem] pt-[calc(10rem+1.4rem)]'
49+
'px-[2.4rem] pt-[calc(10rem+1.4rem)]',
3350
)}
3451
>
3552
{/* 행사 기간 */}
36-
<div aria-label="행사 기간" className={cn('flex justify-center w-[18.4rem] mt-[1.3rem]')}>
53+
<div
54+
aria-label='행사 기간'
55+
className={cn('flex justify-center w-[18.4rem] mt-[1.3rem]')}
56+
>
3757
<DateTag startDate={startDate} endDate={endDate} />
3858
</div>
3959

4060
{/* 대표 이미지 */}
4161
<section
42-
aria-label="행사 대표 이미지"
62+
aria-label='행사 대표 이미지'
4363
className={cn(
44-
'relative w-full flex justify-center max-w-[35.4rem]',
45-
'mt-[1rem]'
64+
'relative w-full flex justify-center max-w-[35.4rem] h-[43rem]',
65+
'mt-[1rem]',
4666
)}
4767
>
48-
{imageSrc ? (
68+
{imageUrl ? (
4969
<Image
50-
src={imageSrc}
51-
alt={`${name} 이미지`}
52-
width={354}
53-
height={430}
54-
className={cn('w-full h-auto object-cover rounded-[2rem]')}
70+
src={imageUrl}
71+
alt={`${title} 이미지`}
72+
fill
73+
className={cn('object-cover rounded-[2rem]')}
5574
/>
5675
) : (
5776
<div
58-
className={cn('w-full h-[43.6rem] bg-gray-200 rounded-[2rem]')}
59-
role="img"
60-
aria-label={`${name} 이미지가 제공되지 않습니다.`}
77+
className={cn('w-full h-full bg-gray-200 rounded-[2rem]')}
78+
role='img'
79+
aria-label={`${title} 이미지가 제공되지 않습니다.`}
6180
/>
6281
)}
6382
</section>
6483

6584
{/* 행사 카드 */}
6685
<div
67-
aria-label="행사 정보"
86+
aria-label='행사 정보'
6887
className={cn(
6988
'flex flex-col items-center w-full gap-[0.8rem]',
70-
'mt-[0.8rem]'
89+
'mt-[0.8rem]',
7190
)}
7291
>
7392
<EventCard
74-
name={name}
75-
address={address}
76-
description={description}
93+
eventId={eventId}
94+
name={title}
95+
address={address ?? ''}
96+
description={body ?? ''}
7797
variant='gray'
7898
size='large'
99+
imageSrc={imageUrl ?? ''}
100+
liked={eventDetail.isBookmarked ?? false}
79101
/>
80-
81102
{/* 관련 행사 */}
82103
<div
83-
aria-label="관련 행사 목록"
104+
aria-label='관련 행사 목록'
84105
className={cn(
85-
'grid grid-cols-2 gap-[1.2rem] justify-items-center w-full max-w-[35.4rem]'
106+
'grid grid-cols-2 gap-[1.2rem] justify-items-center w-full max-w-[35.4rem]',
86107
)}
87108
>
88-
<div className={cn('w-[17rem]')}>
89-
<EventCard
90-
name='관련 행사'
91-
address=''
92-
description=''
93-
variant='gray'
94-
size='small'
95-
/>
96-
</div>
97-
<div className={cn('w-[17rem]')}>
98-
<EventCard
99-
name='관련 행사'
100-
address=''
101-
description=''
102-
variant='gray'
103-
size='small'
104-
/>
105-
</div>
109+
{nextList.map((item: RelatedEventOrEmpty, idx) => (
110+
<div key={idx} className={cn('w-[17rem]')}>
111+
{isEmptyItem(item) ? (
112+
<EventCard
113+
eventId={0}
114+
name='행사 없음'
115+
address=''
116+
description=''
117+
imageSrc=''
118+
variant='gray'
119+
size='small'
120+
liked={false}
121+
onClick={() => null}
122+
/>
123+
) : (
124+
<EventCard
125+
eventId={item.eventId}
126+
name={item.title}
127+
address=''
128+
description=''
129+
imageSrc={item.imageUrl}
130+
variant='gray'
131+
size='small'
132+
liked={false}
133+
onClick={() => router.push(`/events/${item.eventId}`)}
134+
/>
135+
)}
136+
</div>
137+
))}
106138
</div>
107139
</div>
108140
</main>
109141
</div>
110142
);
111143
};
112144

113-
export default EventDetailPage;
145+
export default EventDetailPage;

src/pages/events/index.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
44
import { useRouter } from 'next/router';
55
import { Icon } from '@/shared/icons';
66
import { cn } from '@/shared/lib';
@@ -10,22 +10,24 @@ import {
1010
BottomNav,
1111
EventCard,
1212
} from '@/shared/components';
13-
import { eventData } from '@/shared/constants/events/eventsData';
14-
import { formatDateToISO, isDateWithinRange } from '@/shared/utils/date';
13+
import { useEvents } from '@/shared/hooks/events/useEvents';
14+
import type { EventData } from '@/shared/types/eventtypes';
15+
import { formatDateToISO } from '@/shared/utils/date';
1516

1617
export default function EventPage() {
1718
const router = useRouter();
18-
const [date, setDate] = useState<Date>();
19+
const { date: dateQuery } = router.query;
20+
const [date, setDate] = useState<Date | undefined>(undefined);
21+
const { events } = useEvents(date);
1922

20-
const selectedDate = formatDateToISO(date);
23+
const filteredEvents = events;
24+
useEffect(() => {
25+
if (router.isReady && dateQuery) {
26+
setDate(new Date(String(dateQuery)));
27+
}
28+
}, [router.isReady, dateQuery]);
2129

22-
const filteredEvents = eventData.filter((event) =>
23-
isDateWithinRange(selectedDate, event.startDate, event.endDate),
24-
);
25-
26-
const handleCardClick = (id: number) => {
27-
router.push(`/events/${id}`);
28-
};
30+
const selectedDateString = date ? formatDateToISO(date) : '';
2931

3032
return (
3133
<div
@@ -34,47 +36,55 @@ export default function EventPage() {
3436
)}
3537
>
3638
{/* 헤더 */}
37-
<ControlBar className="fixed top-[1rem] left-0 right-0 z-50 px-[2rem]" />
39+
<ControlBar className='fixed top-[1rem] left-0 right-0 z-50 px-[2rem]' />
3840

3941
{/* 본문 콘텐츠 */}
4042
<main className='w-full pt-[6.3rem] flex flex-col items-center'>
4143
{/* 날짜 선택 */}
4244
<div className='w-full mt-[3.7rem] flex justify-start'>
4345
{/* 스크린리더가 “날짜 선택”으로 읽히도록 추가 */}
44-
<label htmlFor="event-date" className="sr-only">
46+
<label htmlFor='event-date' className='sr-only'>
4547
행사 날짜 선택
4648
</label>
47-
<DatePicker ariaLabel="행사 날짜 선택" value={date} onChange={setDate} />
49+
<DatePicker
50+
ariaLabel='행사 날짜 선택'
51+
value={date}
52+
onChange={setDate}
53+
/>
4854
</div>
4955

5056
{/* 행사카드 & 빈화면 */}
5157
{filteredEvents.length > 0 ? (
5258
<section
53-
aria-label="이벤트 목록"
59+
aria-label='이벤트 목록'
5460
className={cn(
5561
'grid w-full mt-[1.4rem]',
5662
'grid-cols-2 gap-x-[1.4rem] gap-y-[1.4rem]',
5763
)}
5864
>
59-
{filteredEvents.map((event) => (
65+
{filteredEvents.map((event: EventData) => (
6066
<div
6167
key={event.id}
62-
onClick={() => handleCardClick(event.id)}
68+
onClick={() =>
69+
router.push(`/events/${event.id}?date=${selectedDateString}`)
70+
}
6371
className='cursor-pointer'
6472
>
6573
<EventCard
74+
eventId={event.id}
6675
name={event.name}
6776
address={event.address}
6877
description={event.description}
6978
variant='gray'
7079
size='medium'
71-
imageSrc={event.imageSrc ?? ''}
80+
imageSrc={event.imageSrc}
81+
liked={event.liked}
7282
/>
7383
</div>
7484
))}
7585
</section>
7686
) : (
77-
<div
87+
<div
7888
className='flex flex-col items-center justify-center text-center mt-[15rem]'
7989
role='status'
8090
aria-live='polite'

0 commit comments

Comments
 (0)