Skip to content
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

[FEAT] 유투브 장소 추출 저장 #56

Merged
merged 18 commits into from
Nov 19, 2024
Merged

Conversation

keemsebin
Copy link
Member

@keemsebin keemsebin commented Nov 19, 2024

관련 이슈

close #50

📑 작업 내용

  • 유투브 장소 추출 저장 기능 구현했습니다.
  • 로그인 기능 추가했습니다.
  • 사용자가 로그인 이후 검색한 데이터를 유지하기 위해 sessionStorage를 사용했습니다.
  • 즐겨 찾기 목록 조회, 검색 목록 조회 UI는 임시 구현이고, 아직 미완성 입니다.
  • 바텀시트를 통해 검색 결과 노출, 유투브 추출 장소 노출, 즐겨찾기 리스트 노출 총 3가지 일을 합니다. 따라서 각각을 type을 통해 바텀시트 사용 유형을 구분하고 퍼널 구현을 통해 바텀시트 내 콘텐츠가 바뀌도록 구현했습니다.
  • (영상 파일이 커 사진으로 대체합니다.)
스크린샷 2024-11-19 오전 1 57 12

💬 리뷰 중점 사항/기타 참고 사항

  • PR 작게 가져가려 했는데 장소 추출 퍼널을 만들면서 관련된 퍼널과 로그인 기능을 추가하다 보니 PR이 방대해졌습니다. 죄송합니다. 🥲
  • MapView 컴포넌트가 너무 많은 일을 하는게 아닌가 하는 생각이 들었습니다. 가능한 함수를 간소화하고 싶은데, 어떻게 하면 좋을 지 의견 부탁드립니다!
  • 추후 장소 노출 부분 컴포넌트화 할 예정입니다.
  • 바텀시트 내 퍼널을 통해 컴포넌트 전환시 좌우로 이동하는 framer-motion 이 있으면 좋을 것 같습니다.

Summary by CodeRabbit

릴리스 노트

  • 신규 기능

    • react-cookie 패키지 추가.
    • BookmarkList, BottomSheetContent, ExtractedPlaces, SearchResult, Login, SearchList 컴포넌트 추가.
    • 맵 관련 데이터를 관리하는 MapDataProvideruseMapData 훅 추가.
  • 변경 사항

    • MapView 컴포넌트의 상태 관리 및 마커 추가 로직 개선.
    • Chip 컴포넌트의 스타일 및 변형 정의 업데이트.
    • ListCard 컴포넌트의 시각적 스타일 수정.
    • NaverMapResponsePlace 타입 구조 변경.
    • CustomMarker 컴포넌트의 카테고리 처리 로직 개선.
    • Map 컴포넌트의 높이 스타일을 '100vh'에서 '100dvh'로 변경.
  • 버그 수정

    • SideMenu 버튼의 포커스 스타일 개선.
  • 문서화

    • 새로운 타입 정의 및 프로퍼티 문서화 추가.

@keemsebin keemsebin added the feat 기능 label Nov 19, 2024
@keemsebin keemsebin self-assigned this Nov 19, 2024
@keemsebin keemsebin requested a review from houony as a code owner November 19, 2024 08:31
Copy link

coderabbitai bot commented Nov 19, 2024

Walkthrough

이 변경 사항은 여러 파일에서 패키지 및 컴포넌트의 추가와 수정이 포함되어 있습니다. 주요 변경 사항으로는 .pnp.cjspackage.json 파일에 새로운 패키지와 버전 업데이트가 포함되었으며, src/App.tsx에서 MapDataProvideruseAuth 훅이 추가되었습니다. 또한, 여러 새로운 컴포넌트가 생성되었고, 기존 컴포넌트의 스타일 및 구조가 수정되었습니다. 이러한 변경들은 애플리케이션의 기능과 사용자 인터페이스를 개선하기 위한 것입니다.

Changes

파일 경로 변경 요약
.pnp.cjs RAW_RUNTIME_STATEreact-cookie, @types/hoist-non-react-statics, cookie, hoist-non-react-statics, universal-cookie 추가 및 업데이트
package.json dependencies"react-cookie": "^7.2.2" 추가
src/App.tsx MapDataProvider 추가 및 useAuth 훅 사용 추가
src/assets/icons/index.tsx Check 아이콘 추가
src/components/common/Chip/Chip.variants.ts ChipVariants의 스타일 및 변형 정의 수정
src/components/common/ListCard/index.tsx <div>의 클래스 이름 수정 (패딩 및 배경색 변경)
src/components/common/SideMenu/index.tsx 버튼에 focus:outline-none 클래스 추가
src/components/features/BookmarkList/BookmarkList.tsx 새로운 BookmarkList 컴포넌트 추가
src/components/features/BottomSheetContent/BottomSheetContent.tsx 새로운 BottomSheetContent 컴포넌트 추가
src/components/features/BottomSheetContent/ExtractedPlaces.tsx 새로운 ExtractedPlaces 컴포넌트 및 타입 추가
src/components/features/BottomSheetContent/SearchResult.tsx 새로운 SearchResult 컴포넌트 및 타입 추가
src/components/features/BottomSheetContent/index.tsx ExtractedPlacesSearchResult 컴포넌트 내보내기 추가
src/components/features/BottomSheetContent/types.ts BottomSheetContentProps 타입 추가
src/components/features/ExtractedPlacesList/ExtractedList.tsx 새로운 ExtractedList 컴포넌트 추가
src/components/features/LinkForm/LinkForm.stories.tsx linkStepNames의 import 경로 수정
src/components/features/LoginModal/Login.tsx 새로운 Login 컴포넌트 추가
src/components/features/LoginModal/Login.types.ts Props 타입 추가
src/components/features/LoginModal/index.tsx Login 컴포넌트 내보내기 추가
src/components/features/SearchList/SearchList.tsx 새로운 SearchList 컴포넌트 추가 및 타입 정의 추가
src/constants/funnelStep.ts 새로운 상수 배열 추가 (linkStepNames, extractedPlaceStepNames, searchPlaceStepNames)
src/constants/linkForm.ts linkStepNames 상수 포함된 파일 삭제
src/contexts/MapContext.tsx MapDataContextMapDataProvider 추가
src/hooks/api/bookmarks/useYoutubeBookmark.ts 새로운 useYoutubeBookmark 훅 추가
src/hooks/auth/useAuth.ts 새로운 useAuth 훅 추가
src/hooks/common/useMapData.tsx 새로운 useMapData 훅 추가
src/hooks/common/useMapStorage.tsx 새로운 useMapStorage 훅 추가
src/lib/axios.ts Axios 인스턴스에 withCredentials: true 추가
src/lib/naver.ts HTTP 요청 메서드(get, post, put, patch, del) 비동기 처리로 변경
src/main.tsx CookiesProvider 추가 및 기존 컴포넌트 래핑
src/pages/Link.tsx linkStepNames의 import 경로 수정
src/pages/MapView.tsx 새로운 상태 관리 및 BottomSheet, BottomSheetContent 추가
src/types/naver.ts NaverMapResponsePlace 타입 구조 변경

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant MapDataProvider
    participant BottomSheetContent
    participant BookmarkList

    User->>App: Open Application
    App->>MapDataProvider: Initialize Map Data
    MapDataProvider->>App: Provide Map Data
    App->>BottomSheetContent: Render Bottom Sheet
    BottomSheetContent->>User: Display Options
    User->>BottomSheetContent: Select Bookmark List
    BottomSheetContent->>BookmarkList: Render Bookmark List
    BookmarkList->>User: Display Bookmarks
Loading

Assessment against linked issues

