Skip to content

Conversation

@BokyungCodes
Copy link
Contributor

@BokyungCodes BokyungCodes commented Jul 28, 2025

📌 변경 사항 개요

  • 모든 체험(ExperienceList) 섹션 UI 구현
  • 체험 목록 API 연동 및 필터링/정렬/페이지네이션 기능 적용

📝 상세 내용

  • 카드 UI: 반응형 Grid로 구성 (모바일 2x2, 태블릿 3x3, PC 4x2)
  • 카테고리 필터링: 카테고리 선택 시 해당 카테고리의 체험만 보여줌
  • 가격 정렬 드롭다운: 가격 낮은 순, 높은 순 정렬 가능
  • 페이지네이션: 8개씩 나눠서 페이지 표시, 페이지 변경 시 API 호출
  • API 연동
  • 정렬/카테고리 변경 시 페이지 초기화

🔗 관련 이슈

🖼️ 스크린샷(선택사항)

globalnomad-api-ui.mp4

💡 참고 사항

  • API 연동 후 404/빈 배열 응답되고 있음

Summary by CodeRabbit

  • 신규 기능

    • 경험(액티비티) 목록을 검색, 카테고리별 필터, 가격순 정렬, 페이지네이션 기능과 함께 볼 수 있는 리스트 및 카드 UI를 추가했습니다.
    • 인기 경험(액티비티) 영역이 실제 인기 데이터를 기반으로 동적으로 표시됩니다.
    • 카테고리 필터, 인기 경험 카드, 경험 카드 등 다양한 컴포넌트가 도입되었습니다.
  • 개선 사항

    • 검색창에서 키워드 입력 시 해당 키워드에 맞는 경험만 보여주도록 홈 화면이 동적으로 변경됩니다.
    • 가격 정렬 옵션이 추가되어 가격 낮은순/높은순으로 정렬할 수 있습니다.
    • 카테고리 목록에 '전체' 항목이 추가되고, 일부 카테고리명이 변경되었습니다.
  • 버그 수정

    • 없음
  • 문서화

    • 없음
  • 기타

    • 내부 데이터 타입 및 상수 정의가 추가되어 유지보수가 용이해졌습니다.

@BokyungCodes BokyungCodes linked an issue Jul 28, 2025 that may be closed by this pull request
3 tasks
@BokyungCodes BokyungCodes changed the title Feat/45-3 메인페이지 모든 체험 UI 구현 및 API 연 Feat/45-3 메인페이지 모든 체험 UI 구현 및 API 연동 Jul 28, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 28, 2025

Warning

Rate limit exceeded

@BokyungCodes has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 65deb36 and 563231f.

📒 Files selected for processing (3)
  • src/app/(with-header)/components/ExperienceList.tsx (1 hunks)
  • src/app/(with-header)/components/PopularExperiences.tsx (2 hunks)
  • src/app/api/experiences/getPopularExperiences.ts (1 hunks)

Walkthrough

이 변경 사항은 경험(Experience) 목록 및 인기 경험을 동적으로 조회하고, 검색, 카테고리 필터, 정렬, 페이지네이션 기능을 추가하는 대규모 UI 및 API 통합 작업입니다. 새로운 컴포넌트와 API 유틸리티가 도입되었으며, 기존 컴포넌트들은 확장 및 리팩토링되었습니다.

Changes

Cohort / File(s) Change Summary
경험 데이터 타입 및 응답 정의
src/types/experienceListTypes.ts
Experience, ExperienceResponse 인터페이스 추가로 경험 객체 및 API 응답 구조 정의
경험 목록 및 인기 경험 API 유틸
src/app/api/experiences/getExperiences.ts,
src/app/api/experiences/getPopularExperiences.ts
경험 목록 및 인기 경험을 조회하는 비동기 함수 추가, 쿼리 파라미터 및 응답 타입 지정
카테고리/정렬 상수
src/constants/categories.ts,
src/constants/SortPrices.ts
카테고리 상수에 '전체' 추가 및 일부 라벨 수정, 가격 정렬 옵션 상수 및 매핑 객체 신설
카테고리 필터 컴포넌트
src/app/(with-header)/components/CategoryFilter.tsx
카테고리 선택 및 변경 콜백을 지원하는 UI 컴포넌트 신규 도입
검색바/배너 컴포넌트
src/app/(with-header)/components/SearchBar.tsx,
src/app/(with-header)/components/BannerSection.tsx
검색 콜백(onSearch)을 props로 받아 상위 컴포넌트와 상태 연동, 인터페이스 및 시그니처 변경
경험 카드 컴포넌트
src/app/(with-header)/components/ExperienceCard.tsx,
src/app/(with-header)/components/PopularCard.tsx
경험 정보를 props로 받아 렌더링하도록 일반화, 스타일 및 레이아웃 개선, 인기 경험 카드 신규 도입
경험 목록/인기 경험 리스트
src/app/(with-header)/components/ExperienceList.tsx,
src/app/(with-header)/components/PopularExperiences.tsx
경험 리스트와 인기 경험을 API에서 동적으로 받아와 렌더링, 검색/필터/정렬/페이지네이션 기능 구현
메인 페이지
src/app/(with-header)/page.tsx
클라이언트 컴포넌트로 전환, 검색어 상태 추가, 검색 시 경험 리스트만 노출, 평상시 인기 경험+경험 리스트 노출, onSearch 상태 연동

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomePage
    participant BannerSection
    participant SearchBar
    participant ExperienceList
    participant API as getExperiences API

    User->>BannerSection: 검색어 입력
    BannerSection->>SearchBar: onSearch 콜백 전달
    SearchBar->>BannerSection: onSearch(검색어)
    BannerSection->>HomePage: onSearch(검색어)
    HomePage->>ExperienceList: keyword, isSearchMode 전달
    ExperienceList->>API: getExperiences({keyword, ...})
    API-->>ExperienceList: experiences, totalCount
    ExperienceList-->>User: 검색 결과 렌더링
