Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 7 additions & 3 deletions src/app/(with-header)/components/BannerSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import Image from 'next/image';
import SearchBar from './SearchBar';
import SearchBar from '@/app/(with-header)/components/SearchBar';

export default function BannerSection() {
interface BannerSectionProps {
onSearch: (keyword: string) => void;
}

export default function BannerSection({ onSearch }: BannerSectionProps) {
return (
<section className='relative w-full h-240 md:h-550 mb-93'>
{/* 배경 이미지 */}
Expand All @@ -27,7 +31,7 @@ export default function BannerSection() {
</p>
</div>
<div className='absolute -bottom-100 left-0 right-0'>
<SearchBar />
<SearchBar onSearch={onSearch} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

JSX props 정렬 규칙 적용 권장

ESLint 규칙에 따라 props를 알파벳 순으로 정렬하는 것을 권장합니다.

현재 onSearch prop만 있어서 정렬이 필요하지 않지만, 향후 props 추가 시 알파벳 순 정렬을 유지하세요.

🤖 Prompt for AI Agents
In src/app/(with-header)/components/BannerSection.tsx at line 34, the JSX props
should be alphabetically sorted according to ESLint rules. Although there is
currently only one prop, onSearch, ensure that when adding more props in the
future, they are arranged in alphabetical order to maintain consistency and
comply with linting standards.

</div>
</section>
);
Expand Down
34 changes: 34 additions & 0 deletions src/app/(with-header)/components/CategoryFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import cn from '@/lib/cn';
import Button from '@/components/Button';
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';

interface CategoryFilterProps {
selectedCategory: ActivityCategory;
onChange: (category: ActivityCategory) => void;
className?: string;
}

export default function CategoryFilter({
selectedCategory,
onChange,
className,
}: CategoryFilterProps) {
return (
<div className={cn('relative flex w-full gap-8 overflow-x-auto whitespace-nowrap no-scrollbar', className)}>
{ACTIVITY_CATEGORIES.map((category) => (
<Button
key={category}
variant='category'
selected={selectedCategory === category}
onClick={() => onChange(category)}
className='flex-shrink-0 max-w-80 max-h-41 py-12 text-[16px] rounded-[15px]'
>
{category}
</Button>
))}
<div className='pointer-events-none absolute top-0 right-0 h-full w-100 bg-gradient-to-l from-white to-transparent' />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

접근성 고려사항

그라디언트 오버레이에 aria-hidden="true" 속성을 추가하는 것을 고려해보세요.

-<div className='pointer-events-none absolute top-0 right-0 h-full w-100 bg-gradient-to-l from-white to-transparent' />
+<div className='pointer-events-none absolute top-0 right-0 h-full w-100 bg-gradient-to-l from-white to-transparent' aria-hidden="true" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className='pointer-events-none absolute top-0 right-0 h-full w-100 bg-gradient-to-l from-white to-transparent' />
<div
className="pointer-events-none absolute top-0 right-0 h-full w-100 bg-gradient-to-l from-white to-transparent"
aria-hidden="true"
/>
🤖 Prompt for AI Agents
In src/app/(with-header)/components/CategoryFilter.tsx at line 31, the gradient
overlay div lacks accessibility consideration. Add the attribute
aria-hidden="true" to this div to ensure screen readers ignore it, improving
accessibility.

</div>
);
}
56 changes: 37 additions & 19 deletions src/app/(with-header)/components/ExperienceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import Image from 'next/image';

export default function ExperienceCard() {
interface Props {
imageUrl: string;
title: string;
rating: number;
reviews: number;
price: number;
}
Comment on lines +3 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

인터페이스명을 더 구체적으로 변경하는 것을 권장합니다.

Props보다는 ExperienceCardProps와 같이 구체적인 이름을 사용하면 코드 가독성과 유지보수성이 향상됩니다.

-interface Props {
+interface ExperienceCardProps {
   imageUrl: string;
   title: string;
   rating: number;
   reviews: number;
   price: number;
 }

-}: Props) {
+}: ExperienceCardProps) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface Props {
imageUrl: string;
title: string;
rating: number;
reviews: number;
price: number;
}
interface ExperienceCardProps {
imageUrl: string;
title: string;
rating: number;
reviews: number;
price: number;
}
// ...
export default function ExperienceCard({
imageUrl,
title,
rating,
reviews,
price,
}: ExperienceCardProps) {
// component implementation
}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceCard.tsx between lines 3 and 9,
rename the interface from Props to ExperienceCardProps to make the interface
name more specific and improve code readability and maintainability.

🧹 Nitpick (assertive)

인터페이스 이름을 더 구체적으로 변경하는 것을 고려해보세요.

현재 Props라는 일반적인 이름 대신 ExperienceCardProps와 같이 더 구체적인 이름을 사용하면 코드 가독성이 향상됩니다.

-interface Props {
+interface ExperienceCardProps {
   imageUrl: string;
   title: string;
   rating: number;
   reviews: number;
   price: number;
 }

-}: Props) {
+}: ExperienceCardProps) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceCard.tsx between lines 3 and 9,
rename the interface from the generic name 'Props' to a more specific name like
'ExperienceCardProps' to improve code readability and clarity about what the
props represent.


