Skip to content

feat: api 초기 세팅(패칭 함수 제외)#21

Merged
KwonDeaGeun merged 4 commits intomainfrom
feat/api-init-setting
Sep 22, 2025
Merged

feat: api 초기 세팅(패칭 함수 제외)#21
KwonDeaGeun merged 4 commits intomainfrom
feat/api-init-setting

Conversation

@KwonDeaGeun
Copy link
Owner

@KwonDeaGeun KwonDeaGeun commented Sep 22, 2025

Summary by CodeRabbit

  • New Features
    • 셔틀버스 번호 선택 시 해당 위치로 부드럽게 애니메이션 이동하고 정보 버블을 표시합니다.
    • 앱 전체에 쿼리 클라이언트를 도입해 데이터 캐싱/오류 처리를 표준화하고(개발 환경에서 개발자용 도구를 로딩), API 오류를 사용자 친화적 메시지로 표시합니다.
    • 설정 버튼을 상시 표시하고 설정 패널을 통합했습니다.
  • Refactor
    • 지도 이동 및 버스 선택 로직을 훅으로 분리하고 레이아웃을 정리했습니다.
  • Chores
    • 데이터 요청/캐싱 관련 의존성(react-query, react-query-devtools, ky) 추가했습니다.
  • Style
    • 설정 패널 코드 포매팅을 정리했습니다(동작 변경 없음).

@KwonDeaGeun KwonDeaGeun self-assigned this Sep 22, 2025
@vercel
Copy link

vercel bot commented Sep 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
what-the-bus-web Ready Ready Preview Comment Sep 22, 2025 5:13am

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

React Query가 도입되어 전역 QueryClient와 Devtools가 추가되었습니다. 버스 선택과 지도 이동 로직이 훅으로 분리되어 useBusSelectionmoveToLocation이 추가되었고, API 엔드포인트 및 에러 처리 유틸(API_ENDPOINTS, handleApiError)과 공유 queryClient가 새로 생겼습니다.

Changes

Cohort / File(s) Summary of changes
Dependencies
package.json
@tanstack/react-query, @tanstack/react-query-devtools, ky 추가.
App integration
src/App.tsx
앱을 QueryClientProvider로 래핑, 개발환경에서 ReactQueryDevtools 조건부 로드, useBusSelectionmoveToLocation으로 버스 선택/지도 이동 로직 재배선, SettingsPanel/UI 레이아웃 조정, Kakao API 키 오류 토스트 처리 추가.
UI formatting
src/components/SettingsPanel.tsx
인라인 스타일 객체 포맷 재정렬(동작 변경 없음).
Hooks: selection and map movement
src/hooks/useBusSelection.ts, src/hooks/useMapMovement.ts
useBusSelection 훅 추가(버스 조회·좌표 검증·지도 이동·버블 업데이트). moveToLocation 추가(카카오 지도 부드러운 팬 애니메이션, 이전 애니메이션 취소).
Lib: endpoints and errors
src/lib/endpoints.ts, src/lib/error.ts
API_ENDPOINTS(예: BUS.SEARCH) 추가. ApiErrorResponse 인터페이스 및 handleApiError 함수 추가(ky HTTPError 파싱 및 상태별 메시지 반환).
Lib: React Query client
src/lib/query-client.ts
공유 queryClient 추가(쿼리/뮤테이션 기본옵션 설정, mutation onError에서 handleApiError 사용해 로그).

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
Loading
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 렌더링 (개발 전용)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

귀 쫑긋한 토끼가 말하네,
"쿼리 심고 맵을 슬쩍-팬,
번호 탁 찍으면 말풍선 퐁—
에러는 속삭이며 지나가고,
오늘도 버니가 길을 닦네." 🥕🗺️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목은 PR의 핵심인 "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/api-init-setting

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5ccd7 and a207f41.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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)도 해당 순서로 정의되어 있어 현재 구현에서는 잘 맞습니다.

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between a207f41 and e429966.

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

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between e429966 and 8e4688f.

📒 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 };
  }
}

@KwonDeaGeun KwonDeaGeun merged commit 05df9ff into main Sep 22, 2025
4 checks passed
@KwonDeaGeun KwonDeaGeun deleted the feat/api-init-setting branch September 22, 2025 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant