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 15 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
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,24 @@ 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 { 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 +35,67 @@ 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' && (
{typeof item.category === 'object' ? (
<Chip variant="medium">{item.category.majorCategory}</Chip>
) : (
<Chip variant="medium">{item.category}</Chip>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

item.category의 타입 정의 개선 필요

typeof item.category === 'object'로 타입을 체크하는 대신, 타입스크립트의 타입 시스템을 활용하여 item.category의 타입을 명확히 정의하면 코드의 안정성과 가독성을 높일 수 있습니다.

예를 들어, Category 타입을 정의하고 item.category에 적용해 보세요:

type Category = {
  majorCategory: string;
};

...

{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 +109,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
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}`,
},
}),
});
};
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,
});
};
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
9 changes: 4 additions & 5 deletions src/hooks/common/useBottomFunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ import { ExtractedPlacesList } from '@/components/features/ExtractedPlacesList/E
import { SearchResultsList } from '@/components/features/SearchResultsList';
import { BookmarkSelectionList } from '@/components/features/SearchResultsList/BookmarkSelectionList';
import { FLOW_CONFIGS, FlowType, STEPS, StepType } from '@/constants/funnelStep';
import { ExtractResponse } from '@/hooks/api/link/useYoutubePlace';
import { useFunnel } from '@/hooks/common/useFunnel';
import { Place } from '@/types/naver';

import { useFunnel } from './useFunnel';

import { YoutubeResponse } from '../api/link/useYoutubePlace';
import { NewMarker } from '../api/marker/useNewMarker';
import { useAuth } from '../auth/useAuth';

type bottomFunnelProps = {
type: FlowType;
data?: Place[] | YoutubeResponse;
data?: Place[] | ExtractResponse;
};
export const useBottomFunnel = ({ type, data }: bottomFunnelProps) => {
const { token } = useAuth();
Expand Down Expand Up @@ -57,7 +56,7 @@ export const useBottomFunnel = ({ type, data }: bottomFunnelProps) => {
),
[STEPS.EXTRACTED_PLACES]: (
<ExtractedPlacesList
places={data as YoutubeResponse}
data={data as ExtractResponse}
onNext={() => handleStepChange(STEPS.LIST)}
/>
),
Expand Down
15 changes: 7 additions & 8 deletions src/hooks/common/useYoutubeContext.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { useReducer } from 'react';

type Action = { type: 'SET_LINK'; payload: YoutubeLink };
type Action = { type: 'SET_LINK'; payload: string };

export type ContextProps<T> = {
context: { state: T; setState: (newState: T) => void };
};

export type YoutubeLink = {
youtubeLink: string;
export type Link = {
link: string;
};

export const useYoutubeContext = () => {
const [linkData, dispatch] = useReducer(reducer, initial());

return [linkData, dispatch] as const;
};

const initial = (): YoutubeLink => ({
youtubeLink: '',
const initial = (): Link => ({
link: '',
});

const reducer = (state: YoutubeLink, action: Action): YoutubeLink => {
const reducer = (state: Link, action: Action): Link => {
switch (action.type) {
case 'SET_LINK':
return { ...state, youtubeLink: action.payload.youtubeLink };
return { ...state, link: action.payload };
default:
return state;
}
Expand Down
Loading
Loading