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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3684bcf
chore : react-cookie install
keemsebin Nov 18, 2024
8b9507c
style: update SideMenu and ListCard css, add check icon
keemsebin Nov 18, 2024
38f894d
fix: rename functions related to funnelStep
keemsebin Nov 18, 2024
7f46b91
feat : implement YouTubeLink post api hook
keemsebin Nov 19, 2024
6a29b58
feat: implement login modal component
keemsebin Nov 19, 2024
b7dd27f
feat: implement useAuth hook for managing token authentication
keemsebin Nov 19, 2024
c2504e3
feat: create MapDataProvider for managing map data and bottom sheet s…
keemsebin Nov 19, 2024
06f61f3
feat: conditional rendering for BottomSheetContent based on type and …
keemsebin Nov 19, 2024
39c377c
feat: add funnel structure for search and extracted places
keemsebin Nov 19, 2024
150d835
feat: add post extracted places after extraction
keemsebin Nov 19, 2024
6504afe
feat: add useMapStorage hook for managing map data in sessionStorage
keemsebin Nov 19, 2024
34892fb
feat: define BottomSheetContentProps type for managing bottom sheet c…
keemsebin Nov 19, 2024
430dca5
feat: implement SearchList component
keemsebin Nov 19, 2024
1edaea4
feat: implement logic to load session storage data and connect to bot…
keemsebin Nov 19, 2024
4bb3b70
chore : remove login type in BottomSheetType
keemsebin Nov 19, 2024
1e43e16
fix: add alt attribute to youtuber image for accessibility improvement
keemsebin Nov 19, 2024
699df83
fix: add optional chaining for place properties
keemsebin Nov 19, 2024
a2fa192
style : add max-h, overflow to ListCard
keemsebin Nov 19, 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
66 changes: 66 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 not shown.
Binary file not shown.
Binary file not shown.
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 @@ -31,6 +31,7 @@
"framer-motion": "^11.11.9",
"jest": "^29.7.0",
"react": "^18.3.1",
"react-cookie": "^7.2.2",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-router-dom": "^6.26.2",
Expand Down
11 changes: 8 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
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();
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 (
<Layout>
<MapDataProvider>
<MarkerProvider>
<Outlet />
<Layout>
<Outlet />
</Layout>
</MarkerProvider>
</Layout>
</MapDataProvider>
);
};
3 changes: 3 additions & 0 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import EmptyBookMark from './bookmark-empty.svg?react';
import BookMark from './bookmark.svg?react';
import Bubble from './bubble.svg?react';
import Cafe from './cafe-dessert.svg?react';
import Check from './check.svg?react';
import DeleteRound from './delete-round.svg?react';
import Fidies from './findies.svg?react';
import Findy1 from './findy-1.svg?react';
Expand Down Expand Up @@ -56,6 +57,7 @@ export const Icons = {
user: User,
back: Back,
home: Home,
check: Check,
deleteRound: DeleteRound,
kakaoLogin: KakaoLogin,
naverLogin: NaverLogin,
Expand Down
25 changes: 14 additions & 11 deletions src/components/common/Chip/Chip.variants.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { cva } from 'class-variance-authority';

export const ChipVariants = cva(`bg-primary rounded-md text-white cursor-default px-3 py-1`, {
variants: {
variant: {
small: 'h-6 min-w-6 text-caption',
medium: 'h-7 min-w-7 text-body4',
large: 'h-8 min-w-8 text-body3',
export const ChipVariants = cva(
`flex items-center bg-primary rounded-md text-white cursor-default px-3 py-1`,
{
variants: {
variant: {
small: 'h-5 min-w-6 text-caption',
medium: 'h-6 min-w-7 text-body4',
large: 'h-7 min-w-8 text-body3',
},
},
},
defaultVariants: {
variant: 'medium',
},
});
defaultVariants: {
variant: 'medium',
},
}
);
2 changes: 1 addition & 1 deletion src/components/common/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Props } from './Layout.types';

export const Layout = ({ children }: Props) => {
return (
<div className="relative 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-[30rem] h-dvh bg-white shadow-sm py-0 px-2 m-auto border-x-[1px] border-gray-50">
{children}
</div>
);
Expand Down
6 changes: 5 additions & 1 deletion src/components/common/ListCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { Props } from './ListCard.types';

export const ListCard = forwardRef<HTMLDivElement, Props>(({ children, ...props }, ref) => {
return (
<div className="w-full h-fit py-6 pl-6 pr-3.5 rounded-2xl bg-gray-100" {...props} ref={ref}>
<div
className="w-full max-h-[65dvh] h-fit py-5 pl-6 pr-5 rounded-2xl bg-gray-50 overflow-y-scroll"
{...props}
ref={ref}
>
{children}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/SideMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Tooltip } from '../Tooltip';

export const SideMenu = ({ variant, position = 'left', className = '', ...props }: Props) => {
return (
<button type="button" className="relative" {...props}>
<button type="button" className="relative focus:outline-none" {...props}>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

키보드 접근성 개선이 필요합니다

focus:outline-none 클래스를 사용하면 키보드 사용자가 현재 포커스된 요소를 시각적으로 식별하기 어려울 수 있습니다.

다음과 같이 포커스 스타일을 추가하는 것을 권장드립니다:

-    <button type="button" className="relative focus:outline-none" {...props}>
+    <button type="button" className="relative focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" {...props}>

이렇게 하면:

  • 마우스 사용자에게는 기본 아웃라인이 보이지 않습니다
  • 키보드 사용자에게는 시각적 포커스 표시가 제공됩니다
📝 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
<button type="button" className="relative focus:outline-none" {...props}>
<button type="button" className="relative focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" {...props}>

<IconButton name={variant} className={cn('peer', className)} />
<Tooltip
message={sideMenuTooltip[variant]}
Expand Down
10 changes: 10 additions & 0 deletions src/components/features/BookmarkList/BookmarkList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ListCard } from '@/components/common/ListCard';

export const BookmarkList = () => {
return (
<div>
나의 핀디 리스트
<ListCard>리스트 조회</ListCard>
</div>
);
};
Comment on lines +3 to +10
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

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { YoutubeResponse } from '@/hooks/api/link/useYoutubePlace';
import { useAuth } from '@/hooks/auth/useAuth';
import { Place } from '@/types/naver';

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

import { BookmarkList } from '../BookmarkList/BookmarkList';

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

if (type === 'search') {
return <SearchResult places={data as Place[]} />;
}
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.

🛠️ Refactor suggestion

타입 검증 로직 추가 필요

data as Place[] 타입 단언은 런타임 에러의 위험이 있습니다. 타입 가드를 사용하여 데이터 유효성을 검증하는 것이 좋습니다.

 if (type === 'search') {
+  const isPlaceArray = (data: unknown): data is Place[] =>
+    Array.isArray(data) && data.every(item =>
+      typeof item === 'object' && item !== null && 'name' in item
+    );
+  if (!isPlaceArray(data)) {
+    return <div>잘못된 데이터 형식입니다.</div>;
+  }
-  return <SearchResult places={data as Place[]} />;
+  return <SearchResult places={data} />;
 }
📝 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
if (type === 'search') {
return <SearchResult places={data as Place[]} />;
}
if (type === 'search') {
const isPlaceArray = (data: unknown): data is Place[] =>
Array.isArray(data) && data.every(item =>
typeof item === 'object' && item !== null && 'name' in item
);
if (!isPlaceArray(data)) {
return <div>잘못된 데이터 형식입니다.</div>;
}
return <SearchResult places={data} />;
}


if (type === 'extract') {
return <ExtractedPlaces places={data as YoutubeResponse} />;
}
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

YouTube 응답 데이터 검증 로직 추가 필요

YoutubeResponse 타입 검증이 필요합니다.

 if (type === 'extract') {
+  const isYoutubeResponse = (data: unknown): data is YoutubeResponse =>
+    typeof data === 'object' && data !== null && 'places' in data;
+  if (!isYoutubeResponse(data)) {
+    return <div>유효하지 않은 YouTube 데이터입니다.</div>;
+  }
-  return <ExtractedPlaces places={data as YoutubeResponse} />;
+  return <ExtractedPlaces places={data} />;
 }
📝 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
if (type === 'extract') {
return <ExtractedPlaces places={data as YoutubeResponse} />;
}
if (type === 'extract') {
const isYoutubeResponse = (data: unknown): data is YoutubeResponse =>
typeof data === 'object' && data !== null && 'places' in data;
if (!isYoutubeResponse(data)) {
return <div>유효하지 않은 YouTube 데이터입니다.</div>;
}
return <ExtractedPlaces places={data} />;
}


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

// TODO : 로그인하지 않은 경우 로그인 유도
return (
<div className=" flex items-center justify-center py-8">
<p className="text-gray-500">로그인한 사용자만 즐겨찾기가 가능합니다.</p>
</div>
);
};
27 changes: 27 additions & 0 deletions src/components/features/BottomSheetContent/ExtractedPlaces.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { extractedPlaceStepNames } from '@/constants/funnelStep';
import { YoutubeResponse } from '@/hooks/api/link/useYoutubePlace';
import { useFunnel } from '@/hooks/common/useFunnel';

import { BookmarkList } from '../BookmarkList/BookmarkList';
import { ExtractedList } from '../ExtractedPlacesList/ExtractedList';

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

Comment on lines +11 to +13
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리와 로딩 상태 관리가 필요합니다.

places 데이터가 유효하지 않은 경우나 로딩 중인 상태에 대한 처리가 없습니다.

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

 export const ExtractedPlaces = ({ places }: ExtractedPlacesProps) => {
   const { Funnel, Step, setStep } = useFunnel(extractedPlaceStepNames[0]);
+  if (!places) {
+    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
export const ExtractedPlaces = ({ places }: ExtractedPlacesProps) => {
const { Funnel, Step, setStep } = useFunnel(extractedPlaceStepNames[0]);
export const ExtractedPlaces = ({ places }: ExtractedPlacesProps) => {
const { Funnel, Step, setStep } = useFunnel(extractedPlaceStepNames[0]);
if (!places) {
return <div>데이터를 불러올 수 없습니다.</div>;
}

return (
<>
<Funnel>
<Step name="추출장소">
<ExtractedList places={places} onNext={() => setStep('리스트')} />
</Step>
{/* TODO : 리스트 노출 */}
<Step name="리스트">
<BookmarkList />
</Step>
</Funnel>
</>
);
};
27 changes: 27 additions & 0 deletions src/components/features/BottomSheetContent/SearchResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { searchPlaceStepNames } from '@/constants/funnelStep';
import { useFunnel } from '@/hooks/common/useFunnel';
import { Place } from '@/types/naver';

import { BookmarkList } from '../BookmarkList/BookmarkList';
import { SearchList } from '../SearchList/SearchList';

export type SearchResultProps = {
places: Place[];
};
export const SearchResult = ({ places }: SearchResultProps) => {
const { Funnel, Step, setStep } = useFunnel(searchPlaceStepNames[0]);
return (
<>
<Funnel>
<Step name="추출장소">
<SearchList places={places} onNext={() => setStep('리스트선택')} />
</Step>
{/* TODO : 리스트 선택, 리스트 노출 */}
<Step name="리스트선택">2. 리스트 선택</Step>
<Step name="리스트">
<BookmarkList />
</Step>
</Funnel>
</>
);
};
Comment on lines +11 to +27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

컴포넌트 구현에 대한 여러 개선사항이 있습니다.

  1. 빈 배열 처리가 필요합니다.
  2. 하드코딩된 문자열은 상수로 분리하는 것이 좋습니다.
  3. '리스트선택' 단계가 미완성 상태입니다.

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

 export const SearchResult = ({ places }: SearchResultProps) => {
   const { Funnel, Step, setStep } = useFunnel(searchPlaceStepNames[0]);
+  
+  if (places.length === 0) {
+    return <EmptyState message="검색 결과가 없습니다" />;
+  }
+
   return (
     <>
       <Funnel>
         <Step name="추출장소">
           <SearchList places={places} onNext={() => setStep('리스트선택')} />
         </Step>
-        {/* TODO : 리스트 선택, 리스트 노출 */}
-        <Step name="리스트선택">2. 리스트 선택</Step>
+        <Step name="리스트선택">
+          <ListSelector onSelect={(listId) => {
+            // 선택된 리스트 처리 로직
+            setStep('리스트');
+          }} />
+        </Step>
         <Step name="리스트">
           <BookmarkList />
         </Step>

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

2 changes: 2 additions & 0 deletions src/components/features/BottomSheetContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ExtractedPlaces } from './ExtractedPlaces';
export { SearchResult } from './SearchResult';
7 changes: 7 additions & 0 deletions src/components/features/BottomSheetContent/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { YoutubeResponse } from '@/hooks/api/link/useYoutubePlace';
import { Place } from '@/types/naver';

export type BottomSheetContentProps = {
type: 'search' | 'extract' | 'list' | null;
data?: Place[] | YoutubeResponse | null;
};
Loading
Loading