Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReact Query가 도입되어 전역 QueryClient와 Devtools가 추가되었습니다. 버스 선택과 지도 이동 로직이 훅으로 분리되어 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant UI as App/UI
participant Hook as useBusSelection
participant Data as buses 데이터
participant Move as moveToLocation
participant Map as Kakao Map
User->>UI: 버스 번호 선택
UI->>Hook: onBusNumberSelect(n)
Hook->>Data: 버스 n 조회 (index = n-1)
alt 좌표 유효
Hook->>Move: moveToLocation(lat, lng)
Move->>Map: 애니메이션 pan/setCenter(500ms)
Hook->>UI: setBubbleStop({lat, lng, name})
else 데이터 없음/무효
Hook->>Hook: console.warn / 무시
end
sequenceDiagram
autonumber
participant Root as index/App Root
participant Provider as QueryClientProvider
participant QC as queryClient
participant Devtools as ReactQueryDevtools
Root->>Provider: 앱을 Provider로 래핑 (queryClient 전달)
Provider->>QC: 쿼리/뮤테이션 기본옵션 적용 (retry, staleTime 등)
Note over Provider,QC: 전역 쿼리 컨텍스트 제공
Root->>Devtools: Devtools 렌더링 (개발 전용)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (16)
src/components/SettingsPanel.tsx (1)
28-40: 대화상자 접근성·포커스 관리 보강 제안role="dialog"/aria-modal은 좋습니다. 다만 포커스 트랩/초점 복귀가 없어 키보드 사용자가 배경으로 이동 가능합니다. 열릴 때 패널 내부 첫 요소로 포커스를 이동시키고, Tab 순환을 패널 내로 제한하며, 닫을 때 트리거 버튼으로 포커스를 돌려주세요. 또한 패널(zIndex: 10000)과 토글 버튼(zIndex: 10000)이 동일해 겹침 이슈가 날 수 있으니 패널을 더 높게 두는 것을 권장합니다.
src/lib/endpoints.ts (1)
1-6: 엔드포인트 맵 구성이 간결하고 명확합니다as const로 리터럴 타입을 고정한 점 좋습니다. 확장성을 위해 base path(예: API_BASE)와 결합하는 헬퍼(예: pathBuilder)를 추후 추가해 두면 환경별 전환이 쉬워집니다.
src/lib/error.ts (2)
17-36: 상태코드 맵 보완(408/502/503 등) 및 기본 문구 통일 제안네트워크/타임아웃 시나리오(408, 502, 503)를 추가하면 UX가 좋아집니다. 또한 서버 메시지 부재 시 공통 fallback(예: “일시적인 오류… 다시 시도…”)로 정렬을 권장합니다.
39-44: AbortError(요청 취소) 별도 문구 처리 제안사용자 취소/탭 전환 등으로 AbortError가 많이 발생합니다. error.name === "AbortError" 또는 DOMException.name 체크 후 “요청이 취소되었습니다.” 같은 무해한 메시지로 구분하세요.
src/hooks/useMapMovement.ts (3)
50-89: 카카오 기본 panTo 활용으로 단순화 가능수동 rAF 애니메이션 대신 kakao.maps.Map.panTo가 제공됩니다. 기존 취소 로직도 불필요해지고 유지보수가 쉬워집니다. 내부에서 애니메이션 처리합니다.
대체 예시:
if (window.map && window.kakao) { try { window.map.panTo(new window.kakao.maps.LatLng(targetLat, targetLng)); } catch { /* ignore */ } }
1-3: Window 전역 보강 타입 선언 필요window.map/__panAnimationId 등 커스텀 필드 사용으로 TS에서 any/에러가 날 수 있습니다. 전역 선언을 추가해 타입 안정성을 확보하세요.
예: src/types/global.d.ts
declare global { interface Window { map?: any; kakao?: any; __panAnimationId?: number; __moveFromRN?: (lat: number, lng: number) => void; __pendingMove?: { lat: number; lng: number } | null; __currentBubbleOverlay?: any; __currentBubbleStopName?: string; ReactNativeWebView?: { postMessage: (msg: string) => void }; } } export {};Also applies to: 55-61, 77-85, 88-89
35-37: 좌표 유효성 검사(범위 클램프) 권장Number 변환만으로는 NaN/범위 밖 값이 유입될 수 있습니다. lat[-90,90], lng[-180,180] 범위를 클램프해 방어하세요.
src/lib/query-client.ts (1)
4-19: 쿼리(onError)도 중앙 처리로 일관성 확보Mutation만 onError가 있고, Query 오류는 콘솔에 그대로 남을 수 있습니다. 동일 포맷으로 처리해 사용자 피드백을 통일하세요.
적용 diff:
export const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 60 * 1000, gcTime: 5 * 60 * 1000, + onError: async (error: unknown) => { + const message = await handleApiError(error); + console.error("Query Error:", message); + }, }, mutations: { onError: async (error: unknown) => { const message = await handleApiError(error); console.error("Mutation Error:", message); }, }, }, });src/App.tsx (5)
389-391: DevTools 조건부 렌더링DEV에서만 렌더링되도록 전환합니다.
적용 diff:
- <ReactQueryDevtools initialIsOpen={false} /> + {import.meta.env.DEV ? ( + <Suspense fallback={null}> + <Devtools initialIsOpen={false} /> + </Suspense> + ) : null}
230-239: Kakao API Key 미설정 시 사용자 메시지 보강키가 비어도 스크립트가 onload될 수 있어 이후 init에서 실패 시 원인 파악이 어렵습니다. 키 미존재 시 사전에 토스트/콘솔 경고를 출력하고 로드를 중단하세요.
263-271: postMessage 보안 가드 보완 여지origin 가드는 좋습니다. 필요 시 data.type 화이트리스트 검사(예: JSON.parse 후 type 체크)를 추가하면 오용 방지가 더 강해집니다.
300-314: 전역 window 확장 프로퍼티 타입 선언 누락 가능성__currentBubbleOverlay/__currentBubbleStopName/map 등에 대한 전역 타입 선언을 추가해 컴파일 안정성을 높여주세요. (useMapMovement.ts 코멘트 참고)
327-350: 설정 버튼과 패널 z-index 충돌 가능성둘 다 zIndex 10000입니다. 패널이 열린 경우 버튼이 위에 올라 겹칠 수 있으니 버튼을 숨기거나 z-index를 조정하세요.
src/hooks/useBusSelection.ts (3)
11-16: 입력 n 검증(정수/범위) 추가 권장음수/0/실수/범위를 벗어난 값에 대해 조기 반환하면 의도를 더 명확히 하고 불필요한 접근을 줄일 수 있습니다.
const handleBusNumberSelect = (n: number) => { try { + if (!Number.isInteger(n) || n < 1 || n > buses.length) { + // eslint-disable-next-line no-console + console.warn(`Invalid bus number: ${n}`); + return; + } const idx = n - 1; const bus = buses[idx];
17-23: 중첩 try/catch 제거로 단순화레이블 생성은 방어적 타입 체크로 충분합니다. 불필요한 예외 삼키기를 제거하세요.
- try { - const dir = bus.direction?.trim() ?? ""; - const label = dir ? `셔틀버스(${dir} 방향)` : "셔틀버스"; - setBubbleStop({ lat: bus.lat, lng: bus.lng, name: label }); - } catch { - /* ignore */ - } + const dir = + typeof bus.direction === "string" ? bus.direction.trim() : ""; + const label = dir ? `셔틀버스(${dir} 방향)` : "셔틀버스"; + setBubbleStop({ lat: bus.lat, lng: bus.lng, name: label });
11-12: 콜백 아이덴티티 안정화(useCallback)로 불필요한 리렌더 방지컴포넌트에서 반환된 함수를 자식에 prop으로 넘기는 경우가 있다면
useCallback으로 안정화하는 것이 안전합니다.- const handleBusNumberSelect = (n: number) => { + const handleBusNumberSelect = useCallback((n: number) => { @@ - }; + }, [setBubbleStop]); return handleBusNumberSelect;파일 상단에 값 import 추가(선행 코멘트의 타입 import와 병행):
import { useCallback } from "react";Also applies to: 34-35
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
package.json(2 hunks)src/App.tsx(3 hunks)src/components/SettingsPanel.tsx(1 hunks)src/hooks/useBusSelection.ts(1 hunks)src/hooks/useMapMovement.ts(1 hunks)src/lib/endpoints.ts(1 hunks)src/lib/error.ts(1 hunks)src/lib/query-client.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/lib/query-client.ts (1)
src/lib/error.ts (1)
handleApiError(9-44)
src/hooks/useBusSelection.ts (2)
src/data/bus.ts (1)
buses(8-39)src/hooks/useMapMovement.ts (1)
moveToLocation(1-89)
src/App.tsx (3)
src/hooks/useBusSelection.ts (1)
useBusSelection(4-35)src/lib/query-client.ts (1)
queryClient(4-19)src/hooks/useMapMovement.ts (1)
moveToLocation(1-89)
🔇 Additional comments (1)
src/hooks/useBusSelection.ts (1)
13-16: 확인: 1‑index 가정은 코드상 일관됩니다. BusStops가 [1,2,3,4,5]로 렌더하여 클릭 시 onBusNumberSelect(n)을 호출하고, useBusSelection에서 idx = n - 1로 buses[idx]를 참조합니다. buses 배열(src/data/bus.ts)도 해당 순서로 정의되어 있어 현재 구현에서는 잘 맞습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/lib/error.ts (2)
10-16: HTTPError 안전 파싱 반영: 굿. 타임아웃/게이트웨이류 메시지 보강을.현재 구현은 안정적입니다. 추가로 사용자 친화 메시지를 넓히려면 Timeout/5xx 변형을 커버하세요.
-import { HTTPError } from "ky"; +import { HTTPError, TimeoutError } from "ky"; export const handleApiError = async (error: unknown): Promise<string> => { + if (error instanceof TimeoutError) { + return "요청 시간이 초과되었습니다."; + } if (error instanceof HTTPError) { let errorData: ApiErrorResponse | undefined; try { errorData = (await error.response.json()) as ApiErrorResponse; } catch { errorData = undefined; } if (errorData?.message) { return errorData.message; } switch (error.response.status) { case 400: return "잘못된 요청입니다."; case 401: return "인증이 필요합니다."; case 403: return "접근 권한이 없습니다."; case 404: return "요청한 리소스를 찾을 수 없습니다."; case 409: return "중복된 데이터입니다."; case 422: return "입력 데이터를 확인해주세요."; case 429: return "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."; + case 408: + return "요청 시간이 초과되었습니다."; + case 502: + case 503: + case 504: + return "서버가 일시적으로 응답하지 않습니다. 잠시 후 다시 시도해주세요."; case 500: return "서버 오류가 발생했습니다."; default: - return "알 수 없는 오류가 발생했습니다."; + // 상태 텍스트가 있으면 마지막 보루로 사용 + return error.response.statusText || "알 수 없는 오류가 발생했습니다."; } }Also applies to: 22-41
44-48: 일반 Error 메시지 직접 노출 최소화표준 Error의 원문 메시지는 사용자에게 과도하게 기술적일 수 있습니다. 네트워크 계열(Offline/Fetch 실패 등)은 사용자용 카피로 치환하는 것이 좋습니다.
- if (error instanceof Error) { - return error.message; - } + if (error instanceof Error) { + // 네트워크 일반 오류 메시지 정규화 + const msg = error.message?.toLowerCase?.() ?? ""; + if (msg.includes("network") || msg.includes("failed to fetch") || msg.includes("load")) { + return "네트워크 연결을 확인해주세요."; + } + return "알 수 없는 오류가 발생했습니다."; + }src/App.tsx (3)
210-236: 스크립트 onload/onerror 덮어쓰기 회피기존 핸들러를 덮어쓸 수 있습니다. addEventListener를 사용해 안전하게 등록하세요.
- if (!window.kakao?.maps?.load) { - script.onload = () => { - window.kakao.maps.load(initMap); - }; - script.onerror = () => { + if (!window.kakao?.maps?.load) { + script.addEventListener("load", () => { + window.kakao.maps.load(initMap); + }, { once: true }); + script.addEventListener("error", () => { try { toast({ title: "지도 로드 실패", description: "Kakao Maps API 스크립트를 불러오는 데 실패했습니다.", variant: "destructive", }); } catch { // fallback to console // eslint-disable-next-line no-console console.error( "Kakao Maps API 스크립트를 로드하는데 실패했습니다." ); } - }; + }, { once: true }); } @@ - script.onload = () => { - window.kakao.maps.load(initMap); - }; + script.addEventListener("load", () => { + window.kakao.maps.load(initMap); + }, { once: true }); - script.onerror = () => { - toast({ - title: "지도 로드 실패", - description: - "Kakao Maps API 스크립트를 불러오는 데 실패했습니다.", - variant: "destructive", - }); - }; + script.addEventListener("error", () => { + toast({ + title: "지도 로드 실패", + description: + "Kakao Maps API 스크립트를 불러오는 데 실패했습니다.", + variant: "destructive", + }); + }, { once: true });Also applies to: 245-257
271-279: message 핸들러가 실동작을 하지 않습니다허용/차단만 하고 payload 처리가 없습니다. 필요 없다면 제거하고, 필요하다면 type 기반 라우팅을 구현하세요.
281-301: 제스처/터치 preventDefault는 접근성·스크롤에 영향gesturestart는 비표준이고 passive: false 터치 캡처는 스크롤을 막을 수 있습니다. 가능하면 CSS touch-action으로 대체하세요.
- document.addEventListener("gesturestart", gestureHandler, { - passive: false, - }); - containerEl?.addEventListener("touchmove", touchMoveHandler, { - passive: false, - }); + // 대안: CSS로 제어 (예: map 컨테이너에 touch-action: none / pan-x pan-y 등) + document.addEventListener("gesturestart", gestureHandler, { passive: false }); + containerEl?.addEventListener("touchmove", touchMoveHandler, { passive: false });검증 제안: 모바일에서 맵 주변 스크롤/핀치 동작이 과도하게 차단되는지 실제 기기에서 확인 필요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
package.json(2 hunks)src/App.tsx(3 hunks)src/hooks/useBusSelection.ts(1 hunks)src/lib/error.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/hooks/useBusSelection.ts
- package.json
🧰 Additional context used
🧬 Code graph analysis (1)
src/App.tsx (4)
src/hooks/useBusSelection.ts (1)
useBusSelection(5-34)src/lib/query-client.ts (1)
queryClient(4-19)src/components/BusStops.tsx (1)
BusStops(11-246)src/hooks/useMapMovement.ts (1)
moveToLocation(1-89)
🔇 Additional comments (2)
src/App.tsx (2)
325-403: React Query Provider 구성: 적절합니다Provider 범위와 Devtools(DEV 한정) 배치는 적절합니다.
중복 Provider가 없는지 한 번만 래핑되는지 확인 부탁드립니다(예: index.tsx/main.tsx).
129-148: 중복 불필요 — 전역 Window 선언이 이미 존재합니다src/types/kakao.d.ts에서 map, __panAnimationId, __moveFromRN, __pendingMove, __onMapReady, ReactNativeWebView, __currentBubbleOverlay, __currentBubbleStopName, kakao 등 전역 속성이 선언되어 있으므로 별도 global.d.ts 추가 불필요합니다.
Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (7)
src/App.tsx (7)
81-86: DOM 조회는 useRef로 교체 권장 (useId + getElementById 지양)Ref 사용이 더 안정적이며 SSR/하이드레이션 및 id 포맷 변화에 둔감합니다. 아래 핵심 변경만 요약합니다.
- const container = document.getElementById(mapId); + const container = containerRef.current; if (!container) { console.error("지도를 표시할 HTML 요소를 찾을 수 없습니다."); return; }추가로 필요한 보조 변경(선택 적용):
// 1) import 보강 import { lazy, Suspense, useCallback, useEffect, useId, useState, type ComponentType, useRef } from "react"; // 2) ref 선언 (컴포넌트 상단) const containerRef = useRef<HTMLDivElement | null>(null); // 3) JSX 컨테이너에 ref 부여 <div id={mapId} ref={containerRef} style={{ height: "70vh", width: "100vw" }} />
162-171: innerHTML 사용 제거: DOM API로 안전하게 이미지 구성정적 문자열이긴 하지만
innerHTML패턴은 XSS 습관화를 유도합니다. 버스정류장 아이콘도createElement를 사용하세요.- busIconDiv.innerHTML = - '<img src="/ic_busstop.svg" alt="Bus Icon" width="48" height="48" />'; + const stopImg = document.createElement("img"); + stopImg.src = `${import.meta.env.BASE_URL}ic_busstop.svg`; + stopImg.alt = "Bus Icon"; + stopImg.width = 48; + stopImg.height = 48; + busIconDiv.appendChild(stopImg);
195-199: 배포 베이스 경로 대응: 정적 asset 경로는 BASE_URL과 결합서브패스 배포 시 루트(
/) 경로는 깨질 수 있습니다. Vite의import.meta.env.BASE_URL을 사용하세요.- img.src = "/ic_busfront.svg"; + img.src = `${import.meta.env.BASE_URL}ic_busfront.svg`;
315-335: 글로벌 브리지 정리 누락 가능성: __moveFromRN도 해제앱 언마운트 후에도
window.__moveFromRN가 남으면 오래된 클로저를 참조할 수 있습니다. 클린업에서 해제하세요.window.__currentBubbleOverlay = undefined; window.__currentBubbleStopName = undefined; + window.__moveFromRN = undefined; window.map = undefined;
277-281: 허용 origin 하드코딩 → 환경변수 기반 구성으로 외부화개발 포트가 바뀌면 코드 수정이 필요합니다. 쉼표 구분 env로 주입하는 형태가 운영 친화적입니다.
- const allowedOrigins = new Set([ - ...(selfOrigin ? [selfOrigin] : []), - "http://localhost:3000", - "http://localhost:5173", - ]); + const extraOrigins = + import.meta.env.VITE_ALLOWED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean) ?? []; + const allowedOrigins = new Set<string>([ + ...(selfOrigin ? [selfOrigin] : []), + ...extraOrigins, + ]);추가:
env.d.ts에 키를 선언하면 타입 안전성을 유지할 수 있습니다.interface ImportMetaEnv { readonly VITE_KAKAO_MAP_API_KEY?: string; readonly VITE_ALLOWED_ORIGINS?: string; }
283-291: messageHandler는 현재 no-op — 필요 전까지 이벤트 리스너 추가 보류 권장필터링만 하고 동작이 없어 비용만 발생합니다. 주석(TODO) 추가 또는 리스너 등록을 출시 전 비활성화하세요.
현재 릴레이 처리 계획(프로토콜/페이로드 포맷)이 있으면 간단히 공유해 주세요. 그에 맞춰 타입과 검증 로직 제안드리겠습니다.
33-39: 언어 코드 유효성 검증(화이트리스트) 추가 제안
localStorage값이 임의 문자열이면 UI가 깨질 수 있습니다.'ko' | 'en'등 허용 목록으로 보정하세요.예시:
const raw = localStorage.getItem("wtb:lang") ?? "ko"; const allowed = new Set(["ko", "en"]); return allowed.has(raw) ? raw : "ko";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/App.tsx(3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: KwonDeaGeun
PR: KwonDeaGeun/WhatTheBus-Web#21
File: src/App.tsx:16-23
Timestamp: 2025-09-22T04:45:54.433Z
Learning: KwonDeaGeun은 TypeScript에서 any 타입 사용을 피하고 더 타입 안전한 코드를 선호한다.
📚 Learning: 2025-09-22T04:45:54.433Z
Learnt from: KwonDeaGeun
PR: KwonDeaGeun/WhatTheBus-Web#21
File: src/App.tsx:16-23
Timestamp: 2025-09-22T04:45:54.433Z
Learning: KwonDeaGeun은 TypeScript에서 any 타입 사용을 피하고 더 타입 안전한 코드를 선호한다.
Applied to files:
src/App.tsx
🧬 Code graph analysis (1)
src/App.tsx (4)
src/hooks/useBusSelection.ts (1)
useBusSelection(5-34)src/lib/query-client.ts (1)
queryClient(4-19)src/components/BusStops.tsx (1)
BusStops(11-246)src/hooks/useMapMovement.ts (1)
moveToLocation(1-89)
🔇 Additional comments (5)
src/App.tsx (5)
20-27: React Query Devtools: 개발 환경 전용 동적 로드 + 명시적 타입, 잘 적용됨
ComponentType<DevtoolsProps>로 유니온 문제를 해소했고, 프로덕션 번들 오염 없이 DevTools를 분리합니다. LGTM.
65-73: Kakao API Key 누락 시 조기 실패 처리 반영 완료토스트 알림과 함께 초기화를 중단하는 로직이 추가되어 UX/디버깅 모두 개선되었습니다. 이전 지적사항을 정확히 해결했습니다.
62-62: 버스 번호 선택 훅 연동, 의도대로 동작에러 내성(try/catch)과 좌표 검증까지 훅에 캡슐화되어 App 단 로직 단순화에 도움 됩니다. 좋습니다.
337-415: QueryClientProvider와 Devtools 배치 적절Provider 최상단 래핑과 Devtools의 Provider 내부 렌더링이 올바릅니다.
100-101: window 전역 확장 타입 정의 필요 — 전역 선언 파일(src/types/global.d.ts) 추가 권장App 전반에서 window.map 등 여러 전역 속성을 사용합니다. 저장소에서 전역 보강 선언을 찾을 수 없어(검색 결과 없음) 아래 최소 예시를 src/types/global.d.ts(또는 src/@types/global.d.ts)에 추가하세요 — 프로젝트 상황에 맞게 확장 바랍니다.
// src/types/global.d.ts export {}; declare global { namespace kakao.maps { class LatLng { constructor(lat: number, lng: number); getLat(): number; getLng(): number; } class Map { constructor(container: HTMLElement, options: { center: LatLng; level: number }); setCenter(latlng: LatLng): void; setMinLevel(lv: number): void; setMaxLevel(lv: number): void; setZoomable(flag: boolean): void; getCenter?(): LatLng | { lat: number; lng: number }; } class CustomOverlay { constructor(opts: { position: LatLng; content: HTMLElement; yAnchor?: number }); setMap(map: Map | null): void; } function load(cb: () => void): void; } interface Window { kakao?: { maps: typeof kakao.maps }; map?: kakao.maps.Map; __pendingMove?: { lat: number; lng: number } | null; __panAnimationId?: number; __currentBubbleOverlay?: { setMap: (m: unknown) => void } | null; __currentBubbleStopName?: string; __onMapReady?: () => void; __moveFromRN?: (lat: number, lng: number) => void; ReactNativeWebView?: { postMessage: (msg: string) => void }; } }
Summary by CodeRabbit