Loading
sequenceDiagram
    participant HomePage
    participant PopularExperiences
    participant API as getPopularExperiences API

    HomePage->>PopularExperiences: 마운트 시
    PopularExperiences->>API: getPopularExperiences()
    API-->>PopularExperiences: 인기 경험 목록
    PopularExperiences-->>HomePage: 인기 경험 카드 렌더링
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Possibly related PRs

Suggested reviewers

  • evaain706
  • LeeCh0129

Poem

🐰
검색창에 꿈을 담아
경험 카드 줄지어
인기 여행, 카테고리
필터, 정렬, 모두 모여
API 부지런히 달려
리뷰어님, 오늘도
코드 속 여행을 떠나요!
✈️🌏

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/45-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@BokyungCodes BokyungCodes self-assigned this Jul 28, 2025
@BokyungCodes BokyungCodes added 🎨 style 마크업 및 스타일링 📬 API 서버 api 통신관련 ✨ Feature 기능구현 labels Jul 28, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 32

🔭 Outside diff range comments (1)
src/lib/api/api.ts (1)

1-11: 기본적인 API 설정은 적절하나 개선 사항 제안

Axios 인스턴스 생성이 올바르게 구현되었습니다. 하지만 프로덕션 환경을 고려한 추가 설정을 권장합니다.

다음과 같은 개선사항을 고려해보세요:

 import axios from 'axios';

 const api = axios.create({
   baseURL: process.env.NEXT_PUBLIC_API_URL,
+  timeout: 10000,
   headers: {
     'Content-Type': 'application/json',
   },
 });

+// 요청 인터셉터 (필요시)
+api.interceptors.request.use(
+  (config) => config,
+  (error) => Promise.reject(error)
+);
+
+// 응답 인터셉터 (에러 처리)
+api.interceptors.response.use(
+  (response) => response,
+  (error) => {
+    console.error('API Error:', error);
+    return Promise.reject(error);
+  }
+);

 export default api;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8651d64 and 4b60f39.

📒 Files selected for processing (14)
  • src/app/(with-header)/components/BannerSection.tsx (2 hunks)
  • src/app/(with-header)/components/CategoryFilter.tsx (1 hunks)
  • src/app/(with-header)/components/ExperienceCard.tsx (1 hunks)
  • src/app/(with-header)/components/ExperienceList.tsx (1 hunks)
  • src/app/(with-header)/components/PopularCard.tsx (1 hunks)
  • src/app/(with-header)/components/PopularExperiences.tsx (2 hunks)
  • src/app/(with-header)/components/SearchBar.tsx (1 hunks)
  • src/app/(with-header)/page.tsx (1 hunks)
  • src/app/api/experiences/getExperiences.ts (1 hunks)
  • src/app/api/experiences/getPopularExperiences.ts (1 hunks)
  • src/constants/SortPrices.tsx (1 hunks)
  • src/constants/categories.ts (1 hunks)
  • src/lib/api/api.ts (1 hunks)
  • src/types/experienceListTypes.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/constants/categories.ts (1)
src/types/activityDetailType.ts (1)
  • TitleProps (16-23)
src/types/experienceListTypes.ts (5)
src/types/bookingStoreTypes.ts (1)
  • BookingState (1-23)
src/types/activityDetailType.ts (3)
  • ReviewCardProps (7-12)
  • TitleProps (16-23)
  • ImageGridProps (1-4)
src/components/FloatingBox/BookingInterface.tsx (1)
  • BookingInterface (13-115)
src/types/paginationTypes.ts (1)
  • PaginationProps (1-5)
src/app/(with-header)/activities/[activitiesId]/page.tsx (1)
  • ActivityDetailPage (11-79)
src/app/api/experiences/getExperiences.ts (1)
src/types/experienceListTypes.ts (2)
  • ExperienceResponse (16-20)
  • Experience (1-14)
