Skip to content

Commit d5609e8

Browse files
authored
Merge pull request #118 from codeit-2team/fix/113
Fix/113 헤더, 푸터, 메인 페이지 이슈 해결
2 parents 214c0a8 + f641aac commit d5609e8

File tree

15 files changed

+212
-105
lines changed

15 files changed

+212
-105
lines changed

public/assets/img/main-banner.jpg

776 KB
Loading

public/assets/svg/github.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
3+
const IconGithub = ({ size = 24, color = '#fff', ...props }) => (
4+
<svg
5+
width={size}
6+
height={size}
7+
viewBox="0 0 98 96"
8+
fill={color}
9+
xmlns="http://www.w3.org/2000/svg"
10+
{...props}
11+
>
12+
<path
13+
fillRule="evenodd"
14+
clipRule="evenodd"
15+
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a47 47 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0"
16+
/>
17+
</svg>
18+
);
19+
20+
export default IconGithub;

src/app/(with-header)/components/BannerSection.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import Image from 'next/image';
33
import SearchBar from '@/app/(with-header)/components/SearchBar';
44

55
interface BannerSectionProps {
6-
onSearch: (keyword: string) => void;
6+
keyword: string;
77
}
88

9-
export default function BannerSection({ onSearch }: BannerSectionProps) {
9+
export default function BannerSection({ keyword }: BannerSectionProps) {
1010
return (
1111
<section className='relative w-full h-240 md:h-550 mb-93'>
1212
{/* 배경 이미지 */}
@@ -15,7 +15,7 @@ export default function BannerSection({ onSearch }: BannerSectionProps) {
1515
priority
1616
alt='스트릿 댄스'
1717
className='object-cover'
18-
src='/test/image1.png'
18+
src='/assets/img/main-banner.jpg'
1919
/>
2020

2121
{/* 어두운 오버레이 */}
@@ -24,15 +24,15 @@ export default function BannerSection({ onSearch }: BannerSectionProps) {
2424
{/* 텍스트 콘텐츠 */}
2525
<div className='relative z-10 flex flex-col items-start w-220 max-w-1200 md:w-440 lg:w-full pl-24 pt-74 md:pl-32 lg:pl-0 md:pt-144 lg:pt-159 lg:ml-auto lg:mr-auto gap-8 lg:gap-20 h-full text-white font-bold break-keep'>
2626
<h2 className='text-2xl md:text-[54px] md:leading-[64px] lg:text-[68px] lg:leading-[78px]'>
27-
함께 배우면 즐거운<br />
28-
스트릿 댄스
27+
오로라와 함께하는<br />
28+
여름의 북극 감성 체험
2929
</h2>
3030
<p className='text-md md:text-xl lg:text-2xl'>
31-
1월의 인기 경험 BEST 🔥
31+
자연 속에서 즐기는 이색 액티비티 추천 ❤️
3232
</p>
3333
</div>
3434
<div className='absolute -bottom-100 left-0 right-0'>
35-
<SearchBar onSearch={onSearch} />
35+
<SearchBar keyword={keyword} />
3636
</div>
3737
</section>
3838
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client';
2+
3+
import { useSearchParams } from 'next/navigation';
4+
import BannerSection from '@/app/(with-header)/components/BannerSection';
5+
import PopularExperiences from '@/app/(with-header)/components/PopularExperiences';
6+
import ExperienceList from '@/app/(with-header)/components/ExperienceList';
7+
8+
export default function BasePage() {
9+
const searchParams = useSearchParams();
10+
const keyword = searchParams.get('q') ?? '';
11+
const isSearchMode = Boolean(keyword.trim());
12+
13+
return (
14+
<main>
15+
<BannerSection keyword={keyword} />
16+
{isSearchMode ? (
17+
<ExperienceList keyword={keyword} isSearchMode />
18+
) : (
19+
<>
20+
<PopularExperiences />
21+
<ExperienceList />
22+
</>
23+
)}
24+
</main>
25+
);
26+
}

src/app/(with-header)/components/ExperienceList.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Dropdown from '@components/Dropdown';
88
import Pagination from '@components/Pagination';
99
import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
1010
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
11+
import ExperienceCardSkeleton from '@/app/(with-header)/components/Skeletons/ExperienceCardSkeleton';
1112
import { getExperiences, ExperienceListResult } from '@/app/api/experiences/getExperiences';
1213
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
1314
import {
@@ -26,15 +27,14 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
2627
const [selectedCategory, setSelectedCategory] = useState<ActivityCategory>(ACTIVITY_CATEGORIES[0]);
2728
const [sortOption, setSortOption] = useState<string | undefined>('');
2829

29-
// TanStack Query 사용 (타입 명시 필수)
30-
const { data, isLoading, error } = useQuery<ExperienceListResult>({
30+
const { data, isLoading } = useQuery<ExperienceListResult>({
3131
queryKey: ['experiences', currentPage, selectedCategory, sortOption, keyword],
3232
queryFn: () =>
3333
getExperiences({
3434
page: currentPage,
3535
sort: sortOption,
3636
category: selectedCategory,
37-
keyword,
37+
keyword: keyword || undefined,
3838
}),
3939
placeholderData: (prev) => prev,
4040
});
@@ -44,7 +44,8 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
4444
const totalPage = Math.ceil(totalCount / 8);
4545

4646
return (
47-
<section className='max-w-1200 m-auto px-24 md:px-0 pb-83'>
47+
<section className='max-w-1200 m-auto px-24 lg:px-0 pb-83'>
48+
{/* 🔍 검색 모드일 때 문구 표시 */}
4849
{isSearchMode && keyword && (
4950
<>
5051
<p className="text-left text-lg font-semibold ml-4 md:ml-0 mt-32">
@@ -53,7 +54,7 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
5354
<p className="text-left text-sm font-normal ml-4 md:ml-0 mt-8 mb-16">
5455
<span className="font-semibold">{totalCount}</span>개의 결과
5556
</p>
56-
{experiences.length === 0 && (
57+
{experiences.length === 0 && !isLoading && (
5758
<p className="text-center text-gray-500 mt-32">검색 결과가 없습니다.</p>
5859
)}
5960
</>
@@ -87,13 +88,12 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
8788
<h2 className='text-xl md:text-3xl font-bold'>🛼 모든 체험</h2>
8889
)}
8990

90-
{isLoading ? (
91-
<p className="text-center">체험을 불러오는 중입니다...</p>
92-
) : error ? (
93-
<p className="text-center text-red-500">체험 데이터를 불러오는 데 실패했습니다 😢</p>
94-
) : (
95-
<div className='grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 md:gap-16 lg:gap-24 mt-24'>
96-
{experiences.map((exp) => (
91+
{/* 체험 카드 목록 */}
92+
<div className='grid grid-cols-2 grid-rows-2 md:grid-cols-3 md:grid-rows-3 lg:grid-cols-4 lg:grid-rows-2 gap-8 md:gap-16 lg:gap-24 mt-24'>
93+
{isLoading ? (
94+
Array.from({ length: 8 }).map((_, idx) => <ExperienceCardSkeleton key={idx} />)
95+
) : (
96+
experiences.map((exp) => (
9797
<Link key={exp.id} href={`/activities/${exp.id}`}>
9898
<ExperienceCard
9999
imageUrl={exp.bannerImageUrl}
@@ -103,9 +103,9 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
103103
title={exp.title}
104104
/>
105105
</Link>
106-
))}
107-
</div>
108-
)}
106+
))
107+
)}
108+
</div>
109109
</div>
110110

111111
{experiences.length > 0 && (

src/app/(with-header)/components/PopularExperiences.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
'use client';
22

3-
import IconArrowLeft from '@assets/svg/left-arrow';
4-
import IconArrowRight from '@assets/svg/right-arrow';
53
import { useRef } from 'react';
64
import Link from 'next/link';
75
import { useQuery } from '@tanstack/react-query';
86

7+
import IconArrowLeft from '@assets/svg/left-arrow';
8+
import IconArrowRight from '@assets/svg/right-arrow';
9+
910
import PopularCard from '@/app/(with-header)/components/PopularCard';
11+
import PopularCardSkeleton from '@/app/(with-header)/components/Skeletons/PopularCardSkeleton';
12+
1013
import { getPopularExperiences } from '@/app/api/experiences/getPopularExperiences';
1114

1215
export default function PopularExperiences() {
1316
const sliderRef = useRef<HTMLDivElement>(null);
1417

15-
// TanStack Query 도입
1618
const { data, isLoading, error } = useQuery({
1719
queryKey: ['popularExperiences'],
1820
queryFn: getPopularExperiences,
1921
});
2022

2123
const scrollByCard = (direction: 'left' | 'right') => {
2224
if (!sliderRef.current) return;
25+
2326
const card = sliderRef.current.querySelector('.card');
2427
if (!(card instanceof HTMLElement)) return;
2528

@@ -33,11 +36,9 @@ export default function PopularExperiences() {
3336
});
3437
};
3538

36-
if (isLoading) return <p className="text-center">인기 체험을 불러오는 중입니다...</p>;
37-
if (error || !data) return <p className="text-center text-red-500">데이터를 불러오는 데 실패했어요 😢</p>;
38-
3939
return (
4040
<section className='pt-24 md:pt-34 pl-24 lg:pl-0 pb-40 lg:pb-33 lg:max-w-1200 lg:w-full mx-auto'>
41+
{/* 제목 + 버튼 */}
4142
<div className='flex justify-between items-center pb-16 md:pb-32 mb-6'>
4243
<h2 className='text-xl md:text-3xl font-bold'>🔥 인기 체험</h2>
4344
<div className='flex gap-2'>
@@ -46,23 +47,37 @@ export default function PopularExperiences() {
4647
</div>
4748
</div>
4849

50+
{/* 카드 영역 */}
4951
<div
5052
ref={sliderRef}
5153
className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar'
5254
>
53-
{data.activities.map((exp) => (
54-
<div key={exp.id} className='flex-shrink-0 card'>
55-
<Link href={`/activities/${exp.id}`} className='flex-shrink-0 card'>
56-
<PopularCard
57-
imageUrl={exp.bannerImageUrl}
58-
price={exp.price}
59-
rating={exp.rating}
60-
reviews={exp.reviewCount}
61-
title={exp.title}
62-
/>
63-
</Link>
64-
</div>
65-
))}
55+
{error ? (
56+
<p className="text-red-500 text-sm">인기 체험을 불러오는 데 실패했습니다 😢</p>
57+
) : isLoading || !data ? (
58+
Array.from({ length: 4 }).map((_, idx) => (
59+
<div key={idx} className="flex-shrink-0 card">
60+
<PopularCardSkeleton />
61+
</div>
62+
))
63+
) : (
64+
data.activities.map((exp) => (
65+
<div key={exp.id} className='flex-shrink-0 card'>
66+
<Link
67+
href={`/activities/${exp.id}`}
68+
className='flex-shrink-0 card'
69+
>
70+
<PopularCard
71+
imageUrl={exp.bannerImageUrl}
72+
price={exp.price}
73+
rating={exp.rating}
74+
reviews={exp.reviewCount}
75+
title={exp.title}
76+
/>
77+
</Link>
78+
</div>
79+
))
80+
)}
6681
</div>
6782
</section>
6883
);

src/app/(with-header)/components/SearchBar.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22

33
import Button from '@components/Button';
44
import Input from '@components/Input';
5-
import { FormEvent,useState } from 'react';
5+
import { useRouter } from 'next/navigation';
6+
import { FormEvent, useState, useEffect } from 'react';
67

78
interface SearchBarProps {
8-
onSearch: (keyword: string) => void;
9+
keyword: string;
910
}
1011

11-
export default function SearchBar({ onSearch }: SearchBarProps) {
12-
const [searchValue, setSearchValue] = useState('');
12+
export default function SearchBar({ keyword }: SearchBarProps) {
13+
const [searchValue, setSearchValue] = useState(keyword);
14+
const router = useRouter(); // useRouter는 반드시 클라이언트에서 선언되어야 함
1315

16+
// 검색 버튼 클릭 시 쿼리 파라미터 변경
1417
const handleSubmit = (e: FormEvent) => {
1518
e.preventDefault();
16-
onSearch(searchValue); // 부모(HomePage)로 검색어 전달
17-
setSearchValue(''); // 선택 사항: 검색어 초기화
19+
const trimmed = searchValue.trim();
20+
if (!trimmed) return;
21+
router.push(`/?q=${encodeURIComponent(trimmed)}`);
1822
};
1923

24+
// 외부에서 keyword prop이 바뀌면 input도 동기화
25+
useEffect(() => {
26+
setSearchValue(keyword);
27+
}, [keyword]);
28+
2029
return (
2130
<section className='flex lg:w-full lg:max-w-1200 lg:ml-auto lg:mr-auto justify-center px-16 lg:px-0'>
2231
<div className='flex flex-col w-full gap-15 md:gap-32 px-24 py-16 md:px-24 md:py-32 rounded-[16px] bg-white shadow-md'>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function ExperienceCardSkeleton() {
2+
return (
3+
<div className="flex flex-col w-full gap-16 animate-pulse">
4+
{/* 썸네일 영역 */}
5+
<div className="w-full h-168 md:h-221 lg:h-283 rounded-[20px] bg-gray-200" />
6+
7+
{/* 텍스트 영역 */}
8+
<div className="flex flex-col gap-10">
9+
<div className="w-1/2 h-16 bg-gray-200 rounded" />
10+
<div className="w-full h-20 bg-gray-200 rounded" />
11+
<div className="w-2/3 h-16 bg-gray-200 rounded" />
12+
</div>
13+
</div>
14+
);
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function PopularCardSkeleton() {
2+
return (
3+
<div className="relative w-186 h-186 md:w-384 md:h-384 rounded-[20px] overflow-hidden shadow-md bg-gray-200 animate-pulse">
4+
{/* 어두운 오버레이 레이어 */}
5+
<div className="absolute inset-0 bg-gradient-to-r from-black to-transparent opacity-50" />
6+
7+
{/* 텍스트 정보 블록 위치 */}
8+
<div className="absolute bottom-12 flex flex-col gap-6 md:gap-20 px-20 py-12">
9+
<div className="w-24 h-16 rounded bg-gray-300" /> {/* 별점 */}
10+
<div className="w-40 h-24 md:w-60 md:h-28 rounded bg-gray-300" /> {/* 타이틀 */}
11+
<div className="w-32 h-16 md:w-40 md:h-20 rounded bg-gray-300" /> {/* 가격 */}
12+
</div>
13+
</div>
14+
);
15+
}

src/app/(with-header)/page.tsx

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
'use client';
1+
export const dynamic = 'force-dynamic';
22

3-
import { useState } from 'react';
4-
import BannerSection from '@/app/(with-header)/components/BannerSection';
5-
import PopularExperiences from '@/app/(with-header)/components/PopularExperiences';
6-
import ExperienceList from '@/app/(with-header)/components/ExperienceList';
3+
import BasePage from '@/app/(with-header)/components/BasePage';
74

8-
export default function HomePage() {
9-
const [searchKeyword, setSearchKeyword] = useState('');
10-
11-
return (
12-
<main>
13-
<BannerSection onSearch={setSearchKeyword} />
14-
15-
{searchKeyword ? (
16-
<ExperienceList keyword={searchKeyword} isSearchMode />
17-
) : (
18-
<>
19-
<PopularExperiences />
20-
<ExperienceList />
21-
</>
22-
)}
23-
</main>
24-
);
25-
}
5+
export default function Page() {
6+
return <BasePage />;
7+
}

0 commit comments

Comments
 (0)