-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/#54 API 응답 형식 수정 및 연결 테스트, 로직 수정 #55
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
- query key에 bounds 추가
- 등록 성공, 실패 시 페이지 이동 로직 추가
Walkthrough새로운 장소 생성 뮤테이션 훅과 createNewPlace 서비스가 추가되었고, 쿼리 키(byMap)에 bounds 인수가 도입되었습니다. 캠퍼스 키가 대문자로 변경되고, RefreshButton 컴포넌트와 refresh 아이콘, formatPrice 유틸이 추가되었습니다. 일부 mock 핸들러와 Step 모듈의 re-export가 제거되었고 페이지 컴포넌트들의 export 방식과 스타일이 조정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant PlaceNewPage as Place New Page
participant PlacePreview
participant CreateHook as useCreateNewPlace
participant PlaceService
participant Router
User->>PlaceNewPage: 폼 제출
PlaceNewPage->>PlacePreview: getValues 전달 (미리보기 사용)
PlacePreview->>PlaceService: byPreview(kakaoPlaceId)
PlaceService-->>PlacePreview: preview 데이터 반환
PlaceNewPage->>CreateHook: mutate(formData)
CreateHook->>PlaceService: createNewPlace(formData)
PlaceService-->>CreateHook: { status: 'OK' | 'ERROR' }
alt status === 'OK'
CreateHook->>Router: navigate CLIENT_PATH.PLACE_NEW_SUCCESS
else status === 'ERROR'
CreateHook->>Router: navigate CLIENT_PATH.PLACE_NEW_FAIL
end
sequenceDiagram
participant User
participant Map as MapComponent
participant RefreshBtn as RefreshButton
participant Query as usePlaceQueries.byMap
participant PlaceService
User->>Map: 지도 이동/상호작용
Map->>Map: bounds 계산, showUpdateButton = true
Map->>RefreshBtn: 렌더링(표시)
User->>RefreshBtn: 클릭
RefreshBtn->>Map: handleRefreshClick()
Map->>Query: 실행 (bounds 인수 포함)
Query->>PlaceService: GET /places?maxLat=...&maxLng=...
PlaceService-->>Query: 장소 데이터
Query-->>Map: 데이터로 지도 업데이트
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 주의가 필요한 영역:
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. 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.
Actionable comments posted: 5
🧹 Nitpick comments (6)
apps/web/app/_apis/queries/place.ts (1)
20-21: 쿼리 키에 bounds 포함 시 캐시 관리 고려 필요
bounds객체를 쿼리 키에 직접 포함하면, 지도를 조금씩 이동할 때마다 새로운 캐시 엔트리가 생성될 수 있습니다. 사용자가 지도를 자주 이동하는 경우 메모리에 많은 캐시 엔트리가 누적될 수 있습니다.다음 개선 방안을 고려해보세요:
- bounds 좌표값을 반올림하여 유사한 영역은 같은 캐시 키를 사용
- 캐시 크기 제한 설정 (queryClient의
gcTime조정)예시:
byMap: (bounds: MapBounds | null) => { const roundedBounds = bounds ? { minLatitude: Math.floor(bounds.minLatitude * 100) / 100, minLongitude: Math.floor(bounds.minLongitude * 100) / 100, maxLatitude: Math.ceil(bounds.maxLatitude * 100) / 100, maxLongitude: Math.ceil(bounds.maxLongitude * 100) / 100, } : null return [...PlaceQueryKeys.all(), 'map', roundedBounds] as const },apps/web/app/_utils/formatPrice/formatPrice.ts (1)
1-4: 가격 포맷팅 로직 확인함수가 간결하고 명확합니다. 다만
toLocaleString()에 명시적으로 한국 로케일을 지정하면 더 일관된 결과를 보장할 수 있습니다.선택적으로 다음과 같이 개선할 수 있습니다:
export const formatPrice = (price: number): string => { if (price === 0) return '미등록' - return `${price.toLocaleString()}원` + return `${price.toLocaleString('ko-KR')}원` }apps/web/app/_apis/mutations/useCreateNewPlace.ts (2)
11-12: 불필요한await제거
mutationFn이 이미 async 함수이므로 내부의await는 불필요합니다.- mutationFn: async (placeData: NewPlaceRequest) => - await createNewPlace(placeData), + mutationFn: (placeData: NewPlaceRequest) => createNewPlace(placeData),
20-21: 프로덕션 로깅 개선 권장
console.error는 개발 환경에서는 유용하지만, 프로덕션 환경에서는 적절한 로깅 시스템(예: Sentry, LogRocket 등)을 사용하는 것이 좋습니다.apps/web/app/_mocks/handlers/placeHandlers.ts (1)
4-39: 주석 처리된 코드 정리대부분의 mock 핸들러가 주석 처리되어 있습니다. 이 코드들이 더 이상 필요하지 않다면 완전히 제거하는 것이 코드베이스를 깔끔하게 유지하는 데 도움이 됩니다. Git 히스토리를 통해 언제든 이전 코드를 참조할 수 있습니다.
만약 향후 다시 활성화할 계획이라면, 해당 의도를 주석으로 명시하거나 관련 이슈 번호를 남겨두는 것이 좋습니다.
apps/web/app/places/new/page.tsx (1)
37-38: 사용하지 않는 Step 정의 제거
STEP_ORDER에SUCCESS와FAIL이 정의되어 있지만, 실제로 렌더링되는<Step>컴포넌트는 존재하지 않습니다. 이제useCreateNewPlace훅에서 내비게이션을 처리하므로 이 정의들을 제거하는 것이 좋습니다.export type StepType = | 'EVENT_WELCOME' | 'CAMPUS' | 'PLACE_SEARCH' | 'PLACE_PREVIEW' | 'RECOMMENDED_MENU' | 'DESCRIPTION' | 'CATEGORY' - | 'SUCCESS' - | 'FAIL' const STEP_ORDER: Record<StepType, string> = { EVENT_WELCOME: 'welcome', CAMPUS: '1', PLACE_SEARCH: '2', PLACE_PREVIEW: '3', RECOMMENDED_MENU: '4', DESCRIPTION: '5', CATEGORY: '6', - SUCCESS: 'success', - FAIL: 'fail', }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
apps/web/app/_apis/mutations/useCreateNewPlace.ts(1 hunks)apps/web/app/_apis/queries/place.ts(2 hunks)apps/web/app/_apis/schemas/place.ts(3 hunks)apps/web/app/_apis/services/category.ts(1 hunks)apps/web/app/_apis/services/place.ts(4 hunks)apps/web/app/_constants/campus.ts(1 hunks)apps/web/app/_constants/path.ts(3 hunks)apps/web/app/_mocks/handlers/index.ts(1 hunks)apps/web/app/_mocks/handlers/placeHandlers.ts(1 hunks)apps/web/app/_store/campus.ts(1 hunks)apps/web/app/_utils/formatPrice/formatPrice.ts(1 hunks)apps/web/app/_utils/formatPrice/index.ts(1 hunks)apps/web/app/map/MapComponent.tsx(3 hunks)apps/web/app/map/_components/CampusButtom/CampusButtonBox.tsx(3 hunks)apps/web/app/map/_components/CampusButtom/index.tsx(1 hunks)apps/web/app/map/_components/Marker/Marker.tsx(2 hunks)apps/web/app/map/_components/RefreshButton/RefreshButton.tsx(1 hunks)apps/web/app/map/_components/RefreshButton/index.tsx(1 hunks)apps/web/app/places/[id]/PlaceDetailPage.tsx(1 hunks)apps/web/app/places/[id]/_components/Menus/Menu.tsx(2 hunks)apps/web/app/places/new/_components/Step/Category/Category.tsx(2 hunks)apps/web/app/places/new/_components/Step/Fail/index.tsx(0 hunks)apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx(1 hunks)apps/web/app/places/new/_components/Step/RecommendedMenu/RecommendedMenu.tsx(2 hunks)apps/web/app/places/new/_components/Step/Success/index.tsx(0 hunks)apps/web/app/places/new/_components/Step/index.tsx(0 hunks)apps/web/app/places/new/fail/page.tsx(2 hunks)apps/web/app/places/new/page.tsx(5 hunks)apps/web/app/places/new/success/page.tsx(2 hunks)apps/web/next.config.ts(1 hunks)packages/ui/src/components/Icon/IconMap.ts(2 hunks)packages/ui/src/components/Icon/assets/icons/refresh.tsx(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/app/places/new/_components/Step/index.tsx
- apps/web/app/places/new/_components/Step/Fail/index.tsx
- apps/web/app/places/new/_components/Step/Success/index.tsx
🧰 Additional context used
🧬 Code graph analysis (12)
apps/web/app/map/_components/RefreshButton/RefreshButton.tsx (1)
packages/ui/src/utils/cn.ts (1)
cn(19-21)
apps/web/app/_apis/mutations/useCreateNewPlace.ts (3)
apps/web/app/_apis/schemas/place.ts (1)
NewPlaceRequest(80-80)apps/web/app/_apis/services/place.ts (1)
createNewPlace(105-115)apps/web/app/_constants/path.ts (1)
CLIENT_PATH(45-62)
apps/web/app/places/new/_components/Step/RecommendedMenu/RecommendedMenu.tsx (2)
apps/web/app/_utils/formatPrice/formatPrice.ts (1)
formatPrice(1-4)apps/web/app/_utils/formatPrice/index.ts (1)
formatPrice(1-1)
apps/web/app/places/[id]/PlaceDetailPage.tsx (1)
packages/ui/src/components/Banner/Banner.tsx (1)
Banner(38-122)
apps/web/app/_apis/services/place.ts (2)
apps/web/app/_constants/path.ts (1)
API_PATH(4-43)apps/web/app/_apis/schemas/place.ts (2)
NewPlaceRequest(80-80)NewPlaceResponse(81-83)
apps/web/app/_constants/campus.ts (1)
packages/ui/src/consts/colorVariant.ts (1)
COLOR_VARIANTS(1-26)
apps/web/app/places/[id]/_components/Menus/Menu.tsx (2)
apps/web/app/_utils/formatPrice/formatPrice.ts (1)
formatPrice(1-4)apps/web/app/_utils/formatPrice/index.ts (1)
formatPrice(1-1)
packages/ui/src/components/Icon/IconMap.ts (1)
packages/ui/src/components/Icon/assets/icons/refresh.tsx (1)
Refresh(3-27)
apps/web/app/places/new/page.tsx (2)
apps/web/app/_apis/mutations/useCreateNewPlace.ts (1)
useCreateNewPlace(7-25)apps/web/app/_apis/schemas/place.ts (1)
NewPlaceRequest(80-80)
apps/web/app/_apis/queries/place.ts (1)
apps/web/app/_apis/schemas/place.ts (1)
MapBounds(69-74)
apps/web/app/map/MapComponent.tsx (2)
apps/web/app/_apis/schemas/place.ts (1)
MapBounds(69-74)apps/web/app/map/_components/CampusButtom/CampusButtonBox.tsx (1)
CampusButtonBox(33-57)
apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx (3)
apps/web/app/_apis/schemas/place.ts (1)
NewPlaceRequest(80-80)apps/web/app/_mocks/data/place.ts (1)
PlacePreview(186-222)apps/web/app/_apis/queries/place.ts (1)
usePlaceQueries(27-69)
🔇 Additional comments (40)
apps/web/app/map/_components/Marker/Marker.tsx (1)
55-58: 이벤트 전파 방지 처리 잘 구현됨마커 클릭 시
stopPropagation()을 호출하여 이벤트 버블링을 방지하는 것은 좋은 개선입니다. 이를 통해 마커 클릭 시 의도하지 않은 지도 클릭 핸들러가 실행되는 것을 방지하고, PR의 목적인 지도 상호작용 개선(재검색 버튼 등)과 잘 맞물립니다.apps/web/next.config.ts (2)
28-32: LGTM!
blog.kakaocdn.net패턴이 올바르게 구성되었습니다. HTTPS 프로토콜을 사용하여 보안이 적절히 유지됩니다.
14-17: 원본 리뷰 의견 불일치 - 무시해도 됨코드베이스에서 실제
pstatic.net사용 패턴을 확인한 결과, 원본 리뷰 의견은 부정확합니다:
와일드카드 패턴 (
*.pstatic.net)은 필수입니다: 코드베이스에서search.pstatic.net,pup-review-phinf.pstatic.net,ldb-phinf.pstatic.net등 여러 서브도메인이 실제로 사용되고 있습니다. 이는 Naver CDN의 표준 구조이며, 와일드카드는 의도적이고 정당한 설정입니다.
port: ''는 불필요하지 않습니다: Next.js 공식 문서에 따르면port: ''는 유효한 설정이며, 정의되지 않은 포트와는 다른 의미를 갖습니다. 빈 문자열은 "포트 없는 URL만 매칭"을 의미하는 명시적 설정입니다.현재 코드는 올바르게 구성되어 있습니다.
Likely an incorrect or invalid review comment.
apps/web/app/_constants/campus.ts (3)
10-12: CAMPUS_LOCATION 키가 일관성있게 업데이트되었습니다.CAMPUS 객체의 키 변경과 동일하게 대문자로 업데이트되어 일관성이 유지됩니다.
16-18: CAMPUS_COLOR 키가 일관성있게 업데이트되었습니다.CAMPUS 및 CAMPUS_LOCATION과 동일하게 대문자 키로 업데이트되어 전체적인 일관성이 유지됩니다.
4-6: Breaking change 검증 완료 — 변경이 일관되게 적용되었습니다.코드베이스 전체에서 다음을 확인했습니다:
- 실행 코드에서 소문자 키 참조 없음: 캠퍼스 키가
singwan/cheonan/yesan에서SINGWAN/CHEONAN/YESAN으로 완전히 전환됨- 주석 처리 코드만 존재:
placeHandlers.ts의 주석 처리된 mock 코드(라인 25, 28, 31)에만 소문자 참조가 있으나, 실행되지 않으므로 문제 없음- 타입 일관성: 모든
CampusType사용처에서 새로운 대문자 키로 올바르게 정의되고 사용됨- 양쪽 앱 동기화:
apps/web과apps/admin모두 대문자로 일관되게 변경됨apps/web/app/_store/campus.ts (1)
10-10: 기본값이 새로운 대문자 키 형식에 맞게 올바르게 업데이트되었습니다.
apps/web/app/_constants/campus.ts의 변경사항과 일치하도록 기본 캠퍼스 값이 'singwan'에서 'SINGWAN'으로 정확히 업데이트되었습니다. CampusType이 자동으로 새로운 대문자 키를 반영하므로 타입 안정성도 유지됩니다.packages/ui/src/components/Icon/assets/icons/refresh.tsx (1)
1-27: LGTM! 표준 아이콘 컴포넌트 패턴Refresh 아이콘 컴포넌트가 올바르게 구현되었습니다. SVG props 타입 지정, 기본 크기 설정, props spread 처리가 적절합니다.
packages/ui/src/components/Icon/IconMap.ts (1)
66-66: LGTM! 아이콘 맵에 refresh 추가새로운 Refresh 아이콘이 iconMap에 올바르게 추가되었으며, 기존 패턴을 따르고 있습니다.
Also applies to: 137-137
apps/web/app/map/_components/CampusButtom/CampusButtonBox.tsx (1)
16-16: LGTM! 컴포넌트명 오타 수정 및 prop 이름 개선
CampusButtonBax→CampusButtonBox오타가 수정되었고,onCenterChanged→centerMapToCampus로 prop 이름이 더 명확해졌습니다. 로직 변경 없이 깔끔한 리팩토링입니다.Also applies to: 33-33, 42-42
apps/web/app/map/_components/RefreshButton/index.tsx (1)
1-1: LGTM! 표준 배럴 exportRefreshButton의 표준 배럴 export 패턴입니다.
apps/web/app/map/_components/CampusButtom/index.tsx (1)
2-2: LGTM! export 업데이트컴포넌트명 변경에 맞춰 export가 올바르게 업데이트되었습니다.
apps/web/app/map/_components/RefreshButton/RefreshButton.tsx (1)
1-26: LGTM! 깔끔한 RefreshButton 구현지도 재검색 버튼이 명확하게 구현되었습니다. 절대 위치 지정과 중앙 정렬, refresh 아이콘과 텍스트 조합이 적절합니다.
apps/web/app/map/MapComponent.tsx (3)
27-27: LGTM! 수동 재검색 패턴 구현지도 이동 시 수동 재검색 패턴이 잘 구현되었습니다.
showUpdateButton상태로 재검색 버튼을 제어하고, 사용자 위치/캠퍼스 중심 이동 시에는 즉시 재검색하는 로직이 명확합니다.
useEffect(refreshMapBounds, [refreshMapBounds])는 map 객체가 초기화된 후 bounds를 설정하는 의도로 보이며, useCallback 의존성과 함께 올바르게 동작합니다.Also applies to: 37-69, 75-79
83-85: LGTM! RefreshButton 및 CampusButtonBox 통합RefreshButton의 조건부 렌더링과 CampusButtonBox의 업데이트된 props 전달이 올바르게 구현되었습니다.
Also applies to: 91-91
94-94: 이벤트 전파 처리가 올바르게 구현되어 있습니다.PlaceMarker 컴포넌트에서
e.pointerEvent.stopPropagation()을 호출하고 있으며, 이는handlePreviewPlace()호출 전에 실행됩니다. 따라서 마커 클릭 시 이벤트가 부모 Container로 전파되지 않아 preview 초기화 문제가 발생하지 않습니다.<Marker onClick={(e) => { e.pointerEvent.stopPropagation() handlePreviewPlace() }} ... />apps/web/app/places/new/fail/page.tsx (1)
7-34: 변경사항 확인 완료컴포넌트명 변경 및 기본 내보내기 패턴으로의 전환이 적절합니다. 패딩 추가와 버튼 스타일 변경도 일관성 있게 적용되었습니다.
apps/web/app/_apis/queries/place.ts (1)
46-56: 쿼리 옵션 확인 완료
staleTime: 0과enabled: !!bounds설정이 적절합니다. 재검색 버튼을 통한 수동 갱신 의도와 일치합니다.apps/web/app/places/new/_components/Step/RecommendedMenu/RecommendedMenu.tsx (2)
4-4: 가격 포맷팅 유틸리티 도입
formatPrice유틸리티를 사용하여 가격 표시를 일관되게 처리하는 것은 좋은 개선입니다.
71-71: 가격 표시 형식 통일
formatPrice함수를 통해 가격이 0일 때 "미등록"으로 표시되고, 그 외의 경우 천 단위 구분자와 "원" 기호가 일관되게 적용됩니다.apps/web/app/_utils/formatPrice/index.ts (1)
1-1: 배럴 익스포트 적절함
formatPrice유틸리티의 배럴 익스포트가 표준 패턴에 맞게 구현되었습니다.apps/web/app/places/[id]/_components/Menus/Menu.tsx (2)
1-2: 타입 임포트 최적화 및 유틸리티 추가
PlaceDetail을 타입 전용 임포트로 변경하여 번들 크기를 최적화하고,formatPrice유틸리티를 추가하여 가격 표시를 일관되게 처리합니다.
24-24: 가격 포맷팅 일관성 개선
formatPrice함수를 사용하여 다른 컴포넌트들과 동일한 가격 표시 형식을 적용했습니다.apps/web/app/places/new/_components/Step/Category/Category.tsx (2)
72-75: 제출 버튼으로 변경버튼 타입이
submit으로 변경되어 폼 제출을 트리거하고,isLoading상태에 따라 비활성화됩니다. 뮤테이션 기반 플로우와 일관되게 구현되었습니다.
15-18: Props 변경 검증 완료 - 코드 변경 올바름
isSubmitting과nextStepprops가 제거되고isLoading이 추가된 변경 사항을 검증했습니다.확인 사항:
- Category 컴포넌트에서 새로운 Props 구조(setValue, getValues, isLoading)로 올바르게 마이그레이션됨
- 부모 페이지에서
isLoading={isSubmitting || isPending}으로 폼 제출 상태와 API 호출 상태를 통합하여 전달- 제출 버튼에서
disabled={isLoading}으로 로딩 상태 반영mutate(data)호출로 뮤테이션 기반 제출 플로우 구현 확인- 단계별 네비게이션(
nextStep)에서 뮤테이션 기반 제출로의 전환 완료모든 변경 사항이 일관되게 적용되었으며 폼 제출이 올바르게 처리됩니다.
apps/web/app/places/[id]/PlaceDetailPage.tsx (1)
26-41: 사진이 없을 때 Banner 렌더링 방지
photos.length > 0조건을 추가하여 사진이 없는 경우Banner컴포넌트가 렌더링되지 않도록 개선했습니다. 이는 불필요한 DOM 요소 생성을 방지하고 성능을 최적화합니다.apps/web/app/_apis/mutations/useCreateNewPlace.ts (1)
8-8:replacevspush사용 확인
router.replace를 사용하면 브라우저 히스토리에 추가되지 않아 뒤로가기가 불가능합니다. 맛집 등록 후 결과 페이지에서 뒤로가기를 방지하려는 의도라면 적절하지만, UX 관점에서 의도된 동작인지 확인이 필요합니다.apps/web/app/_apis/schemas/place.ts (2)
12-12:menuId필드 추가 확인메뉴 객체에
menuId필드가 추가되었습니다. 백엔드 API 응답 구조 변경에 맞춰 적절하게 스키마가 업데이트되었습니다.
81-83: 응답 타입 정의 양호
NewPlaceResponse타입이 명확하게 정의되어 있으며, string literal 타입을 사용하여 타입 안정성을 확보했습니다.apps/web/app/_apis/services/place.ts (3)
105-115: 응답 처리 일관성 확인 필요
createNewPlace함수는 다른 API 함수들과 달리response.data가 아닌data를 직접 반환하고 있습니다. 다른 함수들은 모두const { data: response } = await axiosInstance...; const { data } = response패턴을 사용하는데, 이 함수만const { data } = await axiosInstance.post(...)를 사용합니다.백엔드 API가 POST 요청에 대해서는 다른 응답 구조를 사용하는 것이 맞다면 문제없지만, GET과 동일한 래핑 구조를 사용한다면 일관성을 위해 같은 패턴을 적용해야 합니다.
백엔드 API 응답 구조를 확인해주세요:
- GET 요청:
{ data: { data: [...] } }- POST 요청:
{ data: { status: ... } }또는{ status: ... }
24-103: API 응답 래퍼 패턴 적용 확인모든 GET 요청에서 일관되게 응답 래퍼 패턴(
data: response→response.data)을 적용했습니다. 백엔드 API 응답 구조 변경에 맞춰 적절하게 업데이트되었습니다.
108-112: 데이터 정규화 로직 양호
tagIds와categoryIds를 숫자 배열로 변환하는 정규화 로직이 적절합니다. 백엔드 API가 숫자 배열을 기대하는 것으로 보이며, 프론트엔드에서 문자열로 관리하던 값을 전송 전에 변환하는 접근이 합리적입니다.apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx (1)
16-24: 동적 데이터 조회 개선하드코딩된 값('1') 대신
getValues().kakaoPlaceId를 사용하여 동적으로 장소 미리보기 데이터를 조회하도록 개선되었습니다. 이는 실제 사용자가 선택한 장소 정보를 가져오는 올바른 구현입니다.apps/web/app/places/new/success/page.tsx (1)
7-34: 컴포넌트 리팩토링 양호컴포넌트 이름을
SuccessPage로 변경하고 default export로 전환한 것은 Next.js의 페이지 컴포넌트 규칙과 잘 맞습니다. 패딩 추가와 className 업데이트도 UI 개선에 도움이 됩니다.apps/web/app/places/new/page.tsx (2)
75-77: 뮤테이션 통합 확인
useCreateNewPlace훅을 통해 장소 생성 로직이 깔끔하게 분리되었습니다.onSubmit에서 단순히mutate(data)를 호출하고, 성공/실패 처리는 훅 내부에서 관리하는 구조가 적절합니다.
169-169: 로딩 상태 관리 양호
isSubmitting과isPending을 결합하여 폼 검증 중과 API 호출 중 모두 로딩 상태를 표시하는 로직이 적절합니다. 사용자 경험 관점에서 두 단계 모두 대기 상태임을 명확히 전달합니다.apps/web/app/_constants/path.ts (4)
19-23: 새 장소 생성 엔드포인트 추가가 적절합니다.
CREATE: '/places'엔드포인트가 RESTful 규칙에 맞게 추가되었으며,NEW객체 하위에 적절하게 구조화되어 있습니다.
49-50: 등록 결과 페이지 분리가 적절합니다.성공/실패 결과 페이지를 독립된 경로로 분리하여 등록 과정과 등록 결과의 책임을 명확히 분리한 점이 좋습니다. PR 목표에서 언급한 '페이지 단위 책임 분리' 원칙을 잘 반영하고 있습니다.
7-8: 변경사항이 모두 정상적으로 적용되었습니다.검증 결과 이전
categoryId=쿼리 파라미터 형식은 코드베이스에 남아있지 않으며, 서비스 레이어(place.tsline 40)에서 새로운 경로 형식을 올바르게 사용하고 있습니다. 이 breaking change는 모든 호출 지점에서 일관되게 업데이트되었으므로 추가 수정이 필요하지 않습니다.
11-17: 쿼리 파라미터 형식 변경 검증 완료 - 코드 일관성 확인됨검증 결과, 이전 파라미터 형식(
northEastLatitude등)은 코드베이스에 남아있지 않으며, 모든BY_MAP호출처가 새로운 형식으로 올바르게 업데이트되었습니다:
- path.ts (라인 11-17): 새 형식으로 정의됨 (
minLat,maxLat,minLng,maxLng)- place.ts (라인 53): 서비스 레이어에서
MapBounds타입으로 올바르게 전달- MapComponent.tsx:
MapBounds타입으로 일관되게 사용- schemas/place.ts:
MapBounds타입 정의 및 import 체계 정상함수 매개변수 이름(minLatitude, maxLatitude 등)은 명확하게 유지되면서 쿼리 스트링은 간결한 형식으로 변환되어 백엔드 API 규격과 일치합니다. 타입 안전성도 전반적으로 유지되고 있습니다.
| const photos = z.array( | ||
| z.object({ | ||
| photoId: z.number().transform(String), | ||
| photoId: z.nullable(z.number().transform(String)), |
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.
🧩 Analysis chain
photoId의 nullable 변경 확인 필요
photoId가 nullable로 변경되었습니다. 이는 기존에 photoId를 사용하는 모든 컴포넌트에서 null 케이스를 처리하는지 확인이 필요합니다.
다음 스크립트로 photoId 사용처를 확인하세요:
🏁 Script executed:
#!/bin/bash
# photoId를 사용하는 코드 검색
rg -nP --type=ts --type=tsx -C3 'photoId' -g '!**/node_modules/**' -g '!**/_mocks/**'Length of output: 27
Now I'll run a corrected script to search for photoId usage:
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for photoId usage in .ts and .tsx files with context
rg -nP 'photoId' -g '*.ts*' -C3 -g '!node_modules/**' -g '!_mocks/**'
Length of output: 11259
이제 PreviewPlace 컴포넌트의 상세 코드를 검토하여 null 처리 여부를 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# PreviewPlace 컴포넌트의 전체 photos.map 섹션 확인
cat apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx | head -60 | tail -30
Length of output: 879
PreviewPlace 컴포넌트의 타입 정의와 전체 구조를 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# PreviewPlace 컴포넌트의 props 타입 및 전체 구조 확인
cat apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx
Length of output: 2009
이제 place.ts 파일의 PlaceByMap 타입 정의를 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# place.ts 파일에서 PlaceByMap 타입 정의 확인
cat apps/web/app/_apis/schemas/place.ts
Length of output: 1935
PreviewPlace.tsx에서 nullable photoId 처리 필수
photoId가 nullable로 변경되었으나 PreviewPlace.tsx 40줄에서 React 키로 직접 사용되고 있습니다. photo.photoId가 null이면 React는 유효하지 않은 키 경고를 발생시키고 런타임 오류가 발생합니다.
apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx40줄:key={photo.photoId}- null 체크 필수apps/admin/src/app/request/[id]/_api/types.ts4줄:photoId: string타입도 nullable로 업데이트 필요
null인 사진을 필터링하거나, displayOrder 같은 다른 고유 값을 키로 사용하거나, null 체크 로직을 추가해야 합니다.
🤖 Prompt for AI Agents
In apps/web/app/_apis/schemas/place.ts line 21 and related UI, photoId was
changed to nullable but PreviewPlace.tsx
(apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx around line 40)
still uses key={photo.photoId} directly and will break when null; update
PreviewPlace to either filter out photos with null photoId before rendering or
use a safe key (e.g. fallback to a stable alternative like displayOrder or a
composite string using index when photoId is null) and add an explicit
null-check when accessing photo.photoId; additionally update
apps/admin/src/app/request/[id]/_api/types.ts (around line 4) to make photoId
nullable (string | null) to match the schema.
| export const getCategories = async (): Promise<Category[]> => { | ||
| const { data } = await axiosInstance.get(API_PATH.CATEGORY) | ||
| const { data: response } = await axiosInstance.get(API_PATH.CATEGORY) | ||
| const { data } = response | ||
| return CategorySchema.array().parse(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.
🧩 Analysis chain
중첩된 API 응답 구조를 올바르게 처리하고 있습니다.
백엔드 응답 형식 변경에 맞춰 2단계 데이터 추출 로직이 정확하게 구현되었습니다. Zod 스키마를 통한 런타임 검증도 유지되고 있어 타입 안정성이 보장됩니다.
다만, PR 설명에 따르면 이 변경사항이 여러 API 서비스에 영향을 미친다고 하니, 다른 API 서비스 파일들도 동일한 패턴으로 업데이트되었는지 확인해주세요.
다음 스크립트를 실행하여 다른 API 서비스 파일들의 일관성을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Description: 다른 API 서비스 파일들이 동일한 중첩 응답 구조를 처리하는지 확인
# API 서비스 파일들에서 axiosInstance.get/post/put/delete 호출 패턴 검색
echo "=== Checking API service files for response handling pattern ==="
fd -e ts -e tsx --full-path '_apis/services' --exec rg -n -A 3 -B 1 'axiosInstance\.(get|post|put|delete|patch)' {}
echo ""
echo "=== Checking for old pattern (direct data destructuring) ==="
# 이전 패턴 (const { data } = await axiosInstance...)을 직접 사용하는 경우 검색
fd -e ts -e tsx --full-path '_apis/services' --exec rg -n 'const\s*\{\s*data\s*\}\s*=\s*await\s+axiosInstance' {}Length of output: 3516
API 서비스 파일들 간 응답 처리 패턴이 일관되지 않습니다. 즉각적인 수정이 필요합니다.
검증 결과 일부 함수는 새로운 중첩 응답 구조(데이터 2단계 추출)로 업데이트되었으나, 다른 함수들은 여전히 이전 패턴(직접 데이터 추출)을 사용하고 있습니다. 백엔드 응답 형식이 실제로 변경되었다면, 다음 함수들은 런타임에 실패할 것입니다:
수정 필요 위치:
place.ts: addLike (10줄), removeLike (15줄), createNewPlace (113줄)request.ts: getRequests (11줄), getRequestDetail (16줄)event.ts: getEventInfo (11줄), postEventParticipations (19줄), getEventResult (24줄)
이 모든 함수를 category.ts의 getCategories와 동일한 패턴으로 업데이트하세요:
const { data: response } = await axiosInstance.get(...)
const { data } = response
return Schema.parse(data)🤖 Prompt for AI Agents
In apps/web/app/_apis/services/category.ts around lines 5 to 9, getCategories
currently extracts data twice (const { data: response } = await
axiosInstance.get(...); const { data } = response) and parses via
CategorySchema.array().parse(data); update the other listed API functions to
follow this exact response-unwrapping and parsing pattern: for each call first
destructure as { data: response } = await axiosInstance.<method>(...), then
extract const { data } = response, and finally pass that data into the
appropriate Zod schema's parse method (e.g., Schema.parse(data) or
Schema.array().parse(data) as applicable) for these locations — place.ts
(addLike at line 10, removeLike at 15, createNewPlace at 113), request.ts
(getRequests at 11, getRequestDetail at 16), and event.ts (getEventInfo at 11,
postEventParticipations at 19, getEventResult at 24).
| // import { CategoryHandlers } from './categoryHandlers' | ||
| import { PlaceHandlers } from './placeHandlers' | ||
| import { EventHandlers } from './eventHandlers' | ||
| import { LikeHandlers } from './likeHandlers' | ||
| import { RequestHandlers } from './requestHandlers' | ||
|
|
||
| export const handlers = [ | ||
| ...CategoryHandlers, | ||
| // ...CategoryHandlers, |
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.
🧩 Analysis chain
CategoryHandlers 비활성화 확인 필요
CategoryHandlers가 주석 처리되어 있습니다. 카테고리 관련 기능이 여전히 정상적으로 작동하는지 확인이 필요합니다.
다음 스크립트로 카테고리 API 사용처를 확인하여 영향 범위를 검증하세요:
🏁 Script executed:
#!/bin/bash
# Description: 카테고리 관련 API 호출 및 의존성 확인
# 카테고리 API 서비스 호출 확인
echo "=== Category API 사용처 확인 ==="
rg -n --type=ts --type=tsx 'from.*@/_apis/services/category' -A 3
# 카테고리 쿼리 훅 사용 확인
echo -e "\n=== Category Query 사용처 확인 ==="
rg -n --type=ts --type=tsx 'useCategoryQueries' -A 2Length of output: 114
스크립트가 tsx 파일 타입 인식 문제로 실패했습니다. 수정된 명령어로 다시 검증하겠습니다.
🏁 Script executed:
#!/bin/bash
# Description: 카테고리 관련 API 호출 및 의존성 확인 (수정)
# 카테고리 핸들러 사용처 확인
echo "=== CategoryHandlers 사용처 확인 ==="
rg -n 'CategoryHandlers' --type=ts
# 카테고리 API 서비스 호출 확인 (다양한 패턴)
echo -e "\n=== Category API 서비스 사용처 확인 ==="
rg -n 'category' --type=ts -i | grep -E '(import|from|service|api)' | head -20
# categoryHandlers 파일 존재 확인
echo -e "\n=== categoryHandlers 파일 검색 ==="
fd categoryHandlers
# 전체 handlers 파일 내용 확인
echo -e "\n=== handlers 파일 전체 내용 ==="
cat apps/web/app/_mocks/handlers/index.tsLength of output: 3116
categoryHandlers 파일의 내용과 useCategoryQueries 구현을 확인하여 영향 범위를 정확하게 파악하겠습니다.
🏁 Script executed:
#!/bin/bash
# Description: 카테고리 핸들러와 쿼리 훅 상세 분석
# categoryHandlers.ts 파일 내용 확인
echo "=== categoryHandlers.ts 파일 내용 ==="
cat apps/web/app/_mocks/handlers/categoryHandlers.ts
# useCategoryQueries 구현 확인
echo -e "\n=== useCategoryQueries 훅 구현 확인 ==="
fd category -type f | grep -E '(queries|query)' | head -10
# category 쿼리 파일 찾기
echo -e "\n=== category 쿼리 파일 정확한 위치 ==="
find . -name "*category*" -type f | grep -E '(queries|query)' | grep -v node_modules
# category 쿼리 파일 내용
echo -e "\n=== category 쿼리 구현 ==="
cat apps/web/app/_apis/queries/category.ts 2>/dev/null || cat apps/web/app/_apis/queries/category/index.ts 2>/dev/null || echo "File not found in expected locations"Length of output: 1135
getCategories 서비스 구현을 확인하여 실제 API 호출 endpoint를 검증하겠습니다.
🏁 Script executed:
#!/bin/bash
# Description: getCategories 서비스 구현 확인
# category 서비스 파일 확인
echo "=== category 서비스 구현 ==="
cat apps/web/app/_apis/services/category.ts 2>/dev/null || echo "File not found"
# API_PATH 상수 확인 (CATEGORY endpoint)
echo -e "\n=== API_PATH.CATEGORY 정의 확인 ==="
rg -n 'CATEGORY' apps/web/app/_constants/path.ts
# 테스트 파일에서 categoryHandlers 사용 여부 확인
echo -e "\n=== 테스트 파일에서 categoryHandlers 사용 여부 ==="
fd -e test.ts -e spec.ts -e test.tsx -e spec.tsx | xargs rg -l 'CategoryHandlers\|category.*handlers\|Category.*mock' 2>/dev/null || echo "테스트 파일에서 CategoryHandlers 직접 사용 없음"
# handlers 등록 파일 자체가 어디서 사용되는지 확인
echo -e "\n=== handlers 파일(index.ts) 사용처 ==="
rg -n 'from.*_mocks/handlers' --type=ts | head -20Length of output: 687
CategoryHandlers 주석 처리로 인한 카테고리 기능 중단 문제 확인
검증 결과, CategoryHandlers 주석 처리는 실제 문제입니다:
getCategories서비스는GET /categories엔드포인트를 호출합니다CategoryHandlers는 정확히 이 엔드포인트를 모킹합니다- 5개 컴포넌트(
app/page.tsx,places/new/_components/Step/Category/Category.tsx,_components/Categories/Categories.tsx,events/food-slot/page.tsx,categories/[id]/page.tsx)가useCategoryQueries.list()를 통해 카테고리 데이터를 사용합니다
CategoryHandlers의 주석을 제거하고 활성화하세요:
import { CategoryHandlers } from './categoryHandlers'
...
export const handlers = [
...CategoryHandlers,
...PlaceHandlers,
...EventHandlers,
...LikeHandlers,
...RequestHandlers,
]
🤖 Prompt for AI Agents
In apps/web/app/_mocks/handlers/index.ts lines 1-8, CategoryHandlers was
commented out which disables the mocked GET /categories endpoint and breaks
components relying on useCategoryQueries.list(); restore and include
CategoryHandlers in the handlers array by uncommenting its import and adding
...CategoryHandlers as the first spread in the exported handlers array so the
categories mock is registered alongside PlaceHandlers, EventHandlers,
LikeHandlers, and RequestHandlers.
| { | ||
| protocol: 'https', | ||
| hostname: 't1.daumcdn.net', | ||
| pathname: '/**', | ||
| }, | ||
| { | ||
| protocol: 'http', | ||
| hostname: 't1.daumcdn.net', | ||
| pathname: '/**', | ||
| }, |
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.
HTTP 프로토콜 사용은 보안 문제를 야기합니다
t1.daumcdn.net에 대해 HTTP와 HTTPS 두 가지 프로토콜을 모두 허용하고 있습니다. HTTP를 통한 이미지 로딩은 다음과 같은 문제를 발생시킵니다:
- HTTPS 사이트에서 HTTP 리소스를 로드하면 Mixed Content 경고/오류 발생
- 최신 브라우저는 기본적으로 Mixed Content를 차단
- 중간자 공격(MITM)에 취약
HTTPS만 사용하도록 수정하는 것을 권장합니다.
다음 diff를 적용하여 HTTP 패턴을 제거하세요:
{
protocol: 'https',
hostname: 't1.daumcdn.net',
pathname: '/**',
},
-{
- protocol: 'http',
- hostname: 't1.daumcdn.net',
- pathname: '/**',
-},📝 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.
| { | |
| protocol: 'https', | |
| hostname: 't1.daumcdn.net', | |
| pathname: '/**', | |
| }, | |
| { | |
| protocol: 'http', | |
| hostname: 't1.daumcdn.net', | |
| pathname: '/**', | |
| }, | |
| { | |
| protocol: 'https', | |
| hostname: 't1.daumcdn.net', | |
| pathname: '/**', | |
| }, |
🤖 Prompt for AI Agents
In apps/web/next.config.ts around lines 18 to 27, the images config currently
allows both 'https' and 'http' for hostname 't1.daumcdn.net'; remove the HTTP
entry so only the HTTPS pattern remains to prevent mixed-content and MITM risks.
Edit the array to delete the block with protocol: 'http', hostname:
't1.daumcdn.net', pathname: '/**' (or change its protocol to 'https' if
deduplication is needed), leaving only the HTTPS object and ensure the final
config is valid JavaScript/TypeScript.
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: 0
🧹 Nitpick comments (1)
apps/web/app/_apis/services/like.ts (1)
9-15: 중첩된 응답 구조를 올바르게 처리했습니다.좋아요 추가 API에서 새로운 응답 형식을 정확히 반영했습니다. 필요하다면 중간
response변수에 타입 어노테이션을 추가하여 타입 안정성을 강화할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/_apis/services/event.ts(1 hunks)apps/web/app/_apis/services/like.ts(1 hunks)apps/web/app/_apis/services/request.ts(1 hunks)apps/web/app/map/_components/Marker/Marker.tsx(2 hunks)apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/map/_components/Marker/Marker.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/app/_apis/services/event.ts (1)
apps/web/app/_apis/schemas/event.ts (2)
EventSchema(3-14)EventResult(24-24)
apps/web/app/_apis/services/request.ts (2)
apps/web/app/_constants/path.ts (1)
API_PATH(4-43)apps/web/app/_apis/schemas/request.ts (2)
RequestSchema(7-13)RequestDetail(23-23)
apps/web/app/_apis/services/like.ts (1)
apps/web/app/_constants/path.ts (1)
API_PATH(4-43)
🔇 Additional comments (7)
apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx (1)
40-40: nullable photoId 처리를 위한 적절한 fallback 추가
photoId가 nullable로 변경됨에 따라displayOrder를 fallback으로 사용하는 것은 적절한 방어적 코딩입니다.
displayOrder가 항상 정의되어 있고 동일한 장소의 사진 배열 내에서 고유한 값인지 확인해 주세요. React의 key는 안정적이고 고유해야 합니다.apps/web/app/_apis/services/event.ts (3)
10-14: 중첩된 응답 구조를 올바르게 처리했습니다.백엔드 API 응답 형식 변경(
data: { data: ... })을 정확히 반영한 구현입니다.
16-26: 중첩된 응답 구조를 올바르게 처리했습니다.POST 요청에서도 두 단계 구조 분해를 일관되게 적용했습니다.
28-32: 중첩된 응답 구조를 올바르게 처리했습니다.이벤트 결과 조회 API에서도 새로운 응답 형식을 정확히 반영했습니다.
apps/web/app/_apis/services/request.ts (2)
10-14: 중첩된 응답 구조를 올바르게 처리했습니다.요청 목록 조회에서 새로운 API 응답 형식을 정확히 반영했습니다.
16-22: 중첩된 응답 구조를 올바르게 처리했습니다.요청 상세 조회에서도 두 단계 구조 분해를 일관되게 적용했습니다.
apps/web/app/_apis/services/like.ts (1)
17-23: 중첩된 응답 구조를 올바르게 처리했습니다.좋아요 제거 API에서도 두 단계 구조 분해를 일관되게 적용했습니다.
#️⃣연관된 이슈
📝작업 내용
1. API 응답 형식 변경 대응
백엔드 API의 공통 응답 형식이
data: { data: ... }와 같이 한 단계 더 감싸진 구조로 변경되었습니다.이에 따라, 데이터를 처리하는 로직 및 관련 타입 정의를 수정했습니다.
2. 지도 페이지(Map) 기능 개선
사용자가 지도를 이동한 후, 해당 위치의 맛집 목록을 새로고침할 수 있는 '현재 위치에서 재검색' 버튼을 UI에 추가했습니다.
맛집 조회 API의 query key에 지도의 bounds (좌표 범위) 값을 포함시켰습니다.
사용자가 '재검색' 버튼을 클릭하면, 현재 지도의 bounds 값이 업데이트되며, 이로 인해 query key가 변경되어 해당 위치의 맛집 데이터를 새로 요청합니다. (지도 이동 시 자동 갱신이 아님)
3. 맛집 등록 결과 페이지 분리
기존에 맛집 등록 '과정'의 마지막 단계(step)로 처리되던 '성공(success)' 및 '실패(fail)' 화면을 독립된 페이지로 분리했습니다.
이유: '등록 과정'과 '등록 결과'는 기능적 성격이 다르다고 판단했습니다.
기대 효과: 페이지 단위로 책임을 분리함으로써 코드의 유지보수성과 확장성을 향상시켰습니다.
스크린샷 (선택)
💬리뷰 요구사항(선택)
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
개선 사항