src/app/api/experiences/getPopularExperiences.ts (1)
src/types/experienceListTypes.ts (1)
  • Experience (1-14)
🪛 GitHub Actions: CI
src/app/(with-header)/page.tsx

[error] 9-9: @typescript-eslint/no-unused-vars: 'searchKeyword' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]/u.

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

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

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

[warning] 3-44: 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).

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

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

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

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

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

[warning] 3-69: 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).

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

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

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

[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).

🔇 Additional comments (17)
src/constants/categories.ts (1)

9-9: 카테고리 표기 개선 승인

중점(·)을 사용한 카테고리명 변경이 적절합니다. 한국어 표기법에 맞는 개선사항으로 UI 가독성을 향상시킵니다.

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

4-8: 타입 정의와 컴포넌트 구조 개선 승인

onSearch 콜백을 위한 인터페이스 정의와 컴포넌트 시그니처 변경이 적절합니다.

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

7-11: 인터페이스 정의와 props 구조가 올바릅니다

SearchBar 컴포넌트가 onSearch 콜백을 통해 상위 컴포넌트와 올바르게 통신하도록 구현되었습니다.


3-44: 파이프라인 경고 해결이 필요합니다

import 정렬과 props 정렬 관련 ESLint 경고를 해결해야 합니다.

-import { useState, FormEvent } from 'react';
-import Input from '@components/Input';
-import Button from '@components/Button';
+import { FormEvent, useState } from 'react';
+import Button from '@components/Button';
+import Input from '@components/Input';

또한 JSX props도 알파벳 순으로 정렬해야 합니다.

src/app/api/experiences/getPopularExperiences.ts (1)

4-12: 타입 정의가 적절합니다

인터페이스가 명확하고 Experience 타입과 일관성 있게 정의되었습니다.

src/types/experienceListTypes.ts (2)

1-14: 포괄적이고 잘 구조화된 Experience 인터페이스

Experience 인터페이스가 체험 엔티티의 모든 필요한 필드를 포함하고 있습니다. ID, 설명, 상업적 정보, 위치, 미디어, 평점, 타임스탬프 등이 적절히 정의되었습니다.


16-20: 페이지네이션 응답 구조가 적절합니다

ExperienceResponse 인터페이스가 커서 기반 페이지네이션 API 응답을 올바르게 모델링하고 있습니다. cursorId, totalCount, activities 배열의 조합이 적절합니다.

src/app/(with-header)/components/CategoryFilter.tsx (2)

13-34: 카테고리 필터 컴포넌트가 잘 구현되었습니다

수평 스크롤, 선택 상태 표시, 그라디언트 오버레이 등 UX를 고려한 좋은 구현입니다. 콜백 패턴을 통한 상태 관리도 적절합니다.


3-25: 파이프라인 경고 해결이 필요합니다

import 정렬과 props 정렬 관련 ESLint 경고를 해결해야 합니다.

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

JSX props도 알파벳 순으로 정렬하고 콜백을 마지막에 배치해야 합니다.

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

33-44: API 통합이 잘 구현되었습니다

useEffect를 사용한 데이터 페칭과 에러 처리가 적절하게 구현되었습니다. 비동기 함수를 useEffect 내부에서 올바르게 처리했습니다.


3-69: 파이프라인 경고 해결이 필요합니다

import 정렬과 props 정렬 관련 ESLint 경고를 해결해야 합니다. imports를 정렬하고 JSX props를 알파벳 순으로 정렬해야 합니다.


62-72: 동적 데이터 렌더링이 올바르게 구현되었습니다

API에서 가져온 데이터를 PopularCard 컴포넌트로 올바르게 매핑하고 있습니다. key prop 사용과 데이터 바인딩이 적절합니다.

src/app/(with-header)/components/PopularCard.tsx (1)

3-9: 인터페이스 정의가 명확하고 타입 안전성을 잘 보장합니다.

props의 타입 정의가 적절하며, 필요한 모든 데이터를 포함하고 있습니다.

src/app/(with-header)/components/ExperienceCard.tsx (2)

35-37: title에 대한 line-clamp 적용이 좋은 접근입니다.

긴 제목이 레이아웃을 깨뜨리지 않도록 line-clamp-2를 사용한 것이 적절합니다.


35-37: 제목의 line-clamp 처리가 잘 구현되었습니다.

긴 제목을 2줄로 제한하는 line-clamp-2 클래스 사용이 적절합니다. UI 일관성을 위한 좋은 선택입니다.

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

40-46: 키워드 변경 시 상태 초기화 로직이 적절합니다.

검색어가 변경될 때 카테고리, 정렬, 페이지를 초기화하는 것이 사용자 경험 측면에서 올바른 접근입니다.


76-76: 그리드 레이아웃 구현이 잘 되어있습니다.

반응형 그리드 레이아웃이 요구사항에 맞게 잘 구현되었습니다 (모바일 2x2, 태블릿 3x3, PC 4x2).