export default function ExperienceCard({
imageUrl,
title,
rating,
reviews,
price,
}: Props) {
return (
<div className='relative w-186 h-186 md:w-384 md:h-384 rounded-[20px] overflow-hidden shadow-md bg-white'>
{/* 배경 이미지 */}
<Image
src='/test/image1.png'
alt='체험 이미지'
className='w-full object-cover'
fill
/>
{/* 어두운 오버레이 */}
<div className='absolute inset-0 bg-gradient-to-r from-black to-transparent' />
{/* 텍스트 정보 블록 (카드 하단 위치 고정) */}
<div className='absolute bottom-12 flex flex-col gap-6 md:gap-20 px-20 py-12 text-white'>
{/* 별점 정보 */}
<span className='text-md'>⭐ 4.9 (293)</span>
{/* 체험명 (줄바꿈 포함, 반응형 크기) */}
<p className='text-2lg md:text-3xl font-semibold'>함께 배우면 즐거운<br />스트릿 댄스</p>
{/* 가격 정보 */}
<p className='text-lg md:text-xl'>₩ 38,000 <span className='text-gray-600 text-md'>/ 인</span></p>
<div className='flex flex-col w-full gap-16'>
{/* 썸네일 */}
<div className='relative w-full h-168 md:h-221 lg:h-283 rounded-[20px] overflow-hidden'>
<Image
src={imageUrl}
alt={title}
fill
className='object-cover'
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

이미지 컴포넌트의 props 순서를 수정해주세요.

Pipeline 오류에서 지적한 대로 props 순서를 정렬해야 합니다.

-        <Image
-          src={imageUrl}
-          alt={title}
-          fill
-          className='object-cover'
-        />
+        <Image
+          fill
+          alt={title}
+          className='object-cover'
+          src={imageUrl}
+        />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Image
src={imageUrl}
alt={title}
fill
className='object-cover'
/>
<Image
fill
alt={title}
className='object-cover'
src={imageUrl}
/>
🧰 Tools
🪛 GitHub Actions: CI

[warning] 24-26: Props should be sorted alphabetically and shorthand props must be listed before all other props (react/jsx-sort-props).

🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceCard.tsx between lines 22 and 27,
the props of the Image component are not in the correct order. Reorder the props
so that they follow the standard convention, typically placing src and alt
first, followed by layout or sizing props like fill, and then className last.
Adjust the JSX accordingly to fix the pipeline error related to prop ordering.

⚠️ Potential issue

props 정렬 순서를 수정해주세요.

파이프라인 경고에 따라 Image 컴포넌트의 props를 올바른 순서로 정렬해야 합니다.

        <Image
+          alt={title}
+          className='object-cover'
+          fill
           src={imageUrl}
-          alt={title}
-          fill
-          className='object-cover'
        />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Image
src={imageUrl}
alt={title}
fill
className='object-cover'
/>
<Image
alt={title}
className='object-cover'
fill
src={imageUrl}
/>
🧰 Tools
🪛 GitHub Actions: CI

[warning] 24-26: Props should be sorted alphabetically and shorthand props must be listed before all other props (react/jsx-sort-props).

🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceCard.tsx between lines 22 and 27,
the props of the Image component are not in the correct order according to the
pipeline warning. Reorder the props so that they follow the recommended
sequence, typically starting with src, alt, then layout-related props like fill,
and finally className. Adjust the prop order to comply with the linting or style
guidelines.

</div>

{/* 텍스트 정보 */}
<div className='flex flex-col'>
<span className='pb-10 text-lg text-black'>
{rating} <span className='text-gray-700 text-lg'>({reviews})</span>
</span>
<p className='pb-15 text-2lg font-semibold text-black line-clamp-2'>
{title}
</p>
<p className='text-xl text-black font-bold'>
{price.toLocaleString()} <span className='text-gray-900 text-lg font-medium'>/ 인</span>
</p>
</div>
</div>
);
Expand Down
98 changes: 98 additions & 0 deletions src/app/(with-header)/components/ExperienceList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import { useEffect, useState } from 'react';
import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
import Pagination from '@components/Pagination';
import Dropdown from '@components/Dropdown';
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
import { SORT_OPTIONS, SortOption } from '@/constants/SortPrices';
import { Experience } from '@/types/experienceListTypes';
import { getExperiences } from '@/app/api/experiences/getExperiences';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

import 순서를 정리하고 props 정렬을 수정해주세요.

Pipeline 오류에서 지적한 대로 import 정렬과 props 정렬이 필요합니다.

autofix를 실행하여 import를 정렬하고, JSX props를 알파벳 순으로 정렬해주세요.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 3-84: Run autofix to sort imports (simple-import-sort/imports); Callbacks must be listed after all other props and props should be sorted alphabetically (react/jsx-sort-props).

🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx lines 3 to 11, the import
statements are not sorted properly and the JSX props are not in alphabetical
order. Run the autofix tool or manually reorder the import statements
alphabetically by module path, and also reorder the JSX component props
alphabetically to fix the pipeline errors.

⚠️ Potential issue

import 정렬을 수정해주세요.

파이프라인 경고에 따라 import 문들을 올바른 순서로 정렬해야 합니다.

autofix를 실행하여 import 순서를 자동으로 정렬하거나, 다음과 같이 수정해주세요:

 'use client';

-import { useEffect, useState } from 'react';
-import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
-import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
-import Pagination from '@components/Pagination';
-import Dropdown from '@components/Dropdown';
-import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
-import { SORT_OPTIONS, SortOption } from '@/constants/SortPrices';
-import { Experience } from '@/types/experienceListTypes';
-import { getExperiences } from '@/app/api/experiences/getExperiences';
+import { useEffect, useState } from 'react';
+
+import { getExperiences } from '@/app/api/experiences/getExperiences';
+import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
+import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
+import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
+import { SORT_OPTIONS, SortOption } from '@/constants/SortPrices';
+import { Experience } from '@/types/experienceListTypes';
+import Dropdown from '@components/Dropdown';
+import Pagination from '@components/Pagination';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useEffect, useState } from 'react';
import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
import Pagination from '@components/Pagination';
import Dropdown from '@components/Dropdown';
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
import { SORT_OPTIONS, SortOption } from '@/constants/SortPrices';
import { Experience } from '@/types/experienceListTypes';
import { getExperiences } from '@/app/api/experiences/getExperiences';
import { useEffect, useState } from 'react';
import { getExperiences } from '@/app/api/experiences/getExperiences';
import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
import { SORT_OPTIONS, SortOption } from '@/constants/SortPrices';
import { Experience } from '@/types/experienceListTypes';
import Dropdown from '@components/Dropdown';
import Pagination from '@components/Pagination';
🧰 Tools
🪛 GitHub Actions: CI

