-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/45-3 메인페이지 모든 체험 UI 구현 및 API 연동 #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
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 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. 📒 Files selected for processing (3)
Walkthrough이 변경 사항은 경험(Experience) 목록 및 인기 경험을 동적으로 조회하고, 검색, 카테고리 필터, 정렬, 페이지네이션 기능을 추가하는 대규모 UI 및 API 통합 작업입니다. 새로운 컴포넌트와 API 유틸리티가 도입되었으며, 기존 컴포넌트들은 확장 및 리팩토링되었습니다. Changes
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: 검색 결과 렌더링
sequenceDiagram
participant HomePage
participant PopularExperiences
participant API as getPopularExperiences API
HomePage->>PopularExperiences: 마운트 시
PopularExperiences->>API: getPopularExperiences()
API-->>PopularExperiences: 인기 경험 목록
PopularExperiences-->>HomePage: 인기 경험 카드 렌더링
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~18 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
📒 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} /> |
There was a problem hiding this comment.
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' /> |
There was a problem hiding this comment.
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.
| <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.
| interface Props { | ||
| imageUrl: string; | ||
| title: string; | ||
| rating: number; | ||
| reviews: number; | ||
| price: number; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| <Image | ||
| src={imageUrl} | ||
| alt={title} | ||
| fill | ||
| className='object-cover' | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미지 컴포넌트의 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.
| <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.
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.
| <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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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, |
There was a problem hiding this comment.
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.
| 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; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
API 함수 구조는 올바르지만 에러 처리와 환경변수 검증이 필요합니다
커서 기반 페이지네이션과 응답 타입 구조가 적절하게 구현되었습니다. 하지만 몇 가지 개선사항이 있습니다:
- 환경변수
NEXT_PUBLIC_TEAM_ID의 존재 여부 검증 - 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.
src/constants/SortPrices.tsx
Outdated
| export const SORT_OPTIONS = ['가격이 낮은 순', '가격이 높은 순'] as const; | ||
| export type SortOption = (typeof SORT_OPTIONS)[number]; No newline at end of file |
There was a problem hiding this comment.
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.
There was a problem hiding this 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
📒 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: 깔끔한 조건부 렌더링 구현입니다.검색 모드와 일반 모드를 명확히 구분한 조건부 렌더링이 사용자 경험을 향상시킬 것입니다.
isSearchModeprop 전달도 적절합니다.src/app/(with-header)/components/BannerSection.tsx (2)
5-7: Props 인터페이스 정의가 적절합니다.
BannerSectionProps인터페이스가 명확하게 정의되어 타입 안전성을 보장합니다.
35-35: SearchBar에 콜백 전달이 올바르게 구현되었습니다.
onSearchprop이 적절히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인터페이스가 명확하고, 컴포넌트의 책임이 잘 분리되어 있습니다. 선택적classNameprop도 적절합니다.
19-30: 카테고리 버튼 구현이 적절합니다
ACTIVITY_CATEGORIES를 순회하며 선택 상태를 올바르게 관리하고 있습니다.Button컴포넌트의categoryvariant 사용도 적절합니다.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가 깔끔합니다
CategoryFilter와Dropdown컴포넌트의 통합이 잘 이루어져 있고, 상태 변경 시 페이지 리셋 로직도 적절합니다.
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 응답의
activities를experiences로 변환하는 로직이 명확하게 주석과 함께 구현되어 컴포넌트에서 사용하기 적절한 인터페이스를 제공합니다.
| {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> | ||
| ))} |
There was a problem hiding this comment.
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.
| {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.
minimo-9
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메인 페이지 만드시느라 고생 많으셨습니다! 추후에 효과들까지 넣으면 더 완성도 높게 가능할 것 같습니다!!
|
구현 수고하셨습니다! UI도 정말 깔끔한거같아요 api요청로직을 tanstack Query 사용하는 방법으로 적용해보시는것도 추천드릴게요! |
📌 변경 사항 개요
📝 상세 내용
🔗 관련 이슈
🖼️ 스크린샷(선택사항)
globalnomad-api-ui.mp4
💡 참고 사항
Summary by CodeRabbit
신규 기능
개선 사항
버그 수정
문서화
기타