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] 검색 결과 저장 #64

Merged
merged 24 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
43cf42b
feat : install jotai
keemsebin Nov 21, 2024
c130ab9
chore : remove provider
keemsebin Nov 21, 2024
94df10c
refactor : replace context with jotai atom
keemsebin Nov 21, 2024
427fdc0
feat: implement useBottomFunnel and integrate it with BottomSheetContent
keemsebin Nov 21, 2024
52fa9ee
fix: add return response to axios instance
keemsebin Nov 21, 2024
8e1402a
fix: separate YouTube thumbnail extraction function
keemsebin Nov 21, 2024
a4f1363
feat: add useNewMarker hook for creating new markers
keemsebin Nov 21, 2024
45f9d82
feat: implement BookmarkDetail component
keemsebin Nov 21, 2024
0253576
feat: implement BookmarkList component
keemsebin Nov 21, 2024
634c011
style: adjust margin values
keemsebin Nov 21, 2024
471ce67
feat: implement functions for sessionStorage
keemsebin Nov 21, 2024
21a6258
feat: Implement ExtractedPlacesList component
keemsebin Nov 21, 2024
5097ca7
feat: Implement SearchResultsList component
keemsebin Nov 21, 2024
71c20ef
feat: Implement BookmarkSelectionList component
keemsebin Nov 21, 2024
276116a
chore: increase timeout for Axios requests
keemsebin Nov 21, 2024
af76cf5
refactor: update Place type to properties optional
keemsebin Nov 21, 2024
4a58a5b
feat: implement useBookMarkList hook for fetching bookmark list
keemsebin Nov 21, 2024
036d042
feat: add useMarkerList hook for fetching marker list
keemsebin Nov 21, 2024
0b3d483
fix: loadSessionData and markers for authenticated users
keemsebin Nov 21, 2024
30f9c62
feat: integrate sessionDataLoader and apply atom state in MapView
keemsebin Nov 21, 2024
d233c08
fix : replace linkForm useFunnel type
keemsebin Nov 21, 2024
69c0b07
chore : add width to chip storybook
keemsebin Nov 23, 2024
5580580
fix : replace sessionStorage clear function
keemsebin Nov 23, 2024
1e48dda
fix: disable button when no bookmark is selected
keemsebin Nov 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file modified .yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"clsx": "^2.1.1",
"framer-motion": "^11.11.9",
"jest": "^29.7.0",
"jotai": "^2.10.3",
"react": "^18.3.1",
"react-cookie": "^7.2.2",
"react-dom": "^18.3.1",
Expand Down
12 changes: 3 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { Outlet } from 'react-router-dom';

import { Layout } from './components/common/Layout';
import { MapDataProvider } from './contexts/MapContext';
import { MarkerProvider } from './contexts/MarkerContext';
import { useAuth } from './hooks/auth/useAuth';

export const App = () => {
useAuth();
return (
<MapDataProvider>
<MarkerProvider>
<Layout>
<Outlet />
</Layout>
</MarkerProvider>
</MapDataProvider>
<Layout>
<Outlet />
</Layout>
);
};
6 changes: 5 additions & 1 deletion src/components/common/Chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const Basic: Story = {
},
},
render: (args) => {
return <Chip variant={args.variant}>{args.children}</Chip>;
return (
<div className="w-20">
<Chip variant={args.variant}>{args.children}</Chip>
</div>
);
},
};
64 changes: 64 additions & 0 deletions src/components/features/BookmarkDetail/BookmarkDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';

import { Chip } from '@/components/common/Chip';
import { Icon } from '@/components/common/Icon';
import { ListCard } from '@/components/common/ListCard';
import { Body1, Body2, Body4 } from '@/components/common/Typography';
import { markersAtom } from '@/contexts/MarkerAtom';
import { useMarkerList } from '@/hooks/api/marker/useMarkerList';
import { useAuth } from '@/hooks/auth/useAuth';

