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] 네이버 즐겨찾기 저장 #73

Merged
merged 16 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

services:
react-app:
build:
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ListCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Props } from './ListCard.types';
export const ListCard = forwardRef<HTMLDivElement, Props>(({ children, ...props }, ref) => {
return (
<div
className="w-full max-h-[65dvh] h-fit py-5 pl-6 pr-5 rounded-2xl bg-gray-50 overflow-y-scroll"
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}
>
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/Typography/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { cva, type VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/core';

import { Props, TypographyVariant } from './Typography.types';

const variantClasses = cva('whitespace-pre-wrap select-none', {
Expand Down Expand Up @@ -37,10 +39,8 @@ const Typography = ({
className,
...props
}: Props & TypographyVariants) => {
const classes = variantClasses({ type: variant, weight, className });

return (
<p className={classes} {...props}>
<p className={cn(variantClasses({ type: variant, weight }), className)} {...props}>
{children}
</p>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/features/BookmarkDetail/BookmarkDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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';
import { Category } from '@/types/naver';

type Props = { bookmarkId: number; onPrev: () => void };
export const BookmarkDetail = ({ bookmarkId, onPrev }: Props) => {
Expand Down Expand Up @@ -46,8 +47,12 @@ export const BookmarkDetail = ({ bookmarkId, onPrev }: Props) => {
<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>
{item.category && (
<Chip variant="medium">
{typeof item.category === 'object'
? (item.category as Category).majorCategory
: item.category}
</Chip>
)}
</div>
<Body4 className="pt-1 " weight="normal">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { FlowType } from '@/constants/funnelStep';
import { ExtractResponse } from '@/hooks/api/link/useYoutubePlace';
import { useBottomFunnel } from '@/hooks/common/useBottomFunnel';

import { BottomSheetContentProps } from './types';

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

export const BottomSheetContent = ({ type, data }: BottomSheetContentProps) => {
return useBottomFunnel({ type: type as FlowType, data: data as Place[] | YoutubeResponse });
return useBottomFunnel({ type: type as FlowType, data: data as Place[] | ExtractResponse });
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import { Chip } from '@/components/common/Chip';
import { Icon } from '@/components/common/Icon';
import { ListCard } from '@/components/common/ListCard';
import { Body2, Body3, Body4 } from '@/components/common/Typography';
import { findyIconNames } from '@/constants/findyIcons';
import { useNaverBookmark } from '@/hooks/api/bookmarks/useNaverBookmark';
import { useYoutubeBookmark } from '@/hooks/api/bookmarks/useYoutubeBookmark';
import { YoutubeResponse } from '@/hooks/api/link/useYoutubePlace';
import { ExtractResponse } from '@/hooks/api/link/useYoutubePlace';
import { useAuth } from '@/hooks/auth/useAuth';
import { useMarkers } from '@/hooks/common/useMarkers';
import { Category } from '@/types/naver';

import { Login } from '../LoginModal';

type Props = { places: YoutubeResponse; onNext: () => void };
export const ExtractedPlacesList = ({ places, onNext }: Props) => {
type Props = { data: ExtractResponse; onNext: () => void };
export const ExtractedPlacesList = ({ data, 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 { mutate: youtubeMutate } = useYoutubeBookmark(token);
const { mutate: naverMutate } = useNaverBookmark(token);

const handleToggleSelect = (id: number) => {
setSelectedIds((prev) =>
Expand All @@ -32,53 +36,69 @@ export const ExtractedPlacesList = ({ places, onNext }: Props) => {
setIsOpen(true);
return;
}

const savePlaces = {
...places,
places: places.places
const savePlaces: ExtractResponse = {
...data,
places: data.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.clear();
clearMarkers();
onNext();
},
});
if (data.youtuberId) {
youtubeMutate(savePlaces, {
onSuccess: () => {
sessionStorage.clear();
clearMarkers();
onNext();
},
});
}
if (data.name) {
naverMutate(savePlaces, {
onSuccess: () => {
sessionStorage.clear();
clearMarkers();
onNext();
},
});
}
};
return (
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-row gap-4 py-2">
{/* TODO : 이미지가 없을 경우 대체 이미지 추가 */}
<img
src={places.youtuberProfile}
className="w-12 h-12 rounded-full"
alt={`${places.youtuberName}의 프로필 이미지`}
/>
<div className="flex flex-col ">
<Body2 weight="medium">{places.youtuberName}</Body2>
{data.youtuberProfile ? (
<img
src={data.youtuberProfile || ''}
className="w-12 h-12 rounded-full "
alt={`${data.youtuberName}프로필 이미지`}
/>
) : (
<Icon name={findyIconNames[0]} className="w-11 h-11" />
)}
<div className="flex flex-col">
<Body2 weight="medium">{data.youtuberName ?? data.name}</Body2>
<div className="flex flex-row items-center gap-1">
<Icon name="location" size={20} />
<Body3 className=" text-gray-500">{places.places.length}</Body3>
<Icon name="location" size={17} />
<Body3 className="text-gray-500">{data.places.length}</Body3>
</div>
</div>
</div>
<ListCard>
{/* TODO 컴포넌트화 */}
{places.places.map((item, index) => (
<>
{data?.places.map((item, index) => (
<div key={`${item.title}-${item.address}`}>
<div
key={`${item.title}-${item.address}`}
className={`flex flex-row justify-between items-center ${index !== places.places.length - 1 && 'pb-2'}`}
className={`flex flex-row justify-between items-center ${index !== data.places.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>
{item.category && (
<Chip variant="medium">
{typeof item.category === 'object'
? (item.category as Category).majorCategory
: item.category}
</Chip>
)}
</div>
<Body4 className="pt-1" weight="normal">
Expand All @@ -92,8 +112,8 @@ export const ExtractedPlacesList = ({ places, onNext }: Props) => {
onClick={() => handleToggleSelect(item.id as number)}
/>
</div>
{index < places.places.length - 1 && <hr className="border-dashed pt-2" />}
</>
{index < data.places.length - 1 && <hr className="border-dashed pt-2" />}
</div>
))}
</ListCard>
<Button variant="primary" size="large" onClick={handleSave}>
Expand Down
16 changes: 8 additions & 8 deletions src/components/features/LinkForm/LinkInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ import { LinkFormProps } from './types';
export type LinkInputProp<T> = ContextProps<T> & LinkFormProps;

export const LinkInput = ({ onNext, onHomeClick, context }: LinkInputProp<string>) => {
const { state: youtubeLink, setState: setYoutube } = context;
const { state, onChange, onClickReset, isValid, onBlur, ref } = useInput(youtubeLink);
const { state: link, setState: setLink } = context;
const { state: inputValue, onChange, onClickReset, isValid, onBlur, ref } = useInput(link);

const handleSaveAndNext = () => {
setYoutube(state);
onNext();
setLink(inputValue);
onNext(inputValue);
};

return (
<div className="flex flex-col items-center justify-between">
<Header left={<Icon name="home" size={20} onClick={onHomeClick} />} />
<div className="w-full flex flex-col items-start gap-6 my-32 px-6">
<div className="w-full flex flex-col items-start gap-6 my-36 px-6">
<Body1>{`아래에 링크를 입력해주시면,\n특별한 장소 정보를 추출해드릴게요.`}</Body1>
<Input
value={state}
value={inputValue}
onChange={onChange}
onBlur={onBlur}
onClickReset={() => {
onClickReset();
setYoutube('');
setLink('');
}}
isValid={isValid}
ref={ref}
Expand All @@ -41,7 +41,7 @@ export const LinkInput = ({ onNext, onHomeClick, context }: LinkInputProp<string
variant="primary"
size="large"
onClick={handleSaveAndNext}
disabled={state.length === 0 || !isValid}
disabled={inputValue.length === 0 || !isValid}
className="w-full"
>
장소 추출하기
Expand Down
2 changes: 1 addition & 1 deletion src/components/features/LinkForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export type LinkFormProps = {
/**
* Function to be called when moving to the next step.
*/
onNext: () => void;
onNext: (value?: string) => void;
/**
* Function to be called when moving to the previous step.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const BookmarkSelectionList = ({ selectedPlace, onNext }: Props) => {
<Body1 className="my-3 mx-3">북마크 리스트</Body1>
<ListCard>
{data?.data.map((item, index) => (
<>
<div key={item.bookmarkId} className={`flex flex-row justify-between items-center `}>
<div key={item.bookmarkId}>
<div className={`flex flex-row justify-between items-center `}>
<div className="flex flex-row gap-4 py-2.5 items-center justify-center">
{item.youtuberProfile ? (
<img
Expand Down Expand Up @@ -72,7 +72,7 @@ export const BookmarkSelectionList = ({ selectedPlace, onNext }: Props) => {
)}
</div>
{index < data.data.length - 1 && <hr className="border-dashed pt-2" />}
</>
</div>
))}
</ListCard>
<Button variant="primary" size="large" onClick={handleSave} disabled={bookmarkId === 0}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ListCard } from '@/components/common/ListCard';
import { Body1, Body2, Body4 } from '@/components/common/Typography';
import { NewMarker } from '@/hooks/api/marker/useNewMarker';
import { useAuth } from '@/hooks/auth/useAuth';
import { Place } from '@/types/naver';
import { Category, Place } from '@/types/naver';

import { Login } from '../LoginModal';

Expand Down Expand Up @@ -47,7 +47,13 @@ export const SearchResultsList = ({ places, onNext, onSelect }: Props) => {
<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 === 'string' && <Chip variant="medium">{item.category}</Chip>}
{item.category && (
<Chip variant="medium">
{typeof item.category === 'object'
? (item.category as Category).majorCategory
: item.category}
</Chip>
)}
Comment on lines +50 to +56
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

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

현재 구현은 타입 단언(type assertion)을 사용하고 있어 런타임 오류가 발생할 수 있습니다.

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

- {typeof item.category === 'object'
-   ? (item.category as Category).majorCategory
-   : item.category}
+ {typeof item.category === 'object' && 'majorCategory' in item.category
+   ? item.category.majorCategory
+   : item.category}

이렇게 수정하면:

  1. 타입 가드를 통해 더 안전한 타입 체크가 가능합니다
  2. 런타임에서 majorCategory 속성의 존재 여부를 확인할 수 있습니다
📝 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
{item.category && (
<Chip variant="medium">
{typeof item.category === 'object'
? (item.category as Category).majorCategory
: item.category}
</Chip>
)}
{item.category && (
<Chip variant="medium">
{typeof item.category === 'object' && 'majorCategory' in item.category
? item.category.majorCategory
: item.category}
</Chip>
)}

</div>
<Body4 className="pt-1" weight="normal">
{item.address}
Expand Down
15 changes: 15 additions & 0 deletions src/hooks/api/bookmarks/useNaverBookmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMutation } from '@tanstack/react-query';

import { ExtractResponse } from '@/hooks/api/link/useYoutubePlace';
import { post } from '@/lib/axios';

export const useNaverBookmark = (token: string) => {
return useMutation({
mutationFn: (naverData: ExtractResponse) =>
post(`api/bookmarks/naver`, naverData, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
});
};
keemsebin marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 2 additions & 3 deletions src/hooks/api/bookmarks/useYoutubeBookmark.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useMutation } from '@tanstack/react-query';

import { ExtractResponse } from '@/hooks/api/link/useYoutubePlace';
import { post } from '@/lib/axios';

import { YoutubeResponse } from '../link/useYoutubePlace';

export const useYoutubeBookmark = (token: string) => {
return useMutation({
mutationFn: (youtubeData: YoutubeResponse) =>
mutationFn: (youtubeData: ExtractResponse) =>
post(`api/bookmarks/youtube`, youtubeData, {
headers: {
Authorization: `Bearer ${token}`,
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/api/link/useNaverMapPlace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query';

import { post } from '@/lib/external';

import { ExtractResponse } from './useYoutubePlace';

export type NaverMapLink = {
url: string;
};

export const useNaverMapPlace = () => {
return useMutation<ExtractResponse, Error, NaverMapLink>({
mutationFn: ({ url }) =>
post<ExtractResponse>('naver_bookmark', {
url,
}),
retry: 1,
});
};
keemsebin marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 8 additions & 7 deletions src/hooks/api/link/useYoutubePlace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import { useQuery } from '@tanstack/react-query';
import { get } from '@/lib/external';
import { Place } from '@/types/naver';

export type YoutubeResponse = {
youtuberId: string;
youtuberName: string;
youtuberProfile: string;
youtubeLink: string;
export type ExtractResponse = {
name?: string;
youtuberId?: string;
youtuberName?: string;
youtuberProfile?: string;
youtubeLink?: string;
places: Place[];
};

export const useYoutubePlace = (youtubeLink: string) => {
return useQuery<YoutubeResponse>({
return useQuery<ExtractResponse>({
queryKey: ['youtubeLink', youtubeLink],
queryFn: () => get<YoutubeResponse>(`video/place/${encodeURIComponent(youtubeLink)}`),
queryFn: () => get<ExtractResponse>(`video/place/${encodeURIComponent(youtubeLink)}`),
enabled: !!youtubeLink,
retry: 1,
});
Expand Down
Loading
Loading