[warning] 3-84: Run autofix to sort imports (simple-import-sort/imports); Callbacks must be listed after all other props and props should be sorted alphabetically (react/jsx-sort-props).

🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx between lines 3 and 11,
the import statements are not sorted correctly according to the project's import
order rules. Fix this by running the autofix command for import sorting or
manually reorder the imports so that they follow the correct grouping and
alphabetical order as per the linting configuration.


interface ExperienceListProps {
keyword?: string;
}

export default function ExperienceList({ keyword }: ExperienceListProps) {
const [currentPage, setCurrentPage] = useState(1);
const [selectedCategory, setSelectedCategory] = useState<ActivityCategory>(ACTIVITY_CATEGORIES[0]);
const [sortOption, setSortOption] = useState<SortOption | ''>('');
const [experiences, setExperiences] = useState<Experience[]>([]);
const [totalCount, setTotalCount] = useState(0);

// API 호출
useEffect(() => {
const fetchExperiences = async () => {
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

카테고리 필터링이 API 호출에 누락되었습니다.

selectedCategory 상태가 있지만 실제 API 호출에서 카테고리 파라미터가 전달되지 않고 있습니다.

       const res = await getExperiences({
         page: currentPage,
+        category: selectedCategory.value !== 'all' ? selectedCategory.value : undefined,
         sort: sortOption,
         keyword, // 검색어
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
const res = await getExperiences({
page: currentPage,
category: selectedCategory.value !== 'all' ? selectedCategory.value : undefined,
sort: sortOption,
keyword, // 검색어
});
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx around lines 27 to 31,
the API call to getExperiences is missing the category filter parameter. Add the
selectedCategory state as a parameter in the getExperiences call to ensure the
API request includes the category filter.


setExperiences(res.experiences);
setTotalCount(res.totalCount);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

API 응답 필드명 불일치와 에러 처리를 수정해주세요.

API에서 activities 필드를 반환하지만 experiences로 접근하고 있습니다. 또한 API 호출 실패에 대한 에러 처리가 없습니다.

     const fetchExperiences = async () => {
+      try {
         const res = await getExperiences({
           page: currentPage,
           sort: sortOption,
           keyword, // 검색어
         });

-        setExperiences(res.experiences);
+        setExperiences(res.activities);
         setTotalCount(res.totalCount);
+      } catch (error) {
+        console.error('Failed to fetch experiences:', error);
+        setExperiences([]);
+        setTotalCount(0);
+      }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
setExperiences(res.experiences);
setTotalCount(res.totalCount);
};
const fetchExperiences = async () => {
try {
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
setExperiences(res.activities);
setTotalCount(res.totalCount);
} catch (error) {
console.error('Failed to fetch experiences:', error);
setExperiences([]);
setTotalCount(0);
}
};
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx between lines 27 and 35,
the code incorrectly accesses the API response field as 'experiences' instead of
'activities', and lacks error handling for the API call. Update the code to use
'res.activities' instead of 'res.experiences' and add a try-catch block around
the API call to handle potential errors gracefully, such as logging the error or
showing a user-friendly message.


fetchExperiences();
}, [currentPage, sortOption, keyword]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

API 호출에 대한 에러 처리와 로딩 상태가 누락되었습니다.

네트워크 오류나 API 실패 시 사용자에게 적절한 피드백을 제공해야 합니다.

로딩 상태와 에러 처리를 추가해주세요:

 export default function ExperienceList({ keyword }: ExperienceListProps) {
   const [currentPage, setCurrentPage] = useState(1);
   const [selectedCategory, setSelectedCategory] = useState<ActivityCategory>(ACTIVITY_CATEGORIES[0]);
   const [sortOption, setSortOption] = useState<SortOption | ''>('');
   const [experiences, setExperiences] = useState<Experience[]>([]);
   const [totalCount, setTotalCount] = useState(0);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);

   // API 호출
   useEffect(() => {
     const fetchExperiences = async () => {
+      setLoading(true);
+      setError(null);
+      try {
         const res = await getExperiences({
           page: currentPage,
           sort: sortOption,
           keyword, // 검색어
         });

         setExperiences(res.experiences);
         setTotalCount(res.totalCount);
+      } catch (err) {
+        setError('체험 목록을 불러오는데 실패했습니다.');
+        console.error('Failed to fetch experiences:', err);
+      } finally {
+        setLoading(false);
+      }
     };

     fetchExperiences();
   }, [currentPage, sortOption, keyword]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchExperiences = async () => {
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
setExperiences(res.experiences);
setTotalCount(res.totalCount);
};
fetchExperiences();
}, [currentPage, sortOption, keyword]);
export default function ExperienceList({ keyword }: ExperienceListProps) {
const [currentPage, setCurrentPage] = useState(1);
const [selectedCategory, setSelectedCategory] = useState<ActivityCategory>(ACTIVITY_CATEGORIES[0]);
const [sortOption, setSortOption] = useState<SortOption | ''>('');
const [experiences, setExperiences] = useState<Experience[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// API 호출
useEffect(() => {
const fetchExperiences = async () => {
setLoading(true);
setError(null);
try {
const res = await getExperiences({
page: currentPage,
sort: sortOption,
keyword, // 검색어
});
setExperiences(res.experiences);
setTotalCount(res.totalCount);
} catch (err) {
setError('체험 목록을 불러오는데 실패했습니다.');
console.error('Failed to fetch experiences:', err);
} finally {
setLoading(false);
}
};
fetchExperiences();
}, [currentPage, sortOption, keyword]);
}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx around lines 25 to 38,
the useEffect hook that fetches experiences lacks error handling and loading
state management. Add a loading state variable to indicate when the fetch is in
progress and an error state variable to capture any API call failures. Update
the fetchExperiences function to set loading true before the call, catch any
errors to set the error state, and set loading false after completion. Also,
update the component to display appropriate feedback based on loading and error
states.


useEffect(() => {
if (keyword) {
setSelectedCategory(ACTIVITY_CATEGORIES[0]);
setSortOption('');
setCurrentPage(1);
}
}, [keyword]);

const totalPage = Math.ceil(totalCount / 8); // 한 페이지당 8개 기준
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

페이지 크기 상수를 통일해주세요.

하드코딩된 페이지 크기(8)가 API 파일과 중복됩니다. 공통 상수로 분리하는 것을 고려해보세요.

+const PAGE_SIZE = 8;
+
 export default function ExperienceList({ keyword }: ExperienceListProps) {
   // ... 
-  const totalPage = Math.ceil(totalCount / 8); // 한 페이지당 8개 기준
+  const totalPage = Math.ceil(totalCount / PAGE_SIZE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalPage = Math.ceil(totalCount / 8); // 한 페이지당 8개 기준
const PAGE_SIZE = 8;
export default function ExperienceList({ keyword }: ExperienceListProps) {
// ...
const totalPage = Math.ceil(totalCount / PAGE_SIZE);
// ...
}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx at line 48, the page size
is hardcoded as 8, which duplicates the value in the API file. To fix this,
define a common constant for the page size in a shared constants file and import
it here. Replace the hardcoded 8 with this imported constant to ensure
consistency and maintainability.


return (
<section className='max-w-1200 m-auto px-24 md:px-0 pb-83'>
{/* 필터 + 드롭다운 라인 */}
<div className='flex justify-between items-center mb-40'>
<CategoryFilter
selectedCategory={selectedCategory}
onChange={(category) => {
setSelectedCategory(category);
setCurrentPage(1); // 필터 변경 시 페이지 초기화
}}
/>
<Dropdown
options={SORT_OPTIONS}
placeholder='가격'
value={sortOption}
onChange={(sort) => {
setSortOption(sort);
setCurrentPage(1); // 정렬 변경 시 페이지 초기화
}}
className='w-120 h-41 text-[14px]'
/>
</div>

{/* 카드 리스트 */}
<div className='m-0'>
<h2 className='text-xl md:text-3xl font-bold'>🛼 모든 체험</h2>
<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'>
{experiences.map((exp) => (
<ExperienceCard
key={exp.id}
imageUrl={exp.bannerImageUrl}
title={exp.title}
rating={exp.rating}
reviews={exp.reviewCount}
price={exp.price}
/>
))}
</div>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

컴포넌트가 여러 책임을 가지고 있어 분리를 고려해보세요.

현재 컴포넌트가 상태 관리, API 호출, UI 렌더링을 모두 담당하고 있습니다. 더 나은 유지보수성을 위해 분리를 권장합니다.

다음과 같이 분리를 고려해보세요:

  1. useExperiences 커스텀 훅으로 상태 관리와 API 로직 분리
  2. ExperienceGrid 컴포넌트로 카드 목록 렌더링 분리
  3. ExperienceFilters 컴포넌트로 필터링 UI 분리
// hooks/useExperiences.ts
export const useExperiences = (keyword?: string) => {
  // 상태 관리 및 API 로직
};

// components/ExperienceGrid.tsx  
export const ExperienceGrid = ({ experiences }: { experiences: Experience[] }) => {
  // 그리드 렌더링 로직
};
🤖 Prompt for AI Agents
In src/app/(with-header)/components/ExperienceList.tsx around lines 74 to 88,
the component currently handles state management, API calls, and UI rendering
all together. To improve maintainability, refactor by extracting the state and
API logic into a custom hook named useExperiences, move the experience cards
rendering into a separate ExperienceGrid component, and isolate any filtering UI
into an ExperienceFilters component. This separation will modularize concerns
and simplify the ExperienceList component.


{/* 페이지네이션 */}
<Pagination
currentPage={currentPage}
totalPage={totalPage}
onPageChange={setCurrentPage}
/>
</section>
);
}
40 changes: 40 additions & 0 deletions src/app/(with-header)/components/PopularCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='relative w-186 h-186 md:w-384 md:h-384 rounded-[20px] overflow-hidden shadow-md bg-white'>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

하드코딩된 크기 값들을 상수로 관리하는 것을 고려해보세요.

w-186 h-186 md:w-384 md:h-384 같은 크기 값들이 여러 카드 컴포넌트에서 반복될 가능성이 있습니다.

크기 값들을 상수 파일로 분리하거나 Tailwind config에서 커스텀 클래스로 관리하는 것을 권장합니다:

// constants/cardSizes.ts
export const CARD_SIZES = {
  popular: {
    mobile: 'w-186 h-186',
    desktop: 'md:w-384 md:h-384'
  }
} as const;
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularCard.tsx at line 19, the width and
height values are hardcoded directly in the className string. To improve
maintainability and avoid repetition, extract these size values into a constants
file or define custom Tailwind classes in the Tailwind config. Replace the
hardcoded class strings with references to these constants or custom classes to
centralize size management.

{/* 배경 이미지 */}
<Image
src={imageUrl}
alt={title}
className='w-full object-cover'
fill
/>
{/* 어두운 오버레이 */}
<div className='absolute inset-0 bg-gradient-to-r from-black to-transparent' />
{/* 텍스트 정보 블록 (카드 하단 위치 고정) */}
<div className='absolute bottom-12 flex flex-col gap-6 md:gap-20 px-20 py-12 text-white'>
{/* 별점 정보 */}
<span className='text-md'>{rating} ({reviews})</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

접근성을 위해 별점 표시 개선을 고려해보세요.

현재 이모지 별(⭐)을 사용하고 있는데, 스크린 리더 사용자를 위해 의미적인 별점 표시를 고려해보세요.

-        <span className='text-md'>⭐ {rating} ({reviews})</span>
+        <span className='text-md' aria-label={`평점 ${rating}점, 리뷰 ${reviews}개`}>
+          ⭐ {rating} ({reviews})
+        </span>
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularCard.tsx at line 32, replace the
emoji star (⭐) with a semantic star rating element accessible to screen readers.
Use appropriate ARIA attributes or visually hidden text to convey the rating
meaningfully for assistive technologies, ensuring the star rating is
understandable without relying on the emoji alone.

{/* 체험명 (줄바꿈 포함, 반응형 크기) */}
<p className='text-2lg md:text-3xl font-semibold'>{title}</p>
{/* 가격 정보 */}
<p className='text-lg md:text-xl'>{price.toLocaleString()} <span className='text-gray-600 text-md'>/ 인</span></p>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

가격 표시 텍스트의 일관성을 확인하세요.

"/ 인" 텍스트가 회색으로 표시되어 가독성이 떨어질 수 있습니다. 다른 카드 컴포넌트와 일관성을 유지하는지 확인해주세요.

🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularCard.tsx at line 36, the "/ 인" text
is styled with a gray color that may reduce readability and cause inconsistency
with other card components. Review the styling of this span element and update
its className to match the color and font style used in other card components
for price display, ensuring consistent and clear text appearance.

</div>
</div>
);
}
46 changes: 33 additions & 13 deletions src/app/(with-header)/components/PopularExperiences.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
'use client';

import { useRef } from 'react';
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
import { useEffect, useRef, useState } from 'react';
import IconArrowRight from '@assets/svg/right-arrow';
import IconArrowLeft from '@assets/svg/left-arrow';
import PopularCard from '@/app/(with-header)/components/PopularCard';
import { getPopularExperiences } from '../../api/experiences/getPopularExperiences';
import { Experience } from '@/types/experienceListTypes';

export default function PopularExperiences() {
// 카드 슬라이더를 참조할 DOM ref
const sliderRef = useRef<HTMLDivElement>(null);

// 좌우 버튼 클릭 시 한 장씩 슬라이드 이동
const [popularExperiences, setPopularExperiences] = useState<Experience[]>([]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

로딩 상태 처리 추가를 고려해보세요

사용자 경험 향상을 위해 로딩 상태를 추가하는 것을 고려해보세요.

const [popularExperiences, setPopularExperiences] = useState<Experience[]>([]);
+const [isLoading, setIsLoading] = useState(true);

// useEffect 내부에서
const fetchPopular = async () => {
  try {
+   setIsLoading(true);
    const res = await getPopularExperiences({ cursorId: 0 });
    setPopularExperiences(res.activities);
  } catch (error) {
    console.error('인기 체험을 불러오는 데 실패했습니다:', error);
+ } finally {
+   setIsLoading(false);
  }
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [popularExperiences, setPopularExperiences] = useState<Experience[]>([]);
const [popularExperiences, setPopularExperiences] = useState<Experience[]>([]);
const [isLoading, setIsLoading] = useState(true);
// useEffect 내부에서
const fetchPopular = async () => {
try {
setIsLoading(true);
const res = await getPopularExperiences({ cursorId: 0 });
setPopularExperiences(res.activities);
} catch (error) {
console.error('인기 체험을 불러오는 데 실패했습니다:', error);
} finally {
setIsLoading(false);
}
};
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx at line 13, the state
for popularExperiences is defined but there is no loading state to indicate data
fetching status. Add a loading state variable using useState to track whether
the data is being loaded, update it accordingly during the fetch process, and
use this state to conditionally render a loading indicator or placeholder to
improve user experience.


// ✅ 좌우 버튼 클릭 시 한 장씩 슬라이드 이동
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({ cursorId: 0 });
setPopularExperiences(res.activities);
} catch (error) {
console.error('인기 체험을 불러오는 데 실패했습니다:', error);
}
Comment on lines +41 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

에러 처리 개선 고려사항

현재 콘솔 로그만 사용하고 있지만, 사용자에게 에러 상태를 표시하는 것을 고려해보세요.

} catch (error) {
  console.error('인기 체험을 불러오는 데 실패했습니다:', error);
+ // 에러 상태를 UI에 표시하는 로직 추가 고려
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error('인기 체험을 불러오는 데 실패했습니다:', error);
}
} catch (error) {
console.error('인기 체험을 불러오는 데 실패했습니다:', error);
// 에러 상태를 UI에 표시하는 로직 추가 고려
}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx around lines 38 to
40, the error handling currently only logs the error to the console. Improve
this by adding state management to track the error and display an appropriate
error message or UI element to the user, informing them that loading popular
experiences failed. Update the component to conditionally render this error
message based on the error state.

};

fetchPopular();
}, []);

return (
<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'>
{/* 섹션 제목 + 좌우 화살표 버튼 */}
Expand All @@ -44,10 +59,15 @@ 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' 클래스로 식별
<div key={idx} className='flex-shrink-0 card'>
<ExperienceCard />
{popularExperiences.map((exp) => (
<div key={exp.id} className='flex-shrink-0 card'>
<PopularCard
imageUrl={exp.bannerImageUrl}
title={exp.title}
rating={exp.rating}
reviews={exp.reviewCount}
price={exp.price}
/>
</div>
))}
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/app/(with-header)/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import { useState, FormEvent } from 'react';
import Input from '@components/Input';
import Button from '@components/Button';

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(''); // 선택 사항: 검색어 초기화
};
Comment on lines +16 to 18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

검색 후 입력 필드 초기화에 대한 UX 고려사항

검색 후 입력 필드를 자동으로 초기화하는 것이 항상 좋은 사용자 경험은 아닐 수 있습니다. 사용자가 검색어를 수정하거나 다시 확인하고 싶을 수 있습니다.

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    onSearch(searchValue);
-   setSearchValue('');          // 선택 사항: 검색어 초기화
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSearch(searchValue); // 부모(HomePage)로 검색어 전달
setSearchValue(''); // 선택 사항: 검색어 초기화
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onSearch(searchValue);
};
🤖 Prompt for AI Agents
In src/app/(with-header)/components/SearchBar.tsx around lines 16 to 18, the
current code clears the search input field immediately after a search, which may
hinder user experience by preventing users from easily modifying or reviewing
their search term. Modify the code to avoid automatically resetting the search
input after search submission, allowing the input value to remain for user
convenience.


return (
Expand Down
9 changes: 8 additions & 1 deletion src/app/(with-header)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
'use client';

import { useState } from 'react';
import BannerSection from '@/app/(with-header)/components/BannerSection';
import PopularExperiences from '@/app/(with-header)/components/PopularExperiences';
import ExperienceList from './components/ExperienceList';

export default function HomePage() {
const [searchKeyword, setSearchKeyword] = useState('');

return (
<main>
<BannerSection />
<BannerSection onSearch={setSearchKeyword} />
<PopularExperiences />
<ExperienceList />
</main>
);
}
Loading
Loading