type Props = { bookmarkId: number; onPrev: () => void };
export const BookmarkDetail = ({ bookmarkId, onPrev }: Props) => {
const { token } = useAuth();
const { data } = useMarkerList(bookmarkId, token);
const [, setMarkers] = useAtom(markersAtom);
Comment on lines +14 to +16
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 BookmarkDetail = ({ bookmarkId, onPrev }: Props) => {
   const { token } = useAuth();
-  const { data } = useMarkerList(bookmarkId, token);
+  const { data, isError, isLoading } = useMarkerList(bookmarkId, token);
   const [, setMarkers] = useAtom(markersAtom);
+
+  if (isLoading) return <div>로딩 중...</div>;
+  if (isError) return <div>데이터를 불러오는데 실패했습니다.</div>;
+  if (!token) 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
const { token } = useAuth();
const { data } = useMarkerList(bookmarkId, token);
const [, setMarkers] = useAtom(markersAtom);
const { token } = useAuth();
const { data, isError, isLoading } = useMarkerList(bookmarkId, token);
const [, setMarkers] = useAtom(markersAtom);
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>데이터를 불러오는데 실패했습니다.</div>;
if (!token) return <div>인증이 필요합니다.</div>;


useEffect(() => {
if (data?.markers?.data) {
setMarkers(data.markers.data);
}
}, [data?.markers?.data, setMarkers]);
Comment on lines +18 to +22
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

useEffect 정리(cleanup) 함수 추가 및 조건부 로직 개선이 필요합니다.

컴포넌트가 언마운트될 때 마커를 정리하지 않고 있으며, 데이터 접근 방식이 복잡합니다.

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

 useEffect(() => {
-  if (data?.markers?.data) {
-    setMarkers(data.markers.data);
+  const markers = data?.markers?.data ?? [];
+  setMarkers(markers);
+
+  return () => {
+    setMarkers([]);
   }
-  }, [data?.markers?.data, setMarkers]);
+}, [data?.markers?.data, setMarkers]);
📝 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
useEffect(() => {
if (data?.markers?.data) {
setMarkers(data.markers.data);
}
}, [data?.markers?.data, setMarkers]);
useEffect(() => {
const markers = data?.markers?.data ?? [];
setMarkers(markers);
return () => {
setMarkers([]);
}
}, [data?.markers?.data, setMarkers]);


return (
<div>
<div className="flex flex-row items-center">
<Icon
name="back"
size={30}
onClick={() => {
setMarkers([]);
onPrev();
}}
className="cursor-pointer"
/>
<Body1 className="my-4 mx-3" weight="semibold" onClick={onPrev}>
{data?.bookmarkName}의 핀디 리스트
</Body1>
</div>
<ListCard>
{data?.markers.data.map((item, index) => (
<div key={item.markerId}>
<div
className={`flex flex-row justify-between items-center ${index !== data.markers.data.length - 1 && 'pb-2'}`}
>
<div className="flex flex-col gap-1 py-2">
<div className="flex flex-row gap-3 items-center">
<Body2 className="text-primary">{item.title}</Body2>
{typeof item.category === 'object' && (
<Chip variant="medium">{item.category.majorCategory}</Chip>
)}
</div>
<Body4 className="pt-1 " weight="normal">
{item.address}
</Body4>
</div>
</div>
{index < data.markers.data.length - 1 && <hr className="border-dashed pt-2" />}
</div>
))}
</ListCard>
</div>
Comment on lines +24 to +62
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. className 로직이 복잡합니다.
  3. 데이터 접근 패턴이 반복됩니다.

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

+const CLASSNAMES = {
+  container: 'flex flex-row items-center',
+  backIcon: 'cursor-pointer',
+  title: 'my-4 mx-3',
+  listItem: (isLast: boolean) => `flex flex-row justify-between items-center ${!isLast && 'pb-2'}`,
+  divider: 'border-dashed pt-2',
+};

 return (
   <div>
-    <div className="flex flex-row items-center">
+    <div className={CLASSNAMES.container}>
       <Icon
         name="back"
         size={30}
         onClick={() => {
           setMarkers([]);
           onPrev();
         }}
-        className="cursor-pointer"
+        className={CLASSNAMES.backIcon}
+        role="button"
+        tabIndex={0}
+        onKeyPress={(e) => {
+          if (e.key === 'Enter') {
+            setMarkers([]);
+            onPrev();
+          }
+        }}
       />
-      <Body1 className="my-4 mx-3" weight="semibold" onClick={onPrev}>
+      <Body1 
+        className={CLASSNAMES.title} 
+        weight="semibold" 
+        onClick={onPrev}
+        role="button"
+        tabIndex={0}
+        onKeyPress={(e) => e.key === 'Enter' && onPrev()}
+      >
         {data?.bookmarkName}의 핀디 리스트
       </Body1>
     </div>

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

);
};
1 change: 1 addition & 0 deletions src/components/features/BookmarkDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BookmarkDetail } from './BookmarkDetail';
49 changes: 46 additions & 3 deletions src/components/features/BookmarkList/BookmarkList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
import { Icon } from '@/components/common/Icon';
import { ListCard } from '@/components/common/ListCard';
import { Body1, Body2, Body3 } from '@/components/common/Typography';
import { useBookMarkList } from '@/hooks/api/bookmarks/useBookMarkList';
import { useAuth } from '@/hooks/auth/useAuth';

