- {/* 별점 정보 */}
-
⭐ 4.9 (293)
- {/* 체험명 (줄바꿈 포함, 반응형 크기) */}
-
함께 배우면 즐거운
스트릿 댄스
- {/* 가격 정보 */}
-
₩ 38,000 / 인
+
+ {/* 썸네일 */}
+
+
+
+
+ {/* 텍스트 정보 */}
+
+
+ ⭐ {rating} ({reviews})
+
+
+ {title}
+
+
+ ₩ {price.toLocaleString()} / 인
+
);
diff --git a/src/app/(with-header)/components/ExperienceList.tsx b/src/app/(with-header)/components/ExperienceList.tsx
new file mode 100644
index 0000000..4eb7677
--- /dev/null
+++ b/src/app/(with-header)/components/ExperienceList.tsx
@@ -0,0 +1,137 @@
+'use client';
+
+import Dropdown from '@components/Dropdown';
+import Pagination from '@components/Pagination';
+import { useEffect, useState } from 'react';
+import Link from 'next/link';
+
+import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
+import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
+import { getExperiences } from '@/app/api/experiences/getExperiences';
+import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
+import {
+ SORT_OPTIONS,
+ SORT_VALUE_MAP,
+ SORT_LABEL_MAP,
+} from '@/constants/SortPrices';
+import { Experience } from '@/types/experienceListTypes';
+
+interface ExperienceListProps {
+ keyword?: string;
+ isSearchMode?: boolean;
+}
+
+export default function ExperienceList({ keyword, isSearchMode }: ExperienceListProps) {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedCategory, setSelectedCategory] = useState
(ACTIVITY_CATEGORIES[0]);
+ const [sortOption, setSortOption] = useState('');
+ const [experiences, setExperiences] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+
+ useEffect(() => {
+ const resync = async () => {
+ const res = await getExperiences({
+ page: currentPage,
+ sort: sortOption,
+ category: selectedCategory,
+ keyword,
+ });
+
+ setExperiences(res.experiences);
+ setTotalCount(res.totalCount);
+ };
+
+ resync();
+ }, [currentPage, sortOption, selectedCategory, keyword]);
+
+ useEffect(() => {
+ if (keyword) {
+ setSelectedCategory(ACTIVITY_CATEGORIES[0]);
+ setSortOption('');
+ setCurrentPage(1);
+ }
+ }, [keyword]);
+
+ const totalPage = Math.ceil(totalCount / 8);
+
+ return (
+
+
+ {/* 🔍 검색 모드일 때 문구 표시 */}
+ {isSearchMode && keyword && (
+ <>
+
+ "{keyword}"
+ (으)로 검색한 결과입니다.
+
+
+
+ 총 {totalCount}개의 결과
+
+
+ {experiences.length === 0 && (
+ 검색 결과가 없습니다.
+ )}
+ >
+ )}
+
+ {/* 🧭 필터/정렬 UI (검색 모드 아닐 때만) */}
+ {!isSearchMode && (
+
+ {
+ setSelectedCategory(category);
+ setCurrentPage(1);
+ }}
+ />
+ {
+ const value = SORT_VALUE_MAP[label];
+ setSortOption(value);
+ setCurrentPage(1);
+ }}
+ />
+
+ )}
+
+
+ {/* 🚂 모든 체험 제목 (검색 아닐 때만) */}
+ {!isSearchMode && (
+
🛼 모든 체험
+ )}
+
+ {/* 체험 카드 목록 */}
+
+ {experiences.map((exp) => (
+
+
+
+ ))}
+
+
+
+ {/* 페이지네이션: 결과 있을 때만 표시 */}
+ {experiences.length > 0 && (
+
+ )}
+
+ );
+}
diff --git a/src/app/(with-header)/components/PopularCard.tsx b/src/app/(with-header)/components/PopularCard.tsx
new file mode 100644
index 0000000..c2791cc
--- /dev/null
+++ b/src/app/(with-header)/components/PopularCard.tsx
@@ -0,0 +1,40 @@
+import Image from 'next/image';
+
+interface PopularCardProps {
+ imageUrl: string;
+ title: string;
+ rating: number;
+ reviews: number;
+ price: number;
+}
+
+export default function PopularCard({
+ imageUrl,
+ title,
+ rating,
+ reviews,
+ price,
+}: PopularCardProps) {
+ return (
+
+ {/* 배경 이미지 */}
+
+ {/* 어두운 오버레이 */}
+
+ {/* 텍스트 정보 블록 (카드 하단 위치 고정) */}
+
+ {/* 별점 정보 */}
+
⭐ {rating} ({reviews})
+ {/* 체험명 (줄바꿈 포함, 반응형 크기) */}
+
{title}
+ {/* 가격 정보 */}
+
₩ {price.toLocaleString()} / 인
+
+
+ );
+}
diff --git a/src/app/(with-header)/components/PopularExperiences.tsx b/src/app/(with-header)/components/PopularExperiences.tsx
index 1441b96..de61a2f 100644
--- a/src/app/(with-header)/components/PopularExperiences.tsx
+++ b/src/app/(with-header)/components/PopularExperiences.tsx
@@ -1,41 +1,59 @@
'use client';
-import { useRef } from 'react';
-import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
-import IconArrowRight from '@assets/svg/right-arrow';
import IconArrowLeft from '@assets/svg/left-arrow';
+import IconArrowRight from '@assets/svg/right-arrow';
+import { useEffect, useRef, useState } from 'react';
+import Link from 'next/link';
+
+import PopularCard from '@/app/(with-header)/components/PopularCard';
+import { Experience } from '@/types/experienceListTypes';
+
+import { getPopularExperiences } from '../../api/experiences/getPopularExperiences';
export default function PopularExperiences() {
- // 카드 슬라이더를 참조할 DOM ref
const sliderRef = useRef(null);
+ const [popularExperiences, setPopularExperiences] = useState([]);
+
// 좌우 버튼 클릭 시 한 장씩 슬라이드 이동
const scrollByCard = (direction: 'left' | 'right') => {
if (!sliderRef.current) return;
- // 첫 번째 카드 요소를 찾아서 너비 측정
const card = sliderRef.current.querySelector('.card');
if (!(card instanceof HTMLElement)) return;
- const cardWidth = card.offsetWidth; // 카드 너비
- const gap = parseInt(getComputedStyle(sliderRef.current).gap) || 0; // gap 값
- const distance = cardWidth + gap; // 한 번에 이동할 거리
+ const cardWidth = card.offsetWidth;
+ const gap = parseInt(getComputedStyle(sliderRef.current).gap) || 0;
+ const distance = cardWidth + gap;
- // 슬라이더 스크롤 이동 (좌/우 방향에 따라)
sliderRef.current.scrollBy({
left: direction === 'left' ? -distance : distance,
behavior: 'smooth',
});
};
+ // 인기 체험 목록 불러오기
+ useEffect(() => {
+ const fetchPopular = async () => {
+ try {
+ const res = await getPopularExperiences();
+ setPopularExperiences(res.activities);
+ } catch (error) {
+ console.error('인기 체험을 불러오는 데 실패했습니다:', error);
+ }
+ };
+
+ fetchPopular();
+ }, []);
+
return (
{/* 섹션 제목 + 좌우 화살표 버튼 */}
🔥 인기 체험
- scrollByCard('left')} className='text-2xl px-3' />
- scrollByCard('right')} className='text-2xl px-3' />
+ scrollByCard('left')} />
+ scrollByCard('right')} />
@@ -44,10 +62,21 @@ export default function PopularExperiences() {
ref={sliderRef}
className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar'
>
- {[...Array(4)].map((_, idx) => (
- // 카드 wrapper: flex-shrink-0으로 크기 고정 + 'card' 클래스로 식별
-
-
+ {popularExperiences.map((exp) => (
+
))}
diff --git a/src/app/(with-header)/components/SearchBar.tsx b/src/app/(with-header)/components/SearchBar.tsx
index e5f6662..868e435 100644
--- a/src/app/(with-header)/components/SearchBar.tsx
+++ b/src/app/(with-header)/components/SearchBar.tsx
@@ -1,15 +1,20 @@
'use client';
-import { useState, FormEvent } from 'react';
-import Input from '@components/Input';
import Button from '@components/Button';
+import Input from '@components/Input';
+import { FormEvent,useState } from 'react';
-export default function SearchBar() {
+interface SearchBarProps {
+ onSearch: (keyword: string) => void;
+}
+
+export default function SearchBar({ onSearch }: SearchBarProps) {
const [searchValue, setSearchValue] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
- console.log('검색어:', searchValue); // 검색 로직은 추후 API 연동
+ onSearch(searchValue); // 부모(HomePage)로 검색어 전달
+ setSearchValue(''); // 선택 사항: 검색어 초기화
};
return (
@@ -22,21 +27,21 @@ export default function SearchBar() {