Objective Addressed Explanation
유투부 장소 추출 저장 ( #50 )

Possibly related PRs

Suggested reviewers

  • houony

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

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

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

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

Documentation and Community

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

Copy link

github-actions bot commented Nov 19, 2024

Update: 2024년 11월 19일 19시 40분 50초
Storybook 미리보기: https://6709628f8a6eec04b29d4f39-pnszjjqdkj.chromatic.com/

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

🧹 Outside diff range and nitpick comments (32)
src/components/features/BookmarkList/BookmarkList.tsx (2)

6-6: 텍스트 국제화(i18n) 처리가 필요합니다.

하드코딩된 한글 텍스트를 i18n 시스템을 통해 관리하는 것이 좋습니다.


7-7: ListCard 내부 텍스트에 대한 설명이 필요합니다.

"리스트 조회"라는 텍스트만으로는 사용자가 이해하기 어려울 수 있습니다. 좀 더 명확한 설명이나 아이콘 추가를 고려해주세요.

src/hooks/common/useMapData.tsx (1)

1-3: 불필요한 공백 라인 제거

import 문 사이의 불필요한 공백 라인을 제거하면 코드가 더 깔끔해질 것 같습니다.

다음과 같이 수정하는 것을 제안합니다:

 import { useContext } from 'react';
-
 import { MapDataContext, MapDataContextValue } from '@/contexts/MapContext';
-
src/components/features/LoginModal/Login.types.ts (3)

2-6: JSDoc 문서를 개선해주세요.

isOpen 프로퍼티의 JSDoc에 기본값과 사용 예시를 추가하면 더 명확할 것 같습니다.

다음과 같이 개선해보세요:

  /**
   * Indicates whether the component is open or visible.
+  * @default false
+  * @example
+  * // To show the modal
+  * <Login isOpen={true} />
   * @type {boolean}
   */

8-12: setIsOpen의 JSDoc이 불완전합니다.

setIsOpen 함수의 매개변수와 동작에 대한 설명이 부족합니다.

다음과 같이 개선해보세요:

  /**
-  * update the open/close state of the component.
-  * @param {boolean}
+  * Updates the visibility state of the login modal.
+  * @param {boolean} value - True to show the modal, false to hide it
+  * @returns {void}
+  * @example
+  * // To close the modal
+  * setIsOpen(false);
   */

1-13: 모달 상태 관리 방식 검토 제안

현재 boolean 값으로 모달의 상태를 관리하고 있는데, 추후 여러 모달이 추가될 경우를 고려하여 enum이나 상수를 사용한 타입 정의를 고려해보시면 좋을 것 같습니다.

예시:

export enum ModalType {
  LOGIN = 'LOGIN',
  SIGNUP = 'SIGNUP',
  NONE = 'NONE'
}

export type Props = {
  modalType: ModalType;
  setModalType: (type: ModalType) => void;
};
src/hooks/api/bookmarks/useYoutubeBookmark.ts (2)

1-5: import 구문 순서를 개선해 주세요.

import 구문의 순서를 다음과 같이 정리하면 좋을 것 같습니다:

-import { useMutation } from '@tanstack/react-query';
-
-import { post } from '@/lib/axios';
-
-import { YoutubeResponse } from '../link/useYoutubePlace';
+import { useMutation } from '@tanstack/react-query';
+import { post } from '@/lib/axios';
+import { YoutubeResponse } from '../link/useYoutubePlace';
🧰 Tools
🪛 eslint

[error] 1-1: There should be no empty line within import group

(import/order)


[error] 3-3: @/lib/axios import should occur before import of @tanstack/react-query

(import/order)


7-16: 에러 처리 로직 추가를 고려해 주세요.

현재 구현에서는 API 호출 실패 시의 에러 처리가 명시적으로 되어있지 않습니다. 사용자 경험 향상을 위해 에러 처리를 추가하는 것이 좋을 것 같습니다.

 export const useYoutubeBookmark = (token: string) => {
   return useMutation({
     mutationFn: (youtubeData: YoutubeResponse) =>
       post(`/api/bookmarks/youtube`, youtubeData, {
         headers: {
           Authorization: `Bearer ${token}`,
         },
       }),
+    onError: (error) => {
+      // 에러 상황에 따른 적절한 처리
+      console.error('북마크 저장 실패:', error);
+    },
   });
 };

전반적으로 구현이 깔끔하고 타입 안전성이 잘 확보되어 있습니다. REST API 엔드포인트 구조와 인증 토큰 처리도 적절합니다.

src/hooks/common/useMapStorage.tsx (2)

1-4: 타입 임포트 위치 개선이 필요합니다.

타입 임포트와 일반 임포트를 구분하여 정리하면 코드의 가독성이 향상될 것 같습니다.

다음과 같이 수정하는 것을 제안합니다:

-import { useCallback } from 'react';
-
-import { BottomSheetType } from '@/contexts/MapContext';
-
+// React imports
+import { useCallback } from 'react';
+
+// Type imports
+import type { BottomSheetType } from '@/contexts/MapContext';

5-16: 스토리지 키를 상수로 분리하는 것이 좋습니다.

스토리지 키가 문자열 리터럴로 하드코딩되어 있어 유지보수성이 떨어집니다.

다음과 같이 수정하는 것을 제안합니다:

+const STORAGE_KEYS = {
+  MAP_DATA: 'mapData',
+  BOTTOM_SHEET_TYPE: 'bottomSheetType',
+} as const;
+
 export const useMapStorage = () => {
   const getStoredMapData = useCallback(() => {
-    const storedData = sessionStorage.getItem('mapData');
-    const storedType = sessionStorage.getItem('bottomSheetType') as BottomSheetType | null;
+    const storedData = sessionStorage.getItem(STORAGE_KEYS.MAP_DATA);
+    const storedType = sessionStorage.getItem(STORAGE_KEYS.BOTTOM_SHEET_TYPE) as BottomSheetType | null;
src/main.tsx (1)

Line range hint 1-7: import 구문 순서를 수정해 주세요

import 구문의 순서가 올바르지 않습니다. 다음과 같이 수정해 주세요:

import React from 'react';
+import { CookiesProvider } from 'react-cookie';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { CookiesProvider } from 'react-cookie';
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
🧰 Tools
🪛 eslint

[error] 1-1: There should be at least one empty line between import groups

(import/order)


[error] 3-3: There should be at least one empty line between import groups

(import/order)


[error] 3-3: react-cookie import should occur before import of @tanstack/react-query

(import/order)

src/components/features/BottomSheetContent/SearchResult.tsx (3)

8-10: 타입 정의를 더 구체적으로 개선해보세요.

Place 타입에서 실제로 사용되는 속성만 Pick하여 더 명확한 타입을 정의하면 좋을 것 같습니다.

예시:

-export type SearchResultProps = {
-  places: Place[];
-};
+export type SearchResultProps = {
+  places: Pick<Place, '실제_사용되는_속성들'>[];
+};

19-20: TODO 주석을 해결해주세요.

현재 '리스트선택' 단계가 미구현 상태입니다. 이 기능의 구현 계획이나 일정을 공유해주시면 좋겠습니다.

구현에 도움이 필요하시다면 제가 도와드릴 수 있습니다. GitHub 이슈를 생성해드릴까요?


12-12: 상수로 분리가 필요한 문자열이 있습니다.

setStep('리스트선택')에서 사용된 문자열을 상수로 분리하면 좋을 것 같습니다.

+const STEP_NAMES = {
+  EXTRACT_PLACE: '추출장소',
+  SELECT_LIST: '리스트선택',
+  LIST: '리스트'
+} as const;

 export const SearchResult = ({ places }: SearchResultProps) => {
   const { Funnel, Step, setStep } = useFunnel(searchPlaceStepNames[0]);
src/components/features/BottomSheetContent/ExtractedPlaces.tsx (3)

8-10: 타입 문서화 개선이 필요합니다.

타입의 용도와 구조를 더 명확하게 이해할 수 있도록 JSDoc 주석을 추가하는 것이 좋겠습니다.

다음과 같이 개선해보세요:

+/**
+ * ExtractedPlaces 컴포넌트의 props 타입
+ * @property {YoutubeResponse} places - YouTube에서 추출된 장소 정보
+ */
 export type ExtractedPlacesProps = {
   places: YoutubeResponse;
 };

20-23: TODO 주석 처리가 필요합니다.

리스트 노출에 대한 TODO 주석이 있습니다. 이 기능의 구현 계획이나 일정을 명확히 해주시기 바랍니다.

이 기능 구현을 위한 이슈를 생성해드릴까요?


17-19: 스텝 이름을 상수로 분리하는 것이 좋겠습니다.

"추출장소"와 "리스트"와 같은 스텝 이름을 상수로 분리하면 유지보수가 더 용이해질 것 같습니다.

다음과 같이 개선해보세요:

+const STEP_NAMES = {
+  EXTRACTED: '추출장소',
+  LIST: '리스트',
+} as const;

 export const ExtractedPlaces = ({ places }: ExtractedPlacesProps) => {
   const { Funnel, Step, setStep } = useFunnel(extractedPlaceStepNames[0]);

   return (
     <>
       <Funnel>
-        <Step name="추출장소">
+        <Step name={STEP_NAMES.EXTRACTED}>
           <ExtractedList places={places} onNext={() => setStep('리스트')} />
         </Step>
-        <Step name="리스트">
+        <Step name={STEP_NAMES.LIST}>
src/hooks/auth/useAuth.ts (2)

1-3: 임포트 구문 정리가 필요합니다

외부 라이브러리와 내부 모듈을 구분하여 임포트 구문을 정리하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

-import { useEffect } from 'react';
-import { useCookies } from 'react-cookie';
-import { useNavigate, useSearchParams } from 'react-router-dom';
+import { useEffect } from 'react';
+
+import { useCookies } from 'react-cookie';
+import { useNavigate, useSearchParams } from 'react-router-dom';
🧰 Tools
🪛 eslint

[error] 2-2: There should be at least one empty line between import groups

(import/order)


5-9: 타입 안정성 개선이 필요합니다

토큰의 타입과 쿠키 설정에 대한 타입 정의가 누락되어 있습니다.

다음과 같은 개선사항을 제안합니다:

interface AuthToken {
  token: string;
}

export const useAuth = () => {
  const navigate = useNavigate();
  const [cookies, setCookie, removeCookie] = useCookies<'token', AuthToken>(['token']);
  // ... 나머지 코드
src/components/features/BottomSheetContent/BottomSheetContent.tsx (2)

22-24: 인증 상태에 따른 UX 개선 필요

인증되지 않은 사용자를 위한 안내 메시지를 더 친절하게 제공하면 좋을 것 같습니다.

-  if (token && type === 'list') {
+  if (type === 'list') {
+    if (!token) {
+      return (
+        <div className="flex flex-col items-center justify-center gap-4 py-8">
+          <p className="text-gray-500">즐겨찾기 기능을 사용하시려면 로그인이 필요합니다.</p>
+          <button className="rounded bg-blue-500 px-4 py-2 text-white">
+            로그인하기
+          </button>
+        </div>
+      );
+    }
     return <BookmarkList />;
   }

26-31: TODO 해결 및 UI/UX 개선 필요

로그인 유도 UI가 임시 구현 상태입니다. 사용자 경험 개선이 필요합니다.

다음 사항들을 개선하면 좋을 것 같습니다:

  1. TODO 주석 제거
  2. 로그인 버튼 추가
  3. 더 명확한 안내 메시지
  4. 적절한 아이콘 추가

이러한 개선사항에 대한 구체적인 구현 코드가 필요하시다면 말씀해 주세요.

src/components/common/SideMenu/index.tsx (1)

Line range hint 22-26: className 구현 개선을 제안드립니다

현재 템플릿 리터럴 내부에 불필요한 공백이 있으며, cn 유틸리티를 더 효과적으로 활용할 수 있습니다.

다음과 같이 개선하는 것을 제안드립니다:

-      className={cn(
-        `w-fit wrap flex flex-col gap-3 ${position === 'left' ? 'left-4' : 'right-4'}         `
-      )}
+      className={cn(
+        'w-fit wrap flex flex-col gap-3',
+        position === 'left' ? 'left-4' : 'right-4'
+      )}
src/lib/naver.ts (1)

Line range hint 4-14: 보안 및 에러 처리 개선 필요

axios 인스턴스 구성에 다음 사항들을 고려해주세요:

  • 에러 로그에 민감한 정보가 노출될 수 있습니다
  • 전역 에러 처리를 위한 인터셉터가 없습니다

다음과 같은 개선을 제안드립니다:

export const naverInstance = axios.create({
  baseURL: '/v1',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    'X-Naver-Client-Id': ENV.NAVER_SEARCH_CLIENT_ID,
    'X-Naver-Client-Secret': ENV.NAVER_SEARCH_CLIENT_SECRET,
  },
  params: {},
});

// 에러 처리를 위한 인터셉터 추가
naverInstance.interceptors.response.use(
  response => response,
  error => {
    // 민감한 정보 제거
    if (error.config) {
      delete error.config.headers['X-Naver-Client-Id'];
      delete error.config.headers['X-Naver-Client-Secret'];
    }
    return Promise.reject(error);
  }
);
src/contexts/MapContext.tsx (1)

18-38: 스토리지 로직 분리를 제안드립니다.

현재 컴포넌트에 직접 구현된 스토리지 로직을 별도의 커스텀 훅으로 분리하면 재사용성과 유지보수성이 향상될 것 같습니다.

다음과 같은 구조를 제안드립니다:

// useMapStorage.ts
export function useMapStorage<T>() {
  const saveMapData = useCallback((state: MapDataState<T>) => {
    try {
      if (state.type !== null) {
        sessionStorage.setItem('bottomSheetType', state.type);
      }
      if (state.data !== null) {
        sessionStorage.setItem('mapData', JSON.stringify(state.data));
      }
    } catch (error) {
      console.error('세션 스토리지 저장 중 오류 발생:', error);
    }
  }, []);

  return { saveMapData };
}

이렇게 하면 스토리지 관련 로직을 재사용하기 쉽고, 테스트하기도 용이해질 것 같습니다.

src/components/features/LinkForm/LinkForm.stories.tsx (1)

Line range hint 19-46: 스토리 구현 개선 제안

현재 스토리 구현이 기본적인 기능은 보여주고 있으나, 다음과 같은 개선사항을 고려해보시기 바랍니다:

  1. 실제와 유사한 데이터를 사용하여 컴포넌트의 동작을 더 잘 보여줄 수 있습니다.
  2. 각 단계별 상태 변화를 더 명확하게 문서화할 수 있습니다.
  3. 에러 상태나 로딩 상태 등 다양한 시나리오를 추가하면 좋을 것 같습니다.

다음과 같은 개선된 구현을 제안드립니다:

 export const Basic: Story = {
+  parameters: {
+    docs: {
+      description: {
+        story: '링크 입력부터 장소 추출까지의 기본 플로우를 보여줍니다.',
+      },
+    },
+  },
   render: () => {
     const { Funnel, Step, setStep } = useFunnel(linkStepNames[0]);
+    const [link, setLink] = useState('https://youtube.com/example');
 
     return (
       <Layout>
         <Funnel>
           <Step name="입력전">
-            <Landing onNext={() => setStep('링크입력')} onHomeClick={() => {}} />
+            <Landing 
+              onNext={() => setStep('링크입력')} 
+              onHomeClick={() => console.log('홈으로 이동')} 
+            />
           </Step>
src/components/features/LoginModal/Login.tsx (1)

13-37: 접근성 개선과 텍스트 외부화가 필요합니다.

  1. 접근성 개선:
    • 아이콘 버튼에 aria-label 추가
    • 모달에 적절한 role과 aria-* 속성 추가
  2. 하드코딩된 텍스트를 외부 상수로 분리

다음과 같이 개선해 보세요:

+const TEXTS = {
+  LOGIN_PROMPT: '저장 기능은 회원 전용이에요.\n지금 로그인하시겠어요?',
+  CONTINUE_AS_GUEST: '비회원으로 계속하기',
+  NAVER_LOGIN: '네이버로 로그인',
+  KAKAO_LOGIN: '카카오로 로그인'
+};

 export const Login = ({ isOpen, setIsOpen }: Props) => {
   // ...
   return (
-    <Modal isOpen={isOpen} onClickOutside={() => setIsOpen(false)}>
+    <Modal 
+      isOpen={isOpen} 
+      onClickOutside={() => setIsOpen(false)}
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="login-title"
+    >
       <div className="flex flex-col gap-6 items-center ">
-        <Body1 className="text-center mt-4">{`저장 기능은 회원 전용이에요.\n지금 로그인하시겠어요?`}</Body1>
+        <Body1 id="login-title" className="text-center mt-4">{TEXTS.LOGIN_PROMPT}</Body1>
         <div className="flex flex-row gap-6 items-center mb-1.5">
           <Icon
             name="naverLogin"
             size={60}
             className="cursor-pointer"
             onClick={() => handleAuth('naver')}
+            aria-label={TEXTS.NAVER_LOGIN}
+            role="button"
           />
           // ... Similar changes for kakaoLogin Icon
         </div>
         <Button 
           variant="gray" 
           size="medium" 
           onClick={() => setIsOpen(!isOpen)}
+          aria-label={TEXTS.CONTINUE_AS_GUEST}
         >
-          비회원으로 계속하기
+          {TEXTS.CONTINUE_AS_GUEST}
         </Button>
       </div>
     </Modal>
   );
 };
src/pages/Link.tsx (2)

Line range hint 1-57: 퍼널 스텝 컴포넌트화 제안

현재 퍼널의 각 스텝이 인라인으로 구현되어 있습니다. 코드의 재사용성과 유지보수성을 높이기 위해 각 스텝을 별도의 컴포넌트로 분리하는 것을 제안드립니다.

다음과 같은 구조로 리팩토링하는 것을 추천드립니다:

+// src/components/features/LinkForm/Steps/InputStep.tsx
+export const InputStep = ({ onNext, onHomeClick, context }) => {
+  return (
+    <LinkInput
+      onNext={onNext}
+      onHomeClick={onHomeClick}
+      context={context}
+    />
+  );
+};

// src/pages/Link.tsx
-      <Step name="링크입력">
-        <LinkInput
-          onNext={() => {
-            handleLinkSubmit();
-            setStep('추출상태');
-          }}
-          onHomeClick={handleHomeClick}
-          context={{
-            state: link.youtubeLink,
-            setState: (link) => {
-              dispatch({ type: 'SET_LINK', payload: { youtubeLink: link } });
-            },
-          }}
-        />
-      </Step>
+      <Step name="링크입력">
+        <InputStep
+          onNext={() => {
+            handleLinkSubmit();
+            setStep('추출상태');
+          }}
+          onHomeClick={handleHomeClick}
+          context={{
+            state: link.youtubeLink,
+            setState: (link) => {
+              dispatch({ type: 'SET_LINK', payload: { youtubeLink: link } });
+            },
+          }}
+        />
+      </Step>
🧰 Tools
🪛 eslint

[error] 2-2: There should be no empty line within import group

(import/order)


[error] 2-2: react-router-dom import should occur after import of @/types/naver

(import/order)


Line range hint 21-24: API 호출 최적화 제안

useYoutubePlace 훅의 결과를 메모이제이션하여 불필요한 API 호출을 방지할 수 있습니다. 또한 URL 인코딩 로직을 훅 내부로 이동하는 것이 좋습니다.

다음과 같은 수정을 제안드립니다:

-  const {
-    refetch: fetchPlaces,
-    data,
-    isLoading,
-  } = useYoutubePlace(encodeURIComponent(link.youtubeLink));
+  const {
+    refetch: fetchPlaces,
+    data,
+    isLoading,
+  } = useYoutubePlace(link.youtubeLink);

그리고 useYoutubePlace 훅 내부에서 URL 인코딩을 처리하도록 수정해주세요.

🧰 Tools
🪛 eslint

[error] 2-2: There should be no empty line within import group

(import/order)


[error] 2-2: react-router-dom import should occur after import of @/types/naver

(import/order)

src/components/features/SearchList/SearchList.tsx (1)

12-15: selectedPlace의 타입 개선이 필요합니다.

null 타입을 포함하는 유니온 타입 대신 옵셔널 타입을 사용하면 더 명확할 것 같습니다.

다음과 같이 변경을 제안드립니다:

-const [selectedPlace, setSelectedPlace] = useState<Place | null>(null);
+const [selectedPlace, setSelectedPlace] = useState<Place | undefined>(undefined);
package.json (1)

34-34: 패키지 버전 고정 권장

현재 ^ 를 사용하여 마이너 버전 업데이트를 허용하고 있습니다. 안정적인 배포를 위해 정확한 버전을 지정하는 것이 좋습니다.

-    "react-cookie": "^7.2.2",
+    "react-cookie": "6.1.1"
src/components/features/ExtractedPlacesList/ExtractedList.tsx (1)

15-15: 타입 정의를 더 구체적으로 개선하세요

YoutubeResponse 타입을 직접 사용하는 대신, 컴포넌트에 필요한 속성만 명시적으로 정의하면 컴포넌트의 의도가 더 명확해질 것 같습니다.

다음과 같이 개선해보세요:

-type Props = { places: YoutubeResponse; onNext: () => void };
+type ExtractedPlace = {
+  id: number;
+  title: string;
+  address: string;
+  category?: {
+    majorCategory: string;
+  };
+};
+
+type Props = {
+  places: {
+    youtuberProfile: string;
+    youtuberName: string;
+    places: ExtractedPlace[];
+  };
+  onNext: () => void;
+};
src/pages/MapView.tsx (1)

4-26: 커스텀 훅 사용 구조 개선 제안

여러 상태 관리 훅들이 사용되고 있어 컴포넌트의 복잡도가 높아질 수 있습니다. 상태 관리 로직을 단일 커스텀 훅으로 통합하는 것을 고려해보세요.

예시 구조:

const useMapViewState = () => {
  const mapData = useMapData<Place[] | YoutubeResponse>();
  const mapStorage = useMapStorage();
  const markers = useMarkers();
  const mapState = useMapState();
  
  return { mapData, mapStorage, markers, mapState };
};
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6e035 and 1edaea4.

⛔ Files ignored due to path filters (8)
  • .yarn/cache/@types-hoist-non-react-statics-npm-3.3.5-98b8d8e47b-2a3b64bf3d.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/cookie-npm-0.7.2-6ea9ee4231-9596e8ccdb.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-fe0889169e.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/react-cookie-npm-7.2.2-61248cf180-22948a42b9.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/universal-cookie-npm-7.2.2-c7f1748d18-214c5cf72b.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/install-state.gz is excluded by !**/.yarn/**, !**/*.gz
  • src/assets/icons/check.svg is excluded by !**/*.svg
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (32)
  • .pnp.cjs (7 hunks)
  • package.json (1 hunks)
  • src/App.tsx (1 hunks)
  • src/assets/icons/index.tsx (2 hunks)
  • src/components/common/Chip/Chip.variants.ts (1 hunks)
  • src/components/common/ListCard/index.tsx (1 hunks)
  • src/components/common/SideMenu/index.tsx (1 hunks)
  • src/components/features/BookmarkList/BookmarkList.tsx (1 hunks)
  • src/components/features/BottomSheetContent/BottomSheetContent.tsx (1 hunks)
  • src/components/features/BottomSheetContent/ExtractedPlaces.tsx (1 hunks)
  • src/components/features/BottomSheetContent/SearchResult.tsx (1 hunks)
  • src/components/features/BottomSheetContent/index.tsx (1 hunks)
  • src/components/features/BottomSheetContent/types.ts (1 hunks)
  • src/components/features/ExtractedPlacesList/ExtractedList.tsx (1 hunks)
  • src/components/features/LinkForm/LinkForm.stories.tsx (1 hunks)
  • src/components/features/LoginModal/Login.tsx (1 hunks)
  • src/components/features/LoginModal/Login.types.ts (1 hunks)
  • src/components/features/LoginModal/index.tsx (1 hunks)
  • src/components/features/SearchList/SearchList.tsx (1 hunks)
  • src/constants/funnelStep.ts (1 hunks)
  • src/constants/linkForm.ts (0 hunks)
  • src/contexts/MapContext.tsx (1 hunks)
  • src/hooks/api/bookmarks/useYoutubeBookmark.ts (1 hunks)
  • src/hooks/auth/useAuth.ts (1 hunks)
  • src/hooks/common/useMapData.tsx (1 hunks)
  • src/hooks/common/useMapStorage.tsx (1 hunks)
  • src/lib/axios.ts (1 hunks)
  • src/lib/naver.ts (1 hunks)
  • src/main.tsx (2 hunks)
  • src/pages/Link.tsx (1 hunks)
  • src/pages/MapView.tsx (2 hunks)
  • src/types/naver.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/constants/linkForm.ts
✅ Files skipped from review due to trivial changes (4)
  • src/components/common/ListCard/index.tsx
  • src/components/features/BottomSheetContent/index.tsx
  • src/components/features/LoginModal/index.tsx
  • src/constants/funnelStep.ts
🧰 Additional context used
🪛 eslint
src/hooks/api/bookmarks/useYoutubeBookmark.ts

[error] 1-1: There should be no empty line within import group

(import/order)


[error] 3-3: @/lib/axios import should occur before import of @tanstack/react-query

(import/order)

src/hooks/auth/useAuth.ts

[error] 2-2: There should be at least one empty line between import groups

(import/order)

src/main.tsx

[error] 3-3: There should be at least one empty line between import groups

(import/order)


[error] 3-3: react-cookie import should occur before import of @tanstack/react-query

(import/order)

🔇 Additional comments (28)
.pnp.cjs (2)

79-79: 의존성 추가가 적절해 보입니다!

react-cookie, universal-cookie 및 관련 패키지들이 쿠키 기반 인증 구현을 위해 적절하게 추가되었습니다. 모든 타입 정의와 필수 의존성이 올바르게 포함되어 있습니다.

Also applies to: 4112-4122, 6363-6369, 8169-8169, 8757-8766, 12070-12094, 13974-13984


12070-12094: 쿠키 보안 설정 검증이 필요합니다.

react-cookieuniversal-cookie를 사용할 때 보안 설정을 확인해주세요:

  • httpOnly 플래그 설정
  • secure 플래그 설정 (HTTPS 환경)
  • sameSite 속성 설정
  • 적절한 만료 시간 설정

Also applies to: 13974-13984

src/components/features/BottomSheetContent/types.ts (1)

1-2: 깔끔한 타입 임포트 구조입니다!

필요한 타입들만 명확하게 임포트되어 있습니다.

src/components/features/LoginModal/Login.types.ts (1)

1-13: 타입 정의가 명확하고 잘 구조화되어 있습니다.

타입스크립트 컨벤션을 잘 따르고 있으며, React 컴포넌트의 props 타입으로 적절합니다.

src/components/common/Chip/Chip.variants.ts (2)

3-4: 기본 스타일 개선이 잘 되었습니다!

flexitems-center 클래스 추가로 컨텐츠 정렬이 더 일관되게 처리될 것 같습니다.


8-10: 높이 변경에 따른 영향도 확인이 필요합니다.

모든 variant의 높이가 1단위씩 감소되었습니다. 이 변경이 다른 컴포넌트에 미치는 영향을 확인해주세요.

✅ Verification successful

높이 변경이 안전한 것으로 확인되었습니다.

검색 결과를 통해 다음과 같은 사항들을 확인했습니다:

  • Chip 컴포넌트는 ExtractedList.tsxSearchList.tsx에서만 medium variant로 사용되고 있습니다.
  • 높이값이 하드코딩된 곳은 발견되지 않았습니다.
  • 모든 사용처에서 variant prop을 통해 스타일을 제어하고 있어, 변경된 높이가 자동으로 적용됩니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Chip 컴포넌트의 사용처를 확인하여 높이 변경의 영향도를 파악합니다.

# Chip 컴포넌트의 사용 패턴을 검색
echo "Checking Chip component usage patterns..."
rg -t tsx -t jsx "Chip.*variant=" -A 2

# 이전 높이값이 하드코딩된 곳이 있는지 확인
echo "Checking for hardcoded height values..."
rg -t tsx -t jsx "h-(5|6|7|8).*Chip" -A 2

Length of output: 322


Script:

#!/bin/bash
# Chip 컴포넌트의 사용처를 확인
echo "Checking Chip component usage patterns..."
rg "Chip.*variant=" -A 2

# 이전 높이값이 하드코딩된 곳이 있는지 확인
echo "Checking for hardcoded height values..."
rg "h-(5|6|7|8).*Chip" -A 2

# Chip 컴포넌트가 import된 파일들 확인
echo "Checking files importing Chip component..."
rg "import.*Chip.*from"

Length of output: 1504

src/types/naver.ts (2)

2-6: 타입 구조가 개선되었습니다!

중첩된 구조를 평탄화하고 배열 표기법을 간소화한 것이 좋은 변경사항입니다. 코드의 가독성과 유지보수성이 향상되었습니다.


14-14: 유니온 타입 순서 변경의 영향을 확인해주세요.

Category | string 순서로 변경된 것이 의도적인 변경인지 확인이 필요합니다. 이는 타입 추론에 영향을 미칠 수 있으며, 기존 코드에서 타입 좁히기(type narrowing)를 사용하는 부분이 있다면 확인이 필요합니다.

src/App.tsx (2)

13-15: 컴포넌트 구조가 적절히 구성되었습니다

Layout 컴포넌트와 Outlet이 Provider들로 적절히 래핑되어 있어, 하위 컴포넌트들이 필요한 컨텍스트에 접근할 수 있습니다.


11-17: Context Provider 순서 검증 필요

MapDataProvider와 MarkerProvider의 중첩 순서가 데이터 흐름에 적절한지 확인이 필요합니다.

✅ Verification successful

Context Provider 순서가 적절합니다

MapView 컴포넌트에서의 사용 패턴을 분석한 결과, 현재의 Provider 중첩 순서가 적절합니다:

  • MapDataProvider는 전반적인 앱의 상태(데이터와 타입)를 관리하며, MarkerProvider는 이를 기반으로 마커 관련 기능을 제공합니다.
  • MapView에서 useMapDatauseMarkers를 함께 사용할 때, MapData의 상태 변경에 따라 Marker를 조작하는 흐름이 자연스럽게 이루어집니다.
  • 두 Context 간에 직접적인 의존성이 없어 현재의 중첩 순서가 데이터 흐름에 문제를 일으키지 않습니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Provider 의존성 관계 확인

# MapDataProvider 내부에서 MarkerProvider의 데이터를 참조하는지 확인
ast-grep --pattern 'const $_ = useContext(MarkerContext)' src/contexts/MapContext.tsx

# MarkerProvider 내부에서 MapDataProvider의 데이터를 참조하는지 확인
ast-grep --pattern 'const $_ = useContext(MapContext)' src/contexts/MarkerContext.tsx

Length of output: 175


Script:

#!/bin/bash
# Let's first check if these context files exist and examine their content
fd "MapContext.tsx|MarkerContext.tsx" src/

# If files exist, let's examine their content
fd "MapContext.tsx|MarkerContext.tsx" src/ -x cat {}

# Let's also search for any imports between these contexts
rg "import.*MarkerContext" -A 2
rg "import.*MapContext" -A 2

Length of output: 3152


Script:

#!/bin/bash
# Let's check where these contexts are actually used together
rg "useContext.*MarkerContext|useMarkers" -A 2
rg "useContext.*MapContext|useMapData" -A 2

# Let's also check if there are any components using both contexts
fd ".tsx" src/ -x grep -l "useMarkers\|MarkerContext" {} | xargs grep -l "useMapData\|MapContext"

Length of output: 2761

src/lib/axios.ts (1)

8-8: 백엔드 CORS 설정 확인이 필요합니다.

withCredentials: true 설정을 추가하셨는데, 백엔드에서도 이에 맞는 CORS 설정이 필요합니다. 다음 사항들을 확인해주세요:

  • Access-Control-Allow-Credentials: true 헤더 설정
  • Access-Control-Allow-Origin이 와일드카드(*)가 아닌 구체적인 도메인으로 설정
src/main.tsx (1)

26-30: Provider 구조가 적절하게 구현되었습니다

CookiesProvider를 최상위에 배치하고 QueryClientProvider와 RouterProvider를 그 아래에 중첩한 구조가 올바릅니다. 이를 통해 애플리케이션 전체에서 쿠키 관리 기능을 사용할 수 있습니다.

쿠키 관리 기능의 사용 여부를 확인하기 위해 다음 스크립트를 실행합니다:

✅ Verification successful

Provider 구조가 적절하게 구현되어 있으며, 쿠키 기능이 올바르게 활용되고 있습니다

CookiesProvider를 통한 쿠키 관리 기능이 다음과 같이 효과적으로 사용되고 있음을 확인했습니다:

  • useAuth 훅에서 useCookies를 활용하여 토큰 관리
  • App, ExtractedList, BottomSheetContent 등의 컴포넌트에서 인증 토큰 활용
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 쿠키 관리 기능의 사용 여부 확인

# useEffect나 useCookies 훅의 사용 검색
rg "useCookies|setCookie|getCookie" 

Length of output: 461


Script:

#!/bin/bash
# useAuth 훅의 전체 구현 확인
ast-grep --pattern 'export function useAuth() {
  $$$
}'

# 쿠키를 사용하는 컴포넌트 검색
rg -B2 -A2 "useAuth"

Length of output: 2736

src/components/features/BottomSheetContent/SearchResult.tsx (1)

1-6: 깔끔한 import 구조입니다!

내부/외부 의존성이 명확하게 구분되어 있고, 필요한 컴포넌트들이 적절히 임포트되어 있습니다.

src/components/features/BottomSheetContent/ExtractedPlaces.tsx (1)

1-6: 임포트 구조가 깔끔합니다!

내부/외부 임포트가 잘 구분되어 있고, 필요한 의존성만 포함되어 있습니다.

src/components/features/BottomSheetContent/BottomSheetContent.tsx (2)

1-10: LGTM - 잘 구성된 import 구조입니다!

외부/내부 임포트가 잘 구분되어 있고, 타입 정의가 명확합니다.


11-13: 타입 안전성이 잘 보장되어 있습니다.

BottomSheetContentProps 타입과 useAuth 훅의 사용이 적절합니다.

✅ Verification successful

타입 정의가 명확하고 안전하게 구현되어 있습니다

BottomSheetContentProps의 타입 정의가 적절하며, 관련된 PlaceYoutubeResponse 타입도 잘 구조화되어 있습니다. 특히:

  • type 필드가 명확한 유니온 타입으로 정의되어 있어 타입 안전성이 보장됩니다
  • data 필드의 타입이 Place[]YoutubeResponse 타입을 적절히 활용하고 있습니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify BottomSheetContentProps type definition
ast-grep --pattern 'type BottomSheetContentProps = {
  $$$
}'

Length of output: 421


Script:

#!/bin/bash
# Check Place type definition
ast-grep --pattern 'type Place = {
  $$$
}'

# Check YoutubeResponse type definition
ast-grep --pattern 'type YoutubeResponse = {
  $$$
}'

# Check where these types are imported
rg -l "import.*(?:Place|YoutubeResponse).*from"

Length of output: 1710

src/lib/naver.ts (1)

16-38: 타입 안전성 검증 필요

제네릭 타입 T의 사용이 올바른지 확인이 필요합니다.

✅ Verification successful

Let me check the type definitions and implementations to ensure type safety.


제네릭 타입 사용이 적절하게 구현되어 있음을 확인했습니다

HTTP 메서드의 제네릭 타입 T가 다음과 같이 올바르게 사용되고 있습니다:

  • NaverMapResponseYoutubeResponse 타입이 명확하게 정의되어 있음
  • API 호출 시 해당 타입들이 제네릭 파라미터로 정확하게 전달됨
  • 반환 타입이 응답 데이터 구조와 일치함
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of HTTP methods and their type parameters

# Find all usages of these HTTP methods
echo "Checking usage of HTTP methods..."
rg "import.*from.*['\"].*naver['\"]" -l | xargs rg "(get|post|put|patch|del)<.*>\("

Length of output: 410


Script:

#!/bin/bash
# Check type definitions for NaverMapResponse and YoutubeResponse
echo "Checking type definitions..."
rg "type (NaverMapResponse|YoutubeResponse)" -A 5

# Check the actual implementation of these interfaces
echo "Checking interface implementations..."
ast-grep --pattern 'interface $_ {
  $$$
}'

Length of output: 861

src/contexts/MapContext.tsx (1)

3-13: 타입 정의가 명확하고 잘 구성되어 있습니다!

제네릭을 활용한 타입 설계가 유연하며, null 처리도 적절히 되어 있습니다.

src/components/features/LinkForm/LinkForm.stories.tsx (1)

5-5: 상수 파일 재구성의 영향 확인 필요

상수 파일이 재구성되면서 linkStepNamesfunnelStep.ts로 이동되었습니다. 이 변경이 다른 컴포넌트에 미치는 영향을 확인해야 합니다.

✅ Verification successful

Let me verify the constant relocation by checking the actual content and usage of these constants.


상수 파일 재구성이 정상적으로 완료됨

linkStepNames 상수가 funnelStep.ts로 성공적으로 이전되었으며, 모든 참조가 새로운 경로로 올바르게 업데이트되었습니다. 이전 경로(@/constants/linkForm)를 사용하는 파일이 없어 추가 수정이 필요하지 않습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the impact of constant file reorganization

# Check for any remaining imports from the old path
rg "from ['|\"]@/constants/linkForm['|\"]"

# Check all imports from the new path
rg "from ['|\"]@/constants/funnelStep['|\"]"

Length of output: 536


Script:

#!/bin/bash
# Check the content of funnelStep.ts
fd "funnelStep.ts" --exec cat {}

# Check the usage of linkStepNames in the files
rg "linkStepNames" -A 2

Length of output: 1472

src/components/features/LoginModal/Login.tsx (2)

1-8: 코드 구조가 잘 정리되어 있습니다!

공통 컴포넌트와 타입을 분리하여 관리하는 구조가 깔끔합니다.


11-11: OAuth2 URL 구성의 보안 검증이 필요합니다.

환경 변수로 설정된 API 기본 URL이 올바르게 구성되어 있는지 확인이 필요합니다.

✅ Verification successful

OAuth2 URL 구성이 안전하게 구현되어 있습니다.

  • OAuth2 인증 URL이 환경 변수를 통해 적절히 구성되어 있습니다.
  • 인증 제공자가 'kakao'와 'naver'로 제한되어 있어 URL 주입 공격 위험이 없습니다.
  • ENV.API_BASE_URL이 Vite 환경 변수를 통해 안전하게 관리되고 있습니다.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the OAuth2 URL construction and environment variable usage

# Check for proper URL construction in the codebase
rg -l "oauth2/authorization" --type ts --type tsx

# Check environment variable definition
fd -e ts -e js "env" -x cat {} \; | rg "API_BASE_URL"

Length of output: 254


Script:

#!/bin/bash
# Check for proper URL construction in the codebase with correct file type
rg -l "oauth2/authorization" --type typescript --type-add 'typescript:*.{ts,tsx}'

# Check environment variable usage in Vite configuration
fd -e ts -e js -e cjs vite.config -x cat {} \;

# Check for environment variable validation or sanitization
rg "VITE_API_BASE_URL" -A 3 --type typescript --type-add 'typescript:*.{ts,tsx}'

Length of output: 1041


Script:

#!/bin/bash
# Check the Login component implementation for URL validation
cat src/components/features/LoginModal/Login.tsx

# Check environment variable validation
cat src/lib/env.ts

# Look for any URL sanitization utilities
rg -g '!node_modules' -g '!dist' "validateUrl|sanitizeUrl|isValidUrl" --type typescript --type-add 'typescript:*.{ts,tsx}'

Length of output: 1859

src/pages/Link.tsx (1)

5-5: 상수 파일 위치 변경 확인 필요

linkStepNames 상수의 import 경로가 변경되었습니다. 새로운 위치인 funnelStep.ts에서 올바르게 export되고 있는지 확인이 필요합니다.

✅ Verification successful

상수가 정상적으로 export되어 있음을 확인했습니다

@/constants/funnelStep.ts에서 linkStepNames 상수가 올바르게 export되어 있으며, 한글로 정의된 단계명들('입력전', '링크입력', '추출상태')을 포함하고 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the constant export in the new location
ast-grep --pattern 'export const linkStepNames'

Length of output: 142

src/assets/icons/index.tsx (2)

7-7: 새로운 Check 아이콘 임포트가 올바르게 추가되었습니다.

기존 임포트 패턴을 잘 따르고 있으며 알파벳 순서도 올바르게 유지되었습니다.


60-60: Icons 객체에 check 속성이 적절하게 추가되었습니다.

기존 네이밍 컨벤션을 잘 따르고 있으며, 알파벳 순서도 올바르게 유지되었습니다.

src/components/features/SearchList/SearchList.tsx (2)

1-11: 깔끔한 import 구조와 타입 정의입니다!

컴포넌트들이 논리적으로 잘 구성되어 있고, Props 타입이 명확하게 정의되어 있습니다.


16-27: 에러 처리와 구현 완성이 필요합니다.

  1. handleSave 함수에서 사용자에게 피드백을 제공하면 좋을 것 같습니다.
  2. TODO 주석이 있는데 구체적인 작업 내용을 명시하면 좋겠습니다.

에러 처리 구현을 도와드릴까요? 다음과 같은 개선사항을 제안드립니다:

 const handleSave = () => {
   if (!selectedPlace) {
+    // 사용자에게 장소를 선택하라는 알림 표시
+    toast.warning('장소를 선택해주세요');
     setIsOpen(true);
     return;
   }
   onNext();
 };
package.json (1)

34-34: ⚠️ Potential issue

버전 번호 확인이 필요합니다

react-cookie의 버전이 7.2.2로 지정되어 있는데, 이는 현재 npm에서 제공하는 최신 안정 버전(6.1.1)보다 높습니다. 이것이 의도된 것인지 확인이 필요합니다.

src/components/features/ExtractedPlacesList/ExtractedList.tsx (1)

16-21: 훅 사용이 깔끔하게 구현되었습니다!

상태 관리와 커스텀 훅의 사용이 적절하게 이루어졌습니다.

Comment on lines +3 to +10
export const BookmarkList = () => {
return (
<div>
나의 핀디 리스트
<ListCard>리스트 조회</ListCard>
</div>
);
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

컴포넌트 타입 정의 및 구조 개선이 필요합니다.

컴포넌트의 기본 구조를 개선하기 위해 다음 사항들을 고려해주세요:

  • TypeScript 타입 정의 추가
  • 시맨틱 HTML 요소 사용
  • 접근성 개선
  • 스타일링 구조화

다음과 같이 수정하는 것을 제안드립니다:

+import styled from '@emotion/styled';
 import { ListCard } from '@/components/common/ListCard';

-export const BookmarkList = () => {
+interface BookmarkListProps {
+  className?: string;
+}
+
+export const BookmarkList = ({ className }: BookmarkListProps) => {
   return (
-    <div>
-      나의 핀디 리스트
+    <StyledSection className={className}>
+      <h2>나의 핀디 리스트</h2>
       <ListCard>리스트 조회</ListCard>
-    </div>
+    </StyledSection>
   );
 };
+
+const StyledSection = styled.section`
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+
+  h2 {
+    font-size: 18px;
+    font-weight: 600;
+  }
+`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const BookmarkList = () => {
return (
<div>
나의 핀디 리스트
<ListCard>리스트 조회</ListCard>
</div>
);
};
import styled from '@emotion/styled';
import { ListCard } from '@/components/common/ListCard';
interface BookmarkListProps {
className?: string;
}
export const BookmarkList = ({ className }: BookmarkListProps) => {
return (
<StyledSection className={className}>
<h2>나의 핀디 리스트</h2>
<ListCard>리스트 조회</ListCard>
</StyledSection>
);
};
const StyledSection = styled.section`
display: flex;
flex-direction: column;
gap: 16px;
h2 {
font-size: 18px;
font-weight: 600;
}
`;

💡 Codebase verification

인증 체크가 필요합니다

BottomSheetContent 컴포넌트에서 token을 확인하고는 있지만, BookmarkList 컴포넌트를 렌더링할 때 인증 상태를 확인하지 않고 있습니다. 다음 사항들을 수정해주세요:

  • BookmarkList 컴포넌트 내부에서 useAuth 훅을 사용하여 인증 상태를 확인하고, 비인증 사용자의 접근을 제한해야 합니다.
  • 비인증 상태일 경우 로그인 유도 UI를 표시하도록 구현이 필요합니다.
🔗 Analysis chain

인증 시스템과의 통합 검증이 필요합니다.

이 컴포넌트는 인증된 사용자만 접근 가능해야 하는 것으로 보입니다. 상위 컴포넌트에서 적절한 인증 체크가 이루어지고 있는지 확인이 필요합니다.


Let me check the implementation of useAuth and how BookmarkList is used in the components that import it.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 인증 관련 코드 확인

# BookmarkList 컴포넌트 사용 위치 확인
rg -l "BookmarkList"

# 인증 관련 로직 확인
ast-grep --pattern 'useAuth'

Length of output: 865


Script:

#!/bin/bash
# Check useAuth implementation
cat src/hooks/auth/useAuth.ts

# Check how BookmarkList is used in other components
rg -A 5 "BookmarkList" src/components/features/BottomSheetContent/BottomSheetContent.tsx
rg -A 5 "BookmarkList" src/components/features/BottomSheetContent/ExtractedPlaces.tsx
rg -A 5 "BookmarkList" src/components/features/BottomSheetContent/SearchResult.tsx

Length of output: 2042

Comment on lines 4 to 7
export type BottomSheetContentProps = {
type: 'search' | 'extract' | 'login' | 'list' | null;
data?: Place[] | YoutubeResponse | null;
};
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

'login' 타입이 실제로 사용되지 않는 것으로 확인되었습니다.

BottomSheetContent 컴포넌트의 구현을 확인한 결과, 'login' 타입이 실제 코드에서 사용되지 않고 있습니다. 타입 정의에서 'login'을 제거하고 PR 설명과 일치하는 3가지 타입('search', 'extract', 'list')만 남겨두는 것이 좋겠습니다.

  • src/components/features/BottomSheetContent/types.ts: 'login' 타입을 제거하여 type: 'search' | 'extract' | 'list' | null로 수정해주세요.
🔗 Analysis chain

'type' 속성값 검증이 필요합니다.

PR 설명에는 바텀 시트가 3가지 타입의 컨텐츠(검색 결과, 유튜브 추출 장소, 즐겨찾기 목록)를 표시한다고 되어있는데, 타입 정의에는 'login'이 추가되어 4가지 타입이 있습니다. 이것이 의도된 것인지 확인이 필요합니다.

다음 스크립트로 'login' 타입의 사용처를 확인해보겠습니다:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of 'login' type in BottomSheetContent component

# Search for BottomSheetContent component usage with 'login' type
rg -A 3 "type=['\"]login" 

# Search for conditional rendering based on 'login' type
ast-grep --pattern 'type === "login"'

Length of output: 69


Script:

#!/bin/bash
# Let's try different approaches to find login type usage

# Search for any mention of 'login' in relation to BottomSheetContent
rg -A 5 "BottomSheetContent.*login|login.*BottomSheetContent"

# Search for type property assignments in tsx/ts files
rg "type: ['\"](search|extract|login|list)" -t ts -t tsx

# Search for switch/case or if conditions handling bottom sheet types
ast-grep --pattern 'switch ($expr) {
  $$$
  case "login":
    $$$
}'

# Also check the component implementation
fd "BottomSheetContent.tsx" --exec cat {}

Length of output: 1187

Comment on lines +5 to +8
export const useMapData = <T,>(): MapDataContextValue<T> => {
const context = useContext(MapDataContext);
return context as MapDataContextValue<T>;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

컨텍스트 유효성 검사가 필요합니다

컨텍스트가 undefined일 때의 처리가 누락되어 있습니다. 컨텍스트가 Provider 외부에서 사용될 경우 런타임 오류가 발생할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

 export const useMapData = <T,>(): MapDataContextValue<T> => {
   const context = useContext(MapDataContext);
+  if (context === null || context === undefined) {
+    throw new Error('useMapData는 MapDataProvider 내부에서 사용되어야 합니다');
+  }
   return context as MapDataContextValue<T>;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useMapData = <T,>(): MapDataContextValue<T> => {
const context = useContext(MapDataContext);
return context as MapDataContextValue<T>;
};
export const useMapData = <T,>(): MapDataContextValue<T> => {
const context = useContext(MapDataContext);
if (context === null || context === undefined) {
throw new Error('useMapData는 MapDataProvider 내부에서 사용되어야 합니다');
}
return context as MapDataContextValue<T>;
};


export const useMapData = <T,>(): MapDataContextValue<T> => {
const context = useContext(MapDataContext);
return context as MapDataContextValue<T>;
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

타입 단언 대신 null 체크가 필요합니다

useMapData 훅에서 context가 null일 수 있는 상황에 대한 처리가 누락되어 있습니다. 다음과 같이 수정하는 것이 좋습니다:

export const useMapData = <T,>(): MapDataContextValue<T> => {
  const context = useContext(MapDataContext);
  if (context === null) {
    throw new Error('useMapData must be used within a MapDataProvider');
  }
  return context;
};
  • src/hooks/common/useMapData.tsx: 타입 단언을 제거하고 null 체크 로직 추가 필요
  • MapDataProvider 컴포넌트 외부에서 훅이 호출될 경우 명확한 에러 메시지 제공
🔗 Analysis chain

타입 캐스팅의 안전성 검토 필요

타입 단언(Type Assertion)을 사용하면 타입 안전성이 보장되지 않을 수 있습니다.

타입 가드를 사용하여 런타임에서 타입을 검증하는 것이 좋습니다.


Let me gather more information about the MapDataContextValue type and its usage.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: MapDataContextValue 타입이 올바르게 구현되어 있는지 확인

# MapDataContextValue 인터페이스 정의 확인
rg -A 10 "interface MapDataContextValue"

# 컨텍스트 Provider 구현 확인
ast-grep --pattern 'const MapDataContext = createContext<$_>($_)'

Length of output: 225


Script:

#!/bin/bash
# MapDataContextValue 인터페이스 정의와 구현 상세 확인
rg -A 10 "MapDataContextValue"

# useMapData 훅의 전체 구현 확인
cat src/hooks/common/useMapData.tsx

# MapContext 파일의 전체 내용 확인
cat src/contexts/MapContext.tsx

Length of output: 3013


export const App = () => {
useAuth();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

useAuth 훅의 에러 처리 추가 필요

useAuth 훅의 호출 결과에 대한 에러 처리가 없습니다. 인증 실패나 네트워크 오류 등의 상황에 대한 처리가 필요합니다.

다음과 같은 에러 처리를 추가하는 것을 제안드립니다:

-  useAuth();
+  const { error } = useAuth();
+  if (error) {
+    return <div>인증 오류가 발생했습니다. 다시 시도해 주세요.</div>;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useAuth();
const { error } = useAuth();
if (error) {
return <div>인증 오류가 발생했습니다. 다시 시도해 주세요.</div>;
}

return (
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-row gap-4 py-2">
<img src={places.youtuberProfile} className="w-12 h-12 rounded-full" />
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 접근성을 개선하세요

프로필 이미지에 대체 텍스트가 없어 스크린 리더 사용자가 이해하기 어려울 수 있습니다.

-<img src={places.youtuberProfile} className="w-12 h-12 rounded-full" />
+<img 
+  src={places.youtuberProfile} 
+  className="w-12 h-12 rounded-full" 
+  alt={`${places.youtuberName}의 프로필 이미지`} 
+/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img src={places.youtuberProfile} className="w-12 h-12 rounded-full" />
<img
src={places.youtuberProfile}
className="w-12 h-12 rounded-full"
alt={`${places.youtuberName}의 프로필 이미지`}
/>

Comment on lines +29 to +56
const handleSave = () => {
if (!token) {
setIsOpen(true);
return;
}

const filteredPlaces = places.places.filter((place) =>
selectedIds.includes(place.id as number)
);

const savePlaces = {
...places,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
places: filteredPlaces.map(({ id, ...rest }) => rest),
};

bookmarkMutate(savePlaces, {
onSuccess: () => {
sessionStorage.removeItem('bottomSheetType');
sessionStorage.removeItem('mapData');
clearMarkers();
onNext();
},
onError: (error) => {
console.error('저장 실패', error);
},
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

저장 로직의 개선이 필요합니다

현재 구현에서 몇 가지 개선이 필요한 부분이 있습니다:

  1. 로딩 상태 처리가 없어 사용자 피드백이 부족합니다
  2. 에러 처리가 console.error로만 되어있어 사용자에게 피드백이 없습니다
  3. sessionStorage 정리 시점에 잠재적인 경쟁 상태가 있을 수 있습니다

다음과 같이 개선해보세요:

+const [isLoading, setIsLoading] = useState(false);
+
 const handleSave = () => {
   if (!token) {
     setIsOpen(true);
     return;
   }
+  setIsLoading(true);
 
   const filteredPlaces = places.places.filter((place) =>
     selectedIds.includes(place.id as number)
   );
 
   const savePlaces = {
     ...places,
     places: filteredPlaces.map(({ id, ...rest }) => rest),
   };
 
   bookmarkMutate(savePlaces, {
     onSuccess: () => {
+      try {
         sessionStorage.removeItem('bottomSheetType');
         sessionStorage.removeItem('mapData');
         clearMarkers();
         onNext();
+      } catch (error) {
+        console.error('세션 스토리지 정리 중 오류 발생:', error);
+      }
     },
     onError: (error) => {
-      console.error('저장 실패', error);
+      toast.error('장소 저장에 실패했습니다. 다시 시도해주세요.');
+      console.error('저장 실패:', error);
     },
+    onSettled: () => {
+      setIsLoading(false);
+    }
   });
 };

그리고 버튼 컴포넌트에 로딩 상태를 추가하세요:

-<Button variant="primary" size="large" onClick={handleSave}>
+<Button 
+  variant="primary" 
+  size="large" 
+  onClick={handleSave}
+  disabled={isLoading}
+>
-  저장하기
+  {isLoading ? '저장 중...' : '저장하기'}
 </Button>

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +41 to +57
useEffect(() => {
if (!state.type && !state.data) {
const { data: storedData, type: storedType } = getStoredMapData();
setState({ data: storedData, type: storedType });

if (storedData && storedType === 'search') handleMarkers(storedData);
if (storedData && storedType === 'extract')
handleMarkers(storedData.places.length > 0 ? storedData.places : []);
}
}, [addMarker, clearMarkers, data]);
}, [handleMarkers, state.type, state.data, setState, getStoredMapData]);

useEffect(() => {
if (extractedData?.places?.length) {
handleMarkers(extractedData.places);
setState({ data: extractedData, type: 'extract' });
}
}, [extractedData, handleMarkers, setState]);
Copy link

@coderabbitai coderabbitai bot Nov 19, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

useEffect 의존성 및 동시성 문제 주의

두 개의 useEffect가 동일한 상태를 변경하고 있어 잠재적인 경쟁 상태가 발생할 수 있습니다.

다음과 같은 개선을 제안드립니다:

useEffect(() => {
  if (extractedData?.places?.length) {
    handleMarkers(extractedData.places);
    setState({ data: extractedData, type: 'extract' });
    return; // 추출된 데이터가 있으면 저장된 데이터를 불러오지 않음
  }

  if (!state.type && !state.data) {
    const { data: storedData, type: storedType } = getStoredMapData();
    setState({ data: storedData, type: storedType });

    if (storedData) {
      if (storedType === 'search') handleMarkers(storedData);
      else if (storedType === 'extract') {
        handleMarkers(storedData.places.length > 0 ? storedData.places : []);
      }
    }
  }
}, [extractedData, state.type, state.data, handleMarkers, setState, getStoredMapData]);

Copy link
Member Author

Choose a reason for hiding this comment

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

잠재적인 경쟁 상태에 대해 알려주세요.

Copy link

Choose a reason for hiding this comment

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

@keemsebin, 두 개의 useEffect 훅이 동일한 상태(state)를 변경하고 있어서 잠재적인 경쟁 상태(race condition)가 발생할 수 있습니다.

첫 번째 useEffect에서는 extractedData를 감시하여, 데이터가 있을 경우 마커를 설정하고 상태를 업데이트합니다:

useEffect(() => {
  if (extractedData?.places?.length) {
    handleMarkers(extractedData.places);
    setState({ data: extractedData, type: 'extract' });
  }
}, [extractedData, handleMarkers, setState]);

두 번째 useEffect에서는 state.typestate.data를 감시하여, 상태가 없을 경우 저장된 데이터를 로드하고 마커를 설정합니다:

useEffect(() => {
  if (!state.type && !state.data) {
    const { data: storedData, type: storedType } = getStoredMapData();
    setState({ data: storedData, type: storedType });

    if (storedData && storedType === 'search') handleMarkers(storedData);
    if (storedData && storedType === 'extract')
      handleMarkers(storedData.places.length > 0 ? storedData.places : []);
  }
}, [handleMarkers, state.type, state.data, setState, getStoredMapData]);

이러한 구조에서는 컴포넌트가 마운트될 때 두 useEffect 훅이 거의 동시에 실행되어 하나의 훅에서 설정한 상태와 마커가 다른 훅에서 덮어쓰는 상황이 발생할 수 있습니다. 이는 의도하지 않은 동작을 유발할 수 있습니다.

예를 들어, extractedData가 있는 경우에도 저장된 데이터가 로드되어 마커가 예상과 다르게 설정될 수 있습니다.

이를 방지하기 위해 두 useEffect를 하나로 합치고 조건문을 통해 로직을 제어하면 이러한 경쟁 상태를 피할 수 있습니다:

useEffect(() => {
  if (extractedData?.places?.length) {
    handleMarkers(extractedData.places);
    setState({ data: extractedData, type: 'extract' });
    return; // 추출된 데이터가 있으면 저장된 데이터를 불러오지 않음
  }

  if (!state.type && !state.data) {
    const { data: storedData, type: storedType } = getStoredMapData();
    setState({ data: storedData, type: storedType });

    if (storedData) {
      if (storedType === 'search') handleMarkers(storedData);
      else if (storedType === 'extract') {
        handleMarkers(storedData.places.length > 0 ? storedData.places : []);
      }
    }
  }
}, [extractedData, state.type, state.data, handleMarkers, setState, getStoredMapData]);

이렇게 하면 extractedData가 있을 경우 저장된 데이터를 로드하지 않고, 경쟁 상태를 예방할 수 있습니다.

Comment on lines 58 to +80

const handleSearch = async () => {
setIsInputDisabled(true);
resetCurrentLocation();
const result = await refetch();
const newData = result?.data?.data.items;
const newData = result?.data?.items;

newData?.forEach((marker: Place) => {
const markerData = {
...marker,
title: marker.title.replace(/<[^>]+>/g, ''),
};
addMarker(markerData);
});
if (newData) {
handleMarkers(newData);
setState({ data: newData, type: 'search' });
}
};

const handleLink = () => {
clearMarkers();
navigate('/link');
};

const handleReset = () => {
setIsInputDisabled(false);
onClickReset();
clearMarkers();
setIsInputDisabled(false);
setState({ data: null, type: 'list' });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리 및 상태 관리 개선 필요

handleSearch 함수에 에러 처리가 누락되어 있으며, 상태 업데이트가 여러 곳에서 발생하고 있습니다.

다음과 같은 개선을 제안드립니다:

const handleSearch = async () => {
  try {
    setIsInputDisabled(true);
    resetCurrentLocation();
    
    const result = await refetch();
    const newData = result?.data?.items;
    
    if (newData) {
      handleMarkers(newData);
      setState({ data: newData, type: 'search' });
    } else {
      throw new Error('검색 결과가 없습니다.');
    }
  } catch (error) {
    console.error('검색 중 오류 발생:', error);
    // 사용자에게 에러 메시지 표시
  } finally {
    setIsInputDisabled(false);
  }
};

Comment on lines +107 to +116
<SideMenu
position="right"
variant="emptyBookMark"
onClick={() => setState((prev) => ({ ...prev, type: 'list' }))}
/>
</SideMenu.Group>
</div>
<BottomSheet isOpen={!!state.type}>
<BottomSheetContent type={state.type} data={state.data} />
</BottomSheet>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

UI 상태 처리 개선 필요

BottomSheet의 표시 여부가 단순히 state.type의 존재 여부에만 의존하고 있으며, 로딩 상태나 에러 상태에 대한 처리가 없습니다.

다음과 같은 개선을 제안드립니다:

<div className="absolute bottom-10 right-4 flex flex-col gap-2 justify-center items-center">
  <SideMenu.Group>
    <SideMenu
      position="right"
      variant="emptyBookMark"
      onClick={() => setState((prev) => ({ ...prev, type: 'list' }))}
      disabled={isLoading} // 로딩 중 비활성화
    />
  </SideMenu.Group>
</div>
<ErrorBoundary fallback={<ErrorMessage />}>
  <BottomSheet 
    isOpen={!!state.type} 
    isLoading={isLoading}
  >
    <BottomSheetContent 
      type={state.type} 
      data={state.data} 
      onError={handleError}
    />
  </BottomSheet>
</ErrorBoundary>

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

🧹 Outside diff range and nitpick comments (1)
src/contexts/MapContext.tsx (1)

3-13: 타입 정의에 문서화가 필요합니다.

각 타입에 대한 JSDoc 문서를 추가하여 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.

다음과 같이 수정을 제안드립니다:

+/**
+ * 바텀 시트의 표시 상태를 정의하는 타입
+ * - extract: 유튜브 위치 추출 모드
+ * - search: 검색 결과 모드
+ * - list: 즐겨찾기 목록 모드
+ */
 export type BottomSheetType = 'extract' | 'search' | 'list';

+/**
+ * 지도 데이터의 상태를 관리하는 타입
+ * @template T - 지도 데이터의 타입
+ */
 export type MapDataState<T> = {
   data: T | null;
   type: BottomSheetType | null;
 };
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1edaea4 and 1e43e16.

📒 Files selected for processing (3)
  • src/components/features/BottomSheetContent/types.ts (1 hunks)
  • src/components/features/ExtractedPlacesList/ExtractedList.tsx (1 hunks)
  • src/contexts/MapContext.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/features/BottomSheetContent/types.ts
  • src/components/features/ExtractedPlacesList/ExtractedList.tsx
🔇 Additional comments (1)
src/contexts/MapContext.tsx (1)

25-35: 🛠️ Refactor suggestion

useEffect 정리(cleanup) 함수가 필요합니다.

isFirstLogin 상태가 변경될 때 이전 효과를 정리하지 않으면 메모리 누수가 발생할 수 있습니다.

다음과 같이 수정을 제안드립니다:

 useEffect(() => {
+  let mounted = true;
   if (isFirstLogin) {
     if (state.type !== null) {
       sessionStorage.setItem('bottomSheetType', state.type);
     }
     if (state.data !== null) {
       sessionStorage.setItem('mapData', JSON.stringify(state.data));
     }
-    setIsFirstLogin(false);
+    if (mounted) {
+      setIsFirstLogin(false);
+    }
   }
+  return () => {
+    mounted = false;
+  };
 }, [state, isFirstLogin]);

Likely invalid or redundant comment.

Comment on lines +18 to +24
export const MapDataProvider = <T,>({ children }: { children: ReactNode }) => {
const [state, setState] = useState<MapDataState<T>>({
data: null,
type: null,
});
const [isFirstLogin, setIsFirstLogin] = useState(true);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

세션 스토리지에서 초기 상태를 로드해야 합니다.

현재 구현에서는 세션 스토리지에 데이터를 저장만 하고 초기화 시점에 데이터를 불러오지 않고 있습니다.

다음과 같이 수정을 제안드립니다:

 export const MapDataProvider = <T,>({ children }: { children: ReactNode }) => {
   const [state, setState] = useState<MapDataState<T>>({
-    data: null,
-    type: null,
+    data: (() => {
+      try {
+        const stored = sessionStorage.getItem('mapData');
+        return stored ? JSON.parse(stored) : null;
+      } catch {
+        return null;
+      }
+    })(),
+    type: sessionStorage.getItem('bottomSheetType') as BottomSheetType | null,
   });
   const [isFirstLogin, setIsFirstLogin] = useState(true);

Committable suggestion skipped: line range outside the PR's diff.

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

🧹 Outside diff range and nitpick comments (6)
src/components/common/Layout/index.tsx (1)

5-5: 스타일링 상수 사용을 고려해 보세요.

현재 하드코딩된 값들(max-w-[30rem], border-x-[1px])을 Tailwind 설정 파일의 상수로 정의하면 일관성 있는 스타일링을 유지하기 더 쉬울 것 같습니다.

-    <div className="max-w-[30rem] h-dvh bg-white shadow-sm py-0 px-2 m-auto border-x-[1px] border-gray-50">
+    <div className="max-w-layout h-dvh bg-white shadow-sm py-0 px-2 m-auto border-x border-gray-50">

tailwind.config.js에 다음과 같이 추가하는 것을 제안드립니다:

module.exports = {
  theme: {
    extend: {
      maxWidth: {
        'layout': '30rem'
      }
    }
  }
}
src/utils/CustomMarker.ts (1)

Line range hint 39-51: 컴포넌트 아키텍처 개선이 필요합니다.

현재 구현에서 발견된 개선이 필요한 부분들입니다:

  1. HTML 문자열을 직접 조작하는 대신 React 컴포넌트로 분리하는 것이 좋습니다.
  2. 인라인 스타일을 styled-components나 CSS 모듈로 분리하면 유지보수가 용이해질 것 같습니다.

이러한 변경을 통해 코드의 재사용성과 유지보수성이 향상될 것입니다. 리팩토링 작업을 위한 별도의 이슈를 생성하는 것은 어떨까요?

src/components/features/LinkForm/ExtractionStatus.tsx (4)

27-30: 널 안전성이 개선되었지만 추가 방어 로직이 필요합니다.

place 객체에 대한 옵셔널 체이닝을 추가한 것은 좋은 개선이지만, undefinednull 상태를 더 명확하게 처리할 수 있습니다.

다음과 같이 개선하는 것을 추천드립니다:

-    if (place?.length > 0) {
+    if (Array.isArray(place) && place.length > 0) {
      return onNext();
    }
    onPrev();

41-43: 사용자 피드백 메시지의 일관성 검토가 필요합니다.

장소 추출 결과에 따른 메시지 분기 처리가 잘 되어있습니다. 하지만 사용자 경험 측면에서 몇 가지 개선이 필요해 보입니다.

다음 사항들을 고려해보시기 바랍니다:

  1. 로딩 상태일 때 예상 소요 시간 표시
  2. 실패 케이스에서 더 구체적인 가이드라인 제공
  3. 메시지 텍스트를 상수로 분리하여 관리
+ const STATUS_MESSAGES = {
+   LOADING: '현재 장소 정보를 추출 중입니다.\n평균 30초 정도 소요됩니다.',
+   SUCCESS: '장소 정보가 성공적으로 추출되었습니다!\n지금 바로 확인하고, 원하는 장소를 방문해보세요!',
+   EMPTY: '추출된 장소가 없어요.\n올바른 장소 정보가 포함된 유튜브 링크를 입력해주세요.'
+ };

76-76: 버튼 텍스트 로직 단순화가 필요합니다.

현재 버튼 텍스트 로직이 다소 복잡하게 구성되어 있습니다. 가독성과 유지보수성을 위해 개선이 필요합니다.

다음과 같이 개선하는 것을 추천드립니다:

- {isLoading ? '확인하러 가기' : place?.length > 0 ? '확인하러 가기' : '뒤로가기'}
+ {getButtonText(isLoading, place)}

+ const getButtonText = (isLoading: boolean, place: Place[] | undefined) => {
+   if (isLoading || (Array.isArray(place) && place.length > 0)) {
+     return '확인하러 가기';
+   }
+   return '뒤로가기';
+ };

Line range hint 1-85: 전반적인 컴포넌트 구조 개선이 필요합니다.

컴포넌트의 전반적인 구조는 잘 잡혀있으나, 몇 가지 아키텍처적 개선사항이 있습니다.

  1. 상태 관리 개선:

    • 로딩 상태와 에러 상태를 더 체계적으로 관리
    • React Query나 SWR 같은 데이터 페칭 라이브러리 도입 고려
  2. 성능 최적화:

    • 불필요한 리렌더링 방지를 위한 메모이제이션 검토
    • 큰 이미지에 대한 로딩 최적화
  3. 접근성 개선:

    • ARIA 레이블 추가
    • 키보드 네비게이션 지원
  4. 테스트 용이성:

    • 비즈니스 로직 분리
    • 테스트 가능한 구조로 리팩토링
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1e43e16 and a2fa192.

📒 Files selected for processing (5)
  • src/components/common/Layout/index.tsx (1 hunks)
  • src/components/common/ListCard/index.tsx (1 hunks)
  • src/components/features/LinkForm/ExtractionStatus.tsx (3 hunks)
  • src/components/features/NaverMap/index.tsx (1 hunks)
  • src/utils/CustomMarker.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/components/features/NaverMap/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/common/ListCard/index.tsx
🔇 Additional comments (2)
src/components/common/Layout/index.tsx (1)

5-5: relative 클래스 제거에 대한 확인이 필요합니다.

relative 클래스가 제거되었는데, 이는 자식 요소들의 절대 위치 지정에 영향을 미칠 수 있습니다. 이 변경이 의도적인 것인지, 그리고 하위 컴포넌트들의 위치 지정에 문제가 없는지 확인해 주시기 바랍니다.

✅ Verification successful

Let me check how these components are using absolute positioning and if they are children of the Layout component.


Let me check how these components are actually used within the Layout component to understand the positioning context.


Let me check the previous state of the Layout component to understand the context of the relative class removal.


relative 클래스 제거는 안전합니다

코드베이스를 분석한 결과, relative 클래스의 제거가 레이아웃에 영향을 미치지 않을 것으로 판단됩니다. 절대 위치(absolute)를 사용하는 컴포넌트들은 각각 자체적인 컨테이너 내에서 위치가 지정되어 있으며, Layout 컴포넌트의 relative 속성에 의존하지 않습니다.

  • BottomSheet는 Portal을 통해 렌더링되어 Layout의 영향을 받지 않습니다
  • LinkForm 관련 컴포넌트들의 절대 위치 지정은 각각의 부모 컨테이너를 기준으로 합니다
  • 기타 공통 컴포넌트들(Tooltip, SearchInput 등)도 자체 컨테이너 내에서 위치가 결정됩니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 절대 위치가 지정된 자식 요소들을 확인합니다.

# position: absolute가 사용된 컴포넌트 검색
rg -l "absolute" src/components/

Length of output: 383


Script:

#!/bin/bash
# Check which components use absolute positioning and are rendered within Layout
rg -C 5 "absolute" src/components/common/BottomSheet/index.tsx src/components/common/Tooltip/index.tsx src/components/common/SearchInput/index.tsx src/components/common/Marker/index.tsx src/components/common/Input/index.tsx src/components/features/LinkForm/ExtractionStatus.tsx src/components/features/LinkForm/LinkInput.tsx src/components/features/LinkForm/Landing.tsx

# Check if these components are used within Layout
rg -l "Layout" src/components/

Length of output: 7935


Script:

#!/bin/bash
# Check the LinkForm component since it contains most of the absolute positioned elements
rg -C 10 "Layout" src/components/features/LinkForm/LinkForm.stories.tsx

# Also check the actual Layout usage in the app
rg -C 10 "<Layout" src/

Length of output: 4959


Script:

#!/bin/bash
# Check git history for the Layout component
git log -p src/components/common/Layout/index.tsx

Length of output: 1543

src/utils/CustomMarker.ts (1)

10-10: 타입 정의가 적절히 개선되었습니다!

Category 타입을 추가하고 categoryName의 타입을 유니온 타입으로 확장한 것이 좋은 접근입니다. 이를 통해 타입 안정성이 향상되었습니다.

Also applies to: 32-32

Comment on lines +35 to +36
const category = typeof categoryName === 'object' ? categoryName.majorCategory : categoryName;
const iconName = parseCategory(category as string) as keyof typeof iconMap;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 가드를 사용하여 타입 안전성을 개선하세요.

현재 구현에서 타입 단언(as string)을 사용하는 것은 잠재적인 런타임 오류를 발생시킬 수 있습니다.

다음과 같이 타입 가드를 사용하여 개선할 것을 제안드립니다:

-  const category = typeof categoryName === 'object' ? categoryName.majorCategory : categoryName;
-  const iconName = parseCategory(category as string) as keyof typeof iconMap;
+  const isCategory = (value: string | Category): value is Category => {
+    return typeof value === 'object' && 'majorCategory' in value;
+  };
+  const category = isCategory(categoryName) ? categoryName.majorCategory : categoryName;
+  const iconName = parseCategory(category) as keyof typeof iconMap;

Committable suggestion skipped: line range outside the PR's diff.

@keemsebin keemsebin merged commit 69b4469 into main Nov 19, 2024
5 checks passed
@keemsebin keemsebin deleted the feat/#50-savelocation branch November 19, 2024 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat 기능
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEAT] 유투부 장소 추출 저장
2 participants