export const BookmarkList = () => {
type Props = { onNext: (bookmarkId: number) => void };
export const BookmarkList = ({ onNext }: Props) => {
const { token } = useAuth();
const { data } = useBookMarkList(token);
Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리가 누락되었습니다

useBookMarkList 호출 시 발생할 수 있는 에러 상태에 대한 처리가 없습니다. 사용자에게 적절한 에러 메시지를 표시해야 합니다.

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

- const { data } = useBookMarkList(token);
+ const { data, error, isLoading } = useBookMarkList(token);
+
+ if (isLoading) return <LoadingSpinner />;
+ if (error) return <ErrorMessage message="북마크 목록을 불러오는데 실패했습니다" />;

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


const handleBookmarkClick = (bookmarkId: number) => {
onNext(bookmarkId);
};
return (
<div>
나의 핀디 리스트
<ListCard>리스트 조회</ListCard>
<Body1 className="my-4 mx-3" weight="semibold">
나의 핀디 리스트
</Body1>
<ListCard>
{data?.data.map((item, index) => (
keemsebin marked this conversation as resolved.
Show resolved Hide resolved
<>
<div
Comment on lines +21 to +23
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

key prop이 잘못된 위치에 있습니다

Fragment가 아닌 최상위 div 요소에 key를 배치해야 합니다. 현재 구현은 React의 key 경고를 발생시킬 수 있습니다.

다음과 같이 수정해주세요:

- {data?.data.map((item, index) => (
-   <>
-     <div
-       key={item.bookmarkId}
+ {data?.data.map((item, index) => (
+   <div
+     key={item.bookmarkId}

Also applies to: 47-49

key={item.bookmarkId}
onClick={() => handleBookmarkClick(item.bookmarkId)}
className="flex flex-row justify-between items-center cursor-pointer"
>
<div className="flex flex-row gap-4 py-2.5 items-center justify-center">
{item.youtuberProfile ? (
<img
src={item.youtuberProfile}
className="w-12 h-12 rounded-full"
alt={`${item.name}의 프로필 이미지`}
/>
) : (
<Icon name="findy1" className="w-11 h-11" />
)}
<div className="flex flex-col py-1">
<Body2 weight="medium">{item.name}</Body2>
<div className="flex flex-row items-center gap-1">
<Icon name="location" size={15} />
<Body3 className=" text-gray-500">{item.markersCount}</Body3>
</div>
</div>
</div>
</div>
{index < data.data.length - 1 && <hr className="border-dashed pt-2" />}
</>
))}
</ListCard>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/features/BookmarkList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BookmarkList } from './BookmarkList';
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
import { YoutubeResponse } from '@/hooks/api/link/useYoutubePlace';
import { useAuth } from '@/hooks/auth/useAuth';
import { Place } from '@/types/naver';
import { FlowType } from '@/constants/funnelStep';
import { useBottomFunnel } from '@/hooks/common/useBottomFunnel';

import { ExtractedPlaces } from './ExtractedPlaces';
import { SearchResult } from './SearchResult';
import { BottomSheetContentProps } from './types';

import { BookmarkList } from '../BookmarkList/BookmarkList';
import { YoutubeResponse } from '../../../hooks/api/link/useYoutubePlace';
import { Place } from '../../../types/naver';

export const BottomSheetContent = ({ type, data }: BottomSheetContentProps) => {
const { token } = useAuth();

if (type === 'search') {
return <SearchResult places={data as Place[]} />;
}

if (type === 'extract') {
return <ExtractedPlaces places={data as YoutubeResponse} />;
}

if (token && type === 'list') {
return <BookmarkList />;
}

// TODO : 로그인하지 않은 경우 로그인 유도
return (
<div className=" flex items-center justify-center py-8">
<p className="text-gray-500">로그인한 사용자만 즐겨찾기가 가능합니다.</p>
</div>
);
return useBottomFunnel({ type: type as FlowType, data: data as Place[] | YoutubeResponse });
};
keemsebin marked this conversation as resolved.
Show resolved Hide resolved
27 changes: 0 additions & 27 deletions src/components/features/BottomSheetContent/ExtractedPlaces.tsx

This file was deleted.

27 changes: 0 additions & 27 deletions src/components/features/BottomSheetContent/SearchResult.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions src/components/features/BottomSheetContent/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import { useMarkers } from '@/hooks/common/useMarkers';
import { Login } from '../LoginModal';

type Props = { places: YoutubeResponse; onNext: () => void };
export const ExtractedList = ({ places, onNext }: Props) => {
const { clearMarkers } = useMarkers();
export const ExtractedPlacesList = ({ places, onNext }: Props) => {
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);

const { token } = useAuth();
const { clearMarkers } = useMarkers();
const { mutate: bookmarkMutate } = useYoutubeBookmark(token);

const handleToggleSelect = (id: number) => {
Expand All @@ -32,26 +33,20 @@ export const ExtractedList = ({ places, onNext }: Props) => {
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),
places: places.places
.filter((place) => selectedIds.includes(place.id as number))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ id, ...placeData }) => placeData),
};

bookmarkMutate(savePlaces, {
onSuccess: () => {
sessionStorage.removeItem('bottomSheetType');
sessionStorage.removeItem('mapData');
sessionStorage.clear();
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the search results, I can now generate the final response as we have a clear picture of sessionStorage usage in the codebase. The search shows that sessionStorage is consistently used for two specific items: 'mapData' and 'bottomSheetType', and there's already a utility function clearMapStorage that handles clearing these specific items.

세션 스토리지 클리어 시 clearMapStorage() 유틸리티 함수를 사용해주세요.

현재 코드베이스에서는 'mapData'와 'bottomSheetType' 두 가지 항목만 세션 스토리지에서 관리되고 있습니다. sessionStorage.clear() 대신 이미 구현되어 있는 clearMapStorage() 유틸리티 함수를 사용하면 더 안전하게 특정 항목만 제거할 수 있습니다.

  • src/utils/getStorageData.tsclearMapStorage() 함수를 import하여 사용해주세요.
🔗 Analysis chain

sessionStorage 클리어 범위를 검토해주세요.

sessionStorage.clear()는 모든 세션 스토리지 데이터를 제거합니다. 북마크 저장과 관련된 특정 항목만 제거하는 것이 더 안전할 수 있습니다.

다음 스크립트로 sessionStorage 사용을 확인해보세요:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# sessionStorage 사용 패턴 분석
rg "sessionStorage\." -A 2 -B 2

Length of output: 2386

clearMarkers();
onNext();
},
onError: (error) => {
console.error('저장 실패', error);
},
});
};
return (
Expand Down Expand Up @@ -86,13 +81,13 @@ export const ExtractedList = ({ places, onNext }: Props) => {
<Chip variant="medium">{item.category.majorCategory}</Chip>
)}
</div>
<Body4 className="pt-1 " weight="normal">
<Body4 className="pt-1" weight="normal">
{item.address}
</Body4>
</div>
<Icon
name="check"
className="cursor-pointer h-7"
className="cursor-pointer h-7 w-7 flex-shrink-0"
color={selectedIds.includes(item.id as number) ? 'primary' : 'gray'}
onClick={() => handleToggleSelect(item.id as number)}
/>
Expand Down
1 change: 1 addition & 0 deletions src/components/features/ExtractedPlacesList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ExtractedPlacesList } from './ExtractedPlacesList';
6 changes: 3 additions & 3 deletions src/components/features/LinkForm/ExtractionStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Icon } from '@/components/common/Icon';
import { Body1 } from '@/components/common/Typography';
import { SLIDER_ANIMATION, THUMBNAIL_ANIMATION } from '@/constants/motions';
import { Place } from '@/types/naver';
import { extractVideoId } from '@/utils/extractVideoId';

import { LinkFormProps } from './types';

Expand All @@ -20,9 +21,8 @@ export const ExtractionStatus = ({
onNext,
onHomeClick,
}: ExtractProp) => {
const videoId = url.includes('v=') ? url.split('v=')[1].split('&')[0] : url.split('/').pop();
const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;

const videoId = extractVideoId(url);
const thumbnailUrl = videoId ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` : '';
const handleNavigate = () => {
if (place?.length > 0) {
return onNext();
Expand Down
Loading
Loading