</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.

{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.

Comment on lines +3 to +9
interface Props {
imageUrl: string;
title: string;
rating: number;
reviews: number;
price: number;
}
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.

Comment on lines 22 to 27
<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.

Comment on lines 11 to 24
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
params: {
method: 'offset',
page,
size: 8,
...(category && { category: encodeURIComponent(category) }),
...(sort && { sort }),
...(keyword && { keyword }),
},
});

return res.data as ExperienceResponse;
};
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 const getExperiences = async ({ page, category, sort, keyword }: Params) => {
-  const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
-    params: {
-      method: 'offset',
-      page,
-      size: 8,
-      ...(category && { category: encodeURIComponent(category) }),
-      ...(sort && { sort }),
-      ...(keyword && { keyword }),
-    },
-  });
-
-  return res.data as ExperienceResponse;
+  try {
+    const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
+      params: {
+        method: 'offset',
+        page,
+        size: 8,
+        ...(category && { category: encodeURIComponent(category) }),
+        ...(sort && { sort }),
+        ...(keyword && { keyword }),
+      },
+    });
+
+    return res.data as ExperienceResponse;
+  } catch (error) {
+    console.error('Failed to fetch experiences:', error);
+    throw error;
+  }
 };
📝 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
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
params: {
method: 'offset',
page,
size: 8,
...(category && { category: encodeURIComponent(category) }),
...(sort && { sort }),
...(keyword && { keyword }),
},
});
return res.data as ExperienceResponse;
};
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
try {
const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
params: {
method: 'offset',
page,
size: 8,
...(category && { category: encodeURIComponent(category) }),
...(sort && { sort }),
...(keyword && { keyword }),
},
});
return res.data as ExperienceResponse;
} catch (error) {
console.error('Failed to fetch experiences:', error);
throw error;
}
};
🤖 Prompt for AI Agents
In src/app/api/experiences/getExperiences.ts around lines 11 to 24, the API call
lacks error handling, which can cause issues during network or response
failures. Wrap the API request in a try-catch block to catch any errors, and
handle them appropriately by either logging the error or returning a meaningful
error response to improve user experience and robustness.

🛠️ Refactor suggestion

API 에러 처리 및 환경 변수 유효성 검사를 추가해주세요.

현재 API 호출에서 에러 처리가 없고, 환경 변수의 존재 여부를 확인하지 않습니다.

 export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
+  if (!process.env.NEXT_PUBLIC_TEAM_ID) {
+    throw new Error('NEXT_PUBLIC_TEAM_ID environment variable is not set');
+  }
+
+  try {
     const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
       params: {
         method: 'offset',
         page,
         size: 8,
         ...(category && { category: encodeURIComponent(category) }),
         ...(sort && { sort }),
         ...(keyword && { keyword }),
       },
     });

     return res.data as ExperienceResponse;
+  } catch (error) {
+    console.error('Failed to fetch experiences:', error);
+    throw error;
+  }
 };
📝 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
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
params: {
method: 'offset',
page,
size: 8,
...(category && { category: encodeURIComponent(category) }),
...(sort && { sort }),
...(keyword && { keyword }),
},
});
return res.data as ExperienceResponse;
};
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
if (!process.env.NEXT_PUBLIC_TEAM_ID) {
throw new Error('NEXT_PUBLIC_TEAM_ID environment variable is not set');
}
try {
const res = await api.get(
`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`,
{
params: {
method: 'offset',
page,
size: 8,
...(category && { category: encodeURIComponent(category) }),
...(sort && { sort }),
...(keyword && { keyword }),
},
},
);
return res.data as ExperienceResponse;
} catch (error) {
console.error('Failed to fetch experiences:', error);
throw error;
}
};
🤖 Prompt for AI Agents
In src/app/api/experiences/getExperiences.ts around lines 11 to 24, add error
handling for the API call by wrapping the request in a try-catch block to catch
and handle any errors gracefully. Also, before making the API call, check if the
environment variable NEXT_PUBLIC_TEAM_ID exists and throw or handle an error if
it is missing to prevent undefined behavior.

params: {
method: 'offset',
page,
size: 8,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

페이지 크기를 상수로 분리하는 것을 권장합니다.

하드코딩된 size: 8 값이 여러 곳에서 사용될 가능성이 있습니다.

상수 파일로 분리해주세요:

// constants/pagination.ts
export const EXPERIENCES_PER_PAGE = 8;
-      size: 8,
+      size: EXPERIENCES_PER_PAGE,
🤖 Prompt for AI Agents
In src/app/api/experiences/getExperiences.ts at line 16, the page size is
hardcoded as 8. To improve maintainability and avoid magic numbers, define this
value as a constant in a separate file, for example, create a constant
EXPERIENCES_PER_PAGE in constants/pagination.ts with the value 8, then import
and use this constant instead of the hardcoded number.

🧹 Nitpick (assertive)

페이지 크기를 상수로 분리하는 것을 고려해보세요.

하드코딩된 페이지 크기(8)를 상수로 분리하면 유지보수성이 향상됩니다.

+const PAGE_SIZE = 8;
+
 export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
   const res = await api.get(`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`, {
     params: {
       method: 'offset',
       page,
-      size: 8,
+      size: PAGE_SIZE,
       ...(category && { category: encodeURIComponent(category) }),
       ...(sort && { sort }),
       ...(keyword && { keyword }),
     },
   });
🤖 Prompt for AI Agents
In src/app/api/experiences/getExperiences.ts at line 16, the page size is
hardcoded as 8. To improve maintainability, define this page size as a constant
at the top of the file or in a separate constants file, then replace the
hardcoded value with this constant in the code.

Comment on lines 15 to 29
export const getPopularExperiences = async ({ cursorId }: Params) => {
const res = await api.get<ResponseData>(
`/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`,
{
params: {
method: 'cursor',
cursorId,
sort: 'most_reviewed',
size: 10,
},
}
);

return res.data;
};
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 함수 구조는 올바르지만 에러 처리와 환경변수 검증이 필요합니다

커서 기반 페이지네이션과 응답 타입 구조가 적절하게 구현되었습니다. 하지만 몇 가지 개선사항이 있습니다:

  1. 환경변수 NEXT_PUBLIC_TEAM_ID의 존재 여부 검증
  2. API 호출 에러 처리
export const getPopularExperiences = async ({ cursorId }: Params) => {
+ if (!process.env.NEXT_PUBLIC_TEAM_ID) {
+   throw new Error('NEXT_PUBLIC_TEAM_ID is not defined');
+ }
+
+ try {
    const res = await api.get<ResponseData>(
      `/teams/${process.env.NEXT_PUBLIC_TEAM_ID}/activities`,
      {
        params: {
          method: 'cursor',
          cursorId,
          sort: 'most_reviewed',
          size: 10,
        },
      }
    );

    return res.data;
+ } catch (error) {
+   console.error('Failed to fetch popular experiences:', error);
+   throw error;
+ }
};
🤖 Prompt for AI Agents
In src/app/api/experiences/getPopularExperiences.ts around lines 15 to 29, the
function lacks validation for the environment variable NEXT_PUBLIC_TEAM_ID and
does not handle errors from the API call. Add a check to verify that
NEXT_PUBLIC_TEAM_ID is defined before making the request, throwing or returning
an error if it is missing. Wrap the API call in a try-catch block to catch any
errors during the request and handle them appropriately, such as logging the
error or returning a meaningful error response.

Comment on lines 1 to 2
export const SORT_OPTIONS = ['가격이 낮은 순', '가격이 높은 순'] as const;
export type SortOption = (typeof SORT_OPTIONS)[number]; No newline at end of file
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를 포함하지 않으므로 파일 확장자를 .ts로 변경하는 것이 적절합니다.

파일명을 SortPrices.ts로 변경하세요 (JSX가 없으므로).

🤖 Prompt for AI Agents
In src/constants/SortPrices.tsx at lines 1 to 2, the file currently uses the
.tsx extension but does not contain any JSX code. Rename the file extension from
.tsx to .ts to correctly reflect that it is a TypeScript file without JSX.

@BokyungCodes BokyungCodes added the 🐞 bug 버그관련 label Jul 28, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (16)
src/app/api/experiences/getPopularExperiences.ts (1)

14-26: 환경변수 검증과 에러 처리가 여전히 누락되었습니다.

과거 리뷰에서 지적된 환경변수 검증과 에러 처리가 여전히 구현되지 않았습니다.

+const teamId = process.env.NEXT_PUBLIC_TEAM_ID;
+if (!teamId) {
+  throw new Error('NEXT_PUBLIC_TEAM_ID is not defined');
+}
+
-const teamId = process.env.NEXT_PUBLIC_TEAM_ID;
-const url = `/${teamId}/activities`;

 export const getPopularExperiences = async () => {
+  try {
-    const res = await instance.get(url, {
+    const res = await instance.get(`/${teamId}/activities`, {
       params: {
         method: 'offset',
         sort: 'most_reviewed',
         size: 10,
       },
     });

     return res.data;
+  } catch (error) {
+    console.error('Failed to fetch popular experiences:', error);
+    throw error;
+  }
 };
src/app/(with-header)/components/BannerSection.tsx (1)

1-3: import 정렬 규칙을 여전히 준수하지 않고 있습니다.

과거 리뷰에서 지적된 import 정렬 문제가 여전히 해결되지 않았습니다. 외부 모듈(next/image)이 내부 모듈보다 먼저 와야 합니다.

+import SearchBar from '@/app/(with-header)/components/SearchBar';
 import Image from 'next/image';
-import SearchBar from '@/app/(with-header)/components/SearchBar';
src/app/(with-header)/components/SearchBar.tsx (1)

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

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

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

35-47: 로딩 상태 처리 추가를 고려해보세요

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

src/app/(with-header)/components/CategoryFilter.tsx (1)

31-31: 접근성 고려사항

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

src/app/(with-header)/components/PopularCard.tsx (3)

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

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


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

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


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

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

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

3-17: import 정렬을 수정해주세요

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


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

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


55-55: 페이지 크기 상수를 통일해주세요

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

src/app/(with-header)/components/ExperienceCard.tsx (2)

3-9: 인터페이스명을 더 구체적으로 변경해주세요.

이전 리뷰에서 지적된 것처럼 Props보다는 ExperienceCardProps와 같이 구체적인 이름을 사용하면 코드 가독성과 유지보수성이 향상됩니다.


22-27: Image 컴포넌트의 props 순서를 수정해주세요.

파이프라인 경고에서 지적한 대로 props를 알파벳 순으로 정렬하고 단축 props를 먼저 나열해야 합니다.

        <Image
-          fill
          alt={title}
          className='object-cover'
+          fill
          src={imageUrl}
        />
src/app/api/experiences/getExperiences.ts (3)

17-18: 환경 변수 유효성 검사를 추가해주세요.

이전 리뷰에서 지적된 것처럼 NEXT_PUBLIC_TEAM_ID 환경 변수의 존재 여부를 확인하여 undefined로 인한 문제를 방지해야 합니다.

 const teamId = process.env.NEXT_PUBLIC_TEAM_ID;
+
+if (!teamId) {
+  throw new Error('NEXT_PUBLIC_TEAM_ID environment variable is not set');
+}
+
 const url = `/${teamId}/activities`;

24-33: API 호출에 에러 처리를 추가해주세요.

이전 리뷰에서 지적된 것처럼 네트워크 오류나 API 응답 오류에 대한 처리가 없어 사용자 경험에 부정적 영향을 줄 수 있습니다.

export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
  const isAllCategory = category === '전체';

+  try {
    const res = await instance.get<ExperienceResponse>(url, {
      params: {
        method: 'offset',
        page,
        size: 8,
        ...(!isAllCategory && category && { category }),
        ...(sort && validSorts.includes(sort) && { sort }),
        ...(keyword && { keyword }),
      },
    });
+  } catch (error) {
+    console.error('Failed to fetch experiences:', error);
+    throw error;
+  }

28-28: 페이지 크기를 상수로 분리해주세요.

이전 리뷰에서 권장된 것처럼 하드코딩된 size: 8 값을 상수로 분리하면 유지보수성이 향상됩니다.

+const PAGE_SIZE = 8;
+
export const getExperiences = async ({ page, category, sort, keyword }: Params) => {
  // ...
  const res = await instance.get<ExperienceResponse>(url, {
    params: {
      method: 'offset',
      page,
-      size: 8,
+      size: PAGE_SIZE,
      // ...
    },
  });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acd1d68 and 65deb36.

📒 Files selected for processing (12)
  • src/app/(with-header)/components/BannerSection.tsx (2 hunks)
  • src/app/(with-header)/components/CategoryFilter.tsx (1 hunks)
  • src/app/(with-header)/components/ExperienceCard.tsx (1 hunks)
  • src/app/(with-header)/components/ExperienceList.tsx (1 hunks)
  • src/app/(with-header)/components/PopularCard.tsx (1 hunks)
  • src/app/(with-header)/components/PopularExperiences.tsx (2 hunks)
  • src/app/(with-header)/components/SearchBar.tsx (2 hunks)
  • src/app/(with-header)/page.tsx (1 hunks)
  • src/app/api/experiences/getExperiences.ts (1 hunks)
  • src/app/api/experiences/getPopularExperiences.ts (1 hunks)
  • src/constants/SortPrices.ts (1 hunks)
  • src/constants/categories.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/app/api/experiences/getPopularExperiences.ts (2)
src/types/experienceListTypes.ts (1)
  • Experience (1-14)
src/apis/instance.ts (1)
  • instance (100-100)
src/constants/categories.ts (1)
src/types/activityDetailType.ts (1)
  • TitleProps (16-23)
src/app/(with-header)/components/SearchBar.tsx (2)
src/components/Input.tsx (1)
  • Input (17-63)
src/components/Button.tsx (1)
  • Button (18-49)
src/app/api/experiences/getExperiences.ts (2)
src/types/experienceListTypes.ts (2)
  • ExperienceResponse (16-20)
  • Experience (1-14)
src/apis/instance.ts (1)
  • instance (100-100)
src/app/(with-header)/components/ExperienceList.tsx (8)
src/constants/categories.ts (2)
  • ActivityCategory (19-19)
  • ACTIVITY_CATEGORIES (8-16)
src/types/experienceListTypes.ts (1)
  • Experience (1-14)
src/app/api/experiences/getExperiences.ts (1)
  • getExperiences (21-39)
src/app/(with-header)/components/CategoryFilter.tsx (1)
  • CategoryFilter (13-34)
src/components/Dropdown.tsx (1)
  • Dropdown (29-205)
src/constants/SortPrices.ts (3)
  • SORT_OPTIONS (1-1)
  • SORT_LABEL_MAP (13-16)
  • SORT_VALUE_MAP (7-10)
src/app/(with-header)/components/ExperienceCard.tsx (1)
  • ExperienceCard (11-44)
src/components/Pagination.tsx (1)
  • Pagination (15-83)
🪛 GitHub Check: Code Quality Check
src/app/(with-header)/components/PopularExperiences.tsx

[failure] 39-39:
Expected 0 arguments, but got 1.

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

[failure] 92-92:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<SortOptionValue, "가격 낮은순" | "가격 높은순">'.

🪛 GitHub Actions: CI
src/app/(with-header)/components/ExperienceList.tsx

[error] 92-92: TypeScript error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<SortOptionValue, "가격 낮은순" | "가격 높은순">'.

🔇 Additional comments (23)
src/constants/categories.ts (1)

9-10: 하드코딩된 ‘문화 예술’ 문자열이 더 이상 존재하지 않습니다.

rg -g "*.ts" -g "*.tsx" -A3 -B3 "문화 예술" 검색 결과 일치 항목이 없음을 확인했습니다.
따라서 src/constants/categories.ts 의 변경사항은 코드베이스에 안전하게 반영되었으며 추가 수정이 필요 없습니다.

src/constants/SortPrices.ts (1)

1-16: 잘 구조화된 정렬 상수 정의입니다.

타입 안전성과 양방향 매핑을 제공하는 깔끔한 구현입니다. Record 타입 사용과 as const를 통한 타입 추론이 적절하게 적용되었습니다.

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

9-9: 이전 리뷰 지적사항이 적절히 수정되었습니다.

searchKeyword 상태가 이제 ExperienceList 컴포넌트에 적절히 전달되고 있어 검색 기능이 정상 작동할 것입니다.


15-22: 깔끔한 조건부 렌더링 구현입니다.

검색 모드와 일반 모드를 명확히 구분한 조건부 렌더링이 사용자 경험을 향상시킬 것입니다. isSearchMode prop 전달도 적절합니다.

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

5-7: Props 인터페이스 정의가 적절합니다.

BannerSectionProps 인터페이스가 명확하게 정의되어 타입 안전성을 보장합니다.


35-35: SearchBar에 콜백 전달이 올바르게 구현되었습니다.

onSearch prop이 적절히 SearchBar에 전달되어 검색 기능 통합이 완성되었습니다.

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

7-11: 인터페이스 정의가 깔끔합니다

SearchBarProps 인터페이스와 컴포넌트 시그니처가 올바르게 구현되었습니다. 타입 안전성이 잘 보장되고 있습니다.


4-5: import 정리가 잘 되었습니다

import 문의 정리가 깔끔하게 이루어졌습니다.


31-47: 폼 구현이 적절합니다

onSubmit 핸들러 연결과 UI 요소들의 구현이 올바르게 이루어졌습니다.

src/app/(with-header)/components/CategoryFilter.tsx (2)

7-17: 컴포넌트 인터페이스 설계가 우수합니다

CategoryFilterProps 인터페이스가 명확하고, 컴포넌트의 책임이 잘 분리되어 있습니다. 선택적 className prop도 적절합니다.


19-30: 카테고리 버튼 구현이 적절합니다

ACTIVITY_CATEGORIES를 순회하며 선택 상태를 올바르게 관리하고 있습니다. Button 컴포넌트의 category variant 사용도 적절합니다.

src/app/(with-header)/components/PopularCard.tsx (2)

3-9: 인터페이스 정의가 명확합니다

PopularCardProps 인터페이스의 타입 정의가 적절하고 필요한 모든 속성을 포함하고 있습니다.


21-26: Image 컴포넌트의 props 순서가 올바릅니다

props가 알파벳 순으로 정렬되어 있고 shorthand props가 먼저 배치되어 있습니다.

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

24-29: 상태 관리 구조가 적절합니다

컴포넌트의 상태 변수들이 명확하게 정의되어 있고, 각각의 역할이 잘 분리되어 있습니다.


61-76: 검색 모드 UI 구현이 잘 되었습니다

검색 결과 표시와 빈 결과에 대한 처리가 적절하게 구현되어 있습니다.


78-100: 필터링 및 정렬 UI가 깔끔합니다

CategoryFilterDropdown 컴포넌트의 통합이 잘 이루어져 있고, 상태 변경 시 페이지 리셋 로직도 적절합니다.


109-124: 그리드 레이아웃이 반응형으로 잘 구현되었습니다

모바일, 태블릿, PC에 대한 그리드 레이아웃이 PR 목표에 맞게 잘 구현되어 있습니다 (2x2, 3x3, 4x2).


127-134: 페이지네이션 구현이 적절합니다

결과가 있을 때만 페이지네이션을 표시하는 조건부 렌더링이 올바르게 구현되어 있습니다.

src/app/(with-header)/components/ExperienceCard.tsx (2)

11-17: 컴포넌트 함수 정의가 잘 구현되었습니다.

props 구조분해할당이 명확하고 React 모범 사례를 따르고 있습니다.


30-41: 컨텐츠 렌더링이 잘 구현되었습니다.

가격 포맷팅에 toLocaleString() 사용, 제목에 line-clamp-2 적용, 반응형 디자인을 고려한 스타일링이 적절합니다.

src/app/api/experiences/getExperiences.ts (3)

4-15: 인터페이스 정의가 올바르게 구현되었습니다.

ExperienceResponse 인터페이스가 types 파일과 일치하도록 activities 필드를 사용하여 이전 리뷰에서 지적된 타입 불일치 문제가 해결되었습니다.


21-22: 함수 시그니처와 카테고리 로직이 적절합니다.

한국어 UI에 맞춰 '전체' 카테고리를 올바르게 처리하는 로직이 잘 구현되었습니다.


35-38: 데이터 변환 로직이 잘 구현되었습니다.

API 응답의 activitiesexperiences로 변환하는 로직이 명확하게 주석과 함께 구현되어 컴포넌트에서 사용하기 적절한 인터페이스를 제공합니다.

Comment on lines +65 to 81
{popularExperiences.map((exp) => (
<div key={exp.id} className='flex-shrink-0 card'>
<Link
key={exp.id}
href={`/activities/${exp.id}`} // ✅ 상세페이지로 이동
className='flex-shrink-0 card' // ✅ 여기에 card 클래스 유지
>
<PopularCard
imageUrl={exp.bannerImageUrl}
price={exp.price}
rating={exp.rating}
reviews={exp.reviewCount}
title={exp.title}
/>
</Link>
</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)

중복된 key prop과 클래스 정리 필요

Link 컴포넌트와 외부 div에 동일한 key prop이 있고, card 클래스가 중복되어 있습니다.

다음과 같이 정리할 수 있습니다:

-        {popularExperiences.map((exp) => (
-          <div key={exp.id} className='flex-shrink-0 card'>
-            <Link
-              key={exp.id}
-              href={`/activities/${exp.id}`} // ✅ 상세페이지로 이동
-              className='flex-shrink-0 card' // ✅ 여기에 card 클래스 유지
-            >
+        {popularExperiences.map((exp) => (
+            <Link
+              key={exp.id}
+              href={`/activities/${exp.id}`}
+              className='flex-shrink-0 card'
+            >
              <PopularCard
                imageUrl={exp.bannerImageUrl}
                price={exp.price}
                rating={exp.rating}
                reviews={exp.reviewCount}
                title={exp.title}
              />
            </Link>
-          </div>
        ))}
📝 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
{popularExperiences.map((exp) => (
<div key={exp.id} className='flex-shrink-0 card'>
<Link
key={exp.id}
href={`/activities/${exp.id}`} // ✅ 상세페이지로 이동
className='flex-shrink-0 card' // ✅ 여기에 card 클래스 유지
>
<PopularCard
imageUrl={exp.bannerImageUrl}
price={exp.price}
rating={exp.rating}
reviews={exp.reviewCount}
title={exp.title}
/>
</Link>
</div>
))}
{popularExperiences.map((exp) => (
<Link
key={exp.id}
href={`/activities/${exp.id}`}
className='flex-shrink-0 card'
>
<PopularCard
imageUrl={exp.bannerImageUrl}
price={exp.price}
rating={exp.rating}
reviews={exp.reviewCount}
title={exp.title}
/>
</Link>
))}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx between lines 65 and
81, remove the duplicate key prop from the Link component since the outer div
already has the key. Also, eliminate the redundant 'card' class from the Link
component and keep it only on the outer div to avoid class duplication.

Copy link
Contributor

@minimo-9 minimo-9 left a comment

Choose a reason for hiding this comment

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

메인 페이지 만드시느라 고생 많으셨습니다! 추후에 효과들까지 넣으면 더 완성도 높게 가능할 것 같습니다!!

@evaain706
Copy link
Contributor

구현 수고하셨습니다! UI도 정말 깔끔한거같아요 api요청로직을 tanstack Query 사용하는 방법으로 적용해보시는것도 추천드릴게요!

@BokyungCodes BokyungCodes merged commit ef5f83c into develop Jul 28, 2025
2 checks passed
@BokyungCodes BokyungCodes deleted the feat/45-3 branch July 28, 2025 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 api 통신관련 🐞 bug 버그관련 ✨ Feature 기능구현 🎨 style 마크업 및 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 메인페이지 전체 구조 및 UI 구현

4 participants