Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/apis/contents.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import type {
CultureContentDetail,
GetCultureContentsParams,
GetCultureContentsResponse,
OngoingContent,
} from '../types/content.type';

export async function getOngoingContents(): Promise<OngoingContent[]> {
try {
const { data } = await apiClient.get<OngoingContent[]>('/contents/ongoing');

return data;
} catch (error) {
throw normalizeApiError(error);
}
}

export async function getCultureContents(
params: GetCultureContentsParams = {},
signal?: AbortSignal,
Expand Down
10 changes: 10 additions & 0 deletions src/hooks/useOngoingContents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';

import { getOngoingContents } from '../apis/contents.api';

export function useOngoingContents() {
return useQuery({
queryKey: ['contents', 'ongoing'],
queryFn: getOngoingContents,
});
}
2 changes: 1 addition & 1 deletion src/pages/detail/festival/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function FestivalDetailContent({ contentId }: { contentId: number }) {
<section style={{ marginTop: INFO_CARD_MARGIN_TOP * scale }}>
<DetailInfoCard
address={festivalDetail.address}
hours={festivalPlaceHours ?? '영업시간 정보 없음'}
hours={festivalDetail.period}
phone={festivalDetail.phone}
website={festivalDetail.homepageLabel}
phoneHref={toTelHref(festivalDetail.phone)}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/detail/mappers/cultureContentDetailMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function mapCultureContentDetailToFestivalDetail(
tags: mapTags(content.hashtags),
overview: content.description,
address: content.place.roadAddress,
period: `${content.startDate} ~ ${content.endDate}`,
period: `${content.startDate.replace(/-/g, '.')} ~ ${content.endDate.replace(/-/g, '.')}`,
phone: content.phone,
homepageUrl: content.officialUrl,
homepageLabel: '공식 홈페이지',
Expand Down
21 changes: 10 additions & 11 deletions src/pages/festival/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../../components/common';
import { useGlobalScale } from '../../hooks/useGlobalScale';
import { useCultureContentBanners } from '../../hooks/useCultureContentBanners';
import { useCultureContents } from '../../hooks/useCultureContents';
import { useOngoingContents } from '../../hooks/useOngoingContents';
import { useContentLikeToggle } from '../../hooks/useContentLikeToggle';
import { useRecentCultureContents } from '../../hooks/useRecentCultureContents';
import { toContentTagIds } from '../../utils/contentTags';
Expand All @@ -17,6 +17,8 @@ import { buildFestivalDetailPath } from '../../utils/routes';
import { FeaturedFestivalBanner } from './components';
import useFestivalPreviews from './hooks/useFestivalPreviews';

const ONGOING_PREVIEW_ITEM_COUNT = 2;

const PAGE_PADDING_X = 24;
const PAGE_PADDING_TOP = 12;
const PAGE_PADDING_BOTTOM = 40;
Expand All @@ -38,15 +40,12 @@ function FestivalPage() {
const { featuredFestival } = useFestivalPreviews();
const recentFestivals = useRecentCultureContents().slice(0, 2);
const { data: cultureContentBanners } = useCultureContentBanners();
const {
data: cultureContents,
isPending: isCultureContentsPending,
} = useCultureContents({
category: 'FESTIVAL',
sort: 'RECOMMEND',
size: 2,
});
const ongoingFestivals = cultureContents?.pages[0]?.items ?? [];
const { data: ongoingContents, isPending: isOngoingContentsPending } =
useOngoingContents();
const ongoingFestivals = (ongoingContents ?? []).slice(
0,
ONGOING_PREVIEW_ITEM_COUNT
);
const banner = cultureContentBanners?.[0];
const displayedBanner = banner
? {
Expand Down Expand Up @@ -135,7 +134,7 @@ function FestivalPage() {
gap: LIST_GAP * scale,
}}
>
{isCultureContentsPending ? (
{isOngoingContentsPending ? (
<>
<ContentCardSkeleton />
<ContentCardSkeleton />
Expand Down
76 changes: 22 additions & 54 deletions src/pages/festival/ongoing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useCallback } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';

import {
ContentCard,
ContentCardSkeleton,
} from '../../../components/common';
import { useCultureContents } from '../../../hooks/useCultureContents';
import { useOngoingContents } from '../../../hooks/useOngoingContents';
import { useContentLikeToggle } from '../../../hooks/useContentLikeToggle';
import { useGlobalScale } from '../../../hooks/useGlobalScale';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { toContentTagIds } from '../../../utils/contentTags';
import { buildFestivalDetailPath } from '../../../utils/routes';

Expand All @@ -29,14 +28,6 @@ const LIST_GAP = 16;
const EMPTY_MARGIN_TOP = 40;
const ERROR_MARGIN_TOP = 24;
const MESSAGE_TEXT_SIZE = 13;
const LOAD_MORE_HEIGHT = 40;

const contentSortByFestivalSort = {
RECOMMENDED: 'RECOMMEND',
SAVED: 'LIKE',
DISTANCE: 'DISTANCE',
ENDING_SOON: 'DEADLINE',
} as const;

function FestivalOngoingPage() {
const navigate = useNavigate();
Expand All @@ -47,35 +38,28 @@ function FestivalOngoingPage() {
handleSortSelect,
handleCategorySelect,
} = useFestivalFilters();
const {
data,
fetchNextPage,
hasNextPage,
isError,
isFetchingNextPage,
isPending,
} = useCultureContents({
category:
selectedFilters.category === 'ALL'
? undefined
: selectedFilters.category,
sort: contentSortByFestivalSort[selectedFilters.sort],
size: 20,
});

const festivals = data?.pages.flatMap((page) => page.items) ?? [];
const hasEmptyResult = !isPending && !isError && festivals.length === 0;

const handleIntersect = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
const { data: ongoingContents, isPending, isError } = useOngoingContents();

// /contents/ongoing은 쿼리 파라미터를 아예 받지 않는 엔드포인트로 확인됨
// (category/sort/cursor 등 어떤 값을 보내도, 심지어 존재하지 않는 값을
// 보내도 항상 동일한 결과가 옴 - curl로 재확인). 응답 필드에도 category가
// 없어 카테고리 필터(체험/전시/공연/축제)를 적용할 데이터가 없다. 정렬도
// 종료 임박순(endDate)만 응답 필드로 계산 가능하고, 추천순/저장순/거리순은
// 추천 점수·좋아요 수·좌표가 응답에 없어 그대로 원본 순서를 유지한다.
// 필터 바 UI/상태는 그대로 두되, 실제 필터링·정렬은 API가 관련 필드/
// 파라미터를 지원해야 완전히 동작한다 - 백엔드에 카테고리 필드 및
// 정렬/페이지네이션 파라미터 추가를 요청해야 한다.
const festivals = useMemo(() => {
const items = ongoingContents ?? [];

if (selectedFilters.sort !== 'ENDING_SOON') {
return items;
}
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);

const loadMoreRef = useInfiniteScroll({
enabled: Boolean(hasNextPage) && !isPending,
onIntersect: handleIntersect,
});
return [...items].sort((a, b) => a.endDate.localeCompare(b.endDate));
}, [ongoingContents, selectedFilters.sort]);

const hasEmptyResult = !isPending && !isError && festivals.length === 0;

return (
<section
Expand Down Expand Up @@ -153,16 +137,6 @@ function FestivalOngoingPage() {
}
/>
))}

{isFetchingNextPage
? FESTIVAL_SKELETON_ITEMS.slice(0, 4).map((item) => (
<ContentCardSkeleton
key={`next-page-${item}`}
className="w-full"
imageClassName="aspect-[163/115] h-auto"
/>
))
: null}
</div>

{hasEmptyResult ? (
Expand All @@ -188,12 +162,6 @@ function FestivalOngoingPage() {
행사 목록을 불러오지 못했어요.
</p>
) : null}

<div
ref={loadMoreRef}
style={{ height: LOAD_MORE_HEIGHT * scale }}
aria-hidden="true"
/>
</section>
);
}
Expand Down
15 changes: 5 additions & 10 deletions src/pages/home/components/FestivalSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
ContentCardSkeleton,
SectionHeader,
} from '../../../components/common';
import { useCultureContents } from '../../../hooks/useCultureContents';
import { useOngoingContents } from '../../../hooks/useOngoingContents';
import { useContentLikeToggle } from '../../../hooks/useContentLikeToggle';
import { useGlobalScale } from '../../../hooks/useGlobalScale';
import { buildFestivalDetailPath } from '../../../utils/routes';
import { toContentTagIds } from '../../../utils/contentTags';

const PREVIEW_ITEM_COUNT = 2;

const SECTION_MARGIN_TOP = 32;
const SECTION_PADDING_X = 24;
const LIST_MARGIN_TOP = 16;
Expand All @@ -20,15 +22,8 @@ function FestivalSection() {
const navigate = useNavigate();
const scale = useGlobalScale();
const { getLiked, toggleLike } = useContentLikeToggle();
const {
data: cultureContents,
isPending: isLoading,
} = useCultureContents({
category: 'FESTIVAL',
sort: 'RECOMMEND',
size: 2,
});
const festivals = cultureContents?.pages[0]?.items ?? [];
const { data: ongoingContents, isPending: isLoading } = useOngoingContents();
const festivals = (ongoingContents ?? []).slice(0, PREVIEW_ITEM_COUNT);

return (
<section style={{ marginTop: SECTION_MARGIN_TOP * scale }}>
Expand Down
10 changes: 10 additions & 0 deletions src/types/content.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ export interface CultureContentDetail {
courses: CultureContentCourse[];
}

export interface OngoingContent {
contentId: number;
title: string;
thumbnailImageUrl: string | null;
startDate: string;
endDate: string;
regionName: string;
hashtags: string[];
}

export interface RecentCultureContent {
contentId: number;
title: string;
Expand Down