diff --git a/src/api/scrap/entity.ts b/src/api/scrap/entity.ts new file mode 100644 index 00000000..444adb7f --- /dev/null +++ b/src/api/scrap/entity.ts @@ -0,0 +1,46 @@ +export interface GetMyScrapShopResponse { + content: { + address: string; + category: string; + createdAt: string; + directory: { + createdAt: string; + id: number; + name: string; + scrapCount: number; + updatedAt: string; + }; + name: string; + photo: string; + placeId: string; + ratingCount: number; + scrapId: number; + totalRating: number; + updatedAt: string; + }[]; + empty: boolean; + first: boolean; + last: boolean; + number: number; + numberOfElements: number; + pageable: { + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + unpaged: boolean; + }; + size: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + totalElements: number; + totalPages: number; +} diff --git a/src/api/scrap/index.ts b/src/api/scrap/index.ts new file mode 100644 index 00000000..33142d86 --- /dev/null +++ b/src/api/scrap/index.ts @@ -0,0 +1,10 @@ +import scrapApi from './scrapApiClient'; +import type { GetMyScrapShopResponse } from './entity'; + +export const postScrapShop = (shopId: string) => + // post된 음식점이 들어가는 최상위 directoryId는 0입니다. + scrapApi.post('/scraps', { directoryId: 0, placeId: shopId }); + +export const getMyScrapShop = () => scrapApi.get('/scraps'); + +export const deleteScrapShop = (scrapId: number) => scrapApi.delete(`/scraps/${scrapId}`); diff --git a/src/api/scrap/scrapApiClient.ts b/src/api/scrap/scrapApiClient.ts new file mode 100644 index 00000000..f885c88f --- /dev/null +++ b/src/api/scrap/scrapApiClient.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { API_PATH } from 'config/constants'; + +const scrapApi = axios.create({ + baseURL: `${API_PATH}`, + timeout: 2000, +}); + +scrapApi.interceptors.request.use((config) => { + const accessToken = sessionStorage.getItem('accessToken'); + // eslint-disable-next-line no-param-reassign + if (config.headers && accessToken) config.headers.Authorization = `Bearer ${accessToken}`; + return config; +}); + +export default scrapApi; diff --git a/src/assets/svg/shop/book-mark.svg b/src/assets/svg/shop/book-mark.svg index 1c043dff..654e09ca 100644 --- a/src/assets/svg/shop/book-mark.svg +++ b/src/assets/svg/shop/book-mark.svg @@ -1,3 +1,3 @@ - + diff --git a/src/pages/ShopDetail/ShopDetail.module.scss b/src/pages/ShopDetail/ShopDetail.module.scss index cfc55aed..dcbf712c 100644 --- a/src/pages/ShopDetail/ShopDetail.module.scss +++ b/src/pages/ShopDetail/ShopDetail.module.scss @@ -80,18 +80,6 @@ font-size: 36px; font-weight: normal; } - - & > button { - display: flex; - gap: 2px; - align-items: center; - justify-content: center; - background: #f8f8f8; - padding: 16px 24px; - border-radius: 4px; - font-size: 16px; - color: #666; - } } &__info-name { diff --git a/src/pages/ShopDetail/components/Map/index.tsx b/src/pages/ShopDetail/components/Map/index.tsx index 8634a62e..efcbca57 100644 --- a/src/pages/ShopDetail/components/Map/index.tsx +++ b/src/pages/ShopDetail/components/Map/index.tsx @@ -1,4 +1,5 @@ import useNaverMap from 'pages/Home/components/Map/hooks/useNaverMap'; +import makeToast from 'utils/ts/makeToast'; import SectionHeader from '../SectionHeader'; import styles from './Map.module.scss'; @@ -9,6 +10,16 @@ interface Props { } function Map({ formattedAddress, latitude, longitude }: Props) { + const copyURL = () => { + const urlToCopy = window.location.href; + + navigator.clipboard.writeText(urlToCopy).then(() => { + makeToast('success', 'URL을 클립보드에 복사하였습니다.'); + }).catch(() => { + makeToast('error', 'URL을 복사하는데 실패했습니다.'); + }); + }; + useNaverMap(latitude, longitude); return ( @@ -16,7 +27,7 @@ function Map({ formattedAddress, latitude, longitude }: Props) { {} }} + button={{ content: 'URL 복사', onClick: copyURL }} />
diff --git a/src/pages/ShopDetail/components/ScrapButton/ScrapButton.module.scss b/src/pages/ShopDetail/components/ScrapButton/ScrapButton.module.scss new file mode 100644 index 00000000..124969e7 --- /dev/null +++ b/src/pages/ShopDetail/components/ScrapButton/ScrapButton.module.scss @@ -0,0 +1,17 @@ +.scrap-button { + display: flex; + gap: 2px; + align-items: center; + justify-content: center; + background: #f8f8f8; + padding: 16px 24px; + border-radius: 4px; + font-size: 16px; + color: #666; + transition: all 0.2s ease; + + &--active { + background: #ff7f23; + color: #fff; + } +} diff --git a/src/pages/ShopDetail/components/ScrapButton/index.tsx b/src/pages/ShopDetail/components/ScrapButton/index.tsx new file mode 100644 index 00000000..54dd6390 --- /dev/null +++ b/src/pages/ShopDetail/components/ScrapButton/index.tsx @@ -0,0 +1,30 @@ +import useScrap from 'utils/hooks/useScrap'; +import cn from 'utils/ts/classNames'; +import { ReactComponent as BookMarkIcon } from 'assets/svg/shop/book-mark.svg'; +import styles from './ScrapButton.module.scss'; + +interface Props { + placeId: string; + initialScrapId?: number | null; +} + +function ScrapButton({ placeId, initialScrapId = null }: Props) { + const { scrapId, toggleScrap, isPending } = useScrap(placeId, initialScrapId); + + return ( + + ); +} + +export default ScrapButton; diff --git a/src/pages/ShopDetail/index.tsx b/src/pages/ShopDetail/index.tsx index 3ea614c3..69f6ea5d 100644 --- a/src/pages/ShopDetail/index.tsx +++ b/src/pages/ShopDetail/index.tsx @@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query'; import AuthTopNavigation from 'components/Auth/AuthTopNavigation'; import { fetchShop } from 'api/shop'; import StarRatingPreview from 'components/StarRating/StarRatingPreview'; -import { ReactComponent as BookMarkIcon } from 'assets/svg/shop/book-mark.svg'; import { ReactComponent as InfoIcon } from 'assets/svg/shop/info.svg'; import styles from './ShopDetail.module.scss'; import ImageCarousel from './components/ImageCarousel'; import FriendReviewList from './components/ReviewList/FriendReviewList'; import MyReviewList from './components/ReviewList/MyReviewList'; +import ScrapButton from './components/ScrapButton'; import Map from './components/Map'; // import mock from './mock'; @@ -26,11 +26,11 @@ function ShopDetail() { if (data) { const { // shopId, - // placeId, // periods, - // scrap, // openNow, // category, + // placeId, + scrap, name, formattedAddress, lat, @@ -60,10 +60,7 @@ function ShopDetail() {

{name}

- + {placeId && }
diff --git a/src/utils/hooks/useScrap.ts b/src/utils/hooks/useScrap.ts new file mode 100644 index 00000000..ea87bad3 --- /dev/null +++ b/src/utils/hooks/useScrap.ts @@ -0,0 +1,31 @@ +import { useMutation } from '@tanstack/react-query'; +import { deleteScrapShop, postScrapShop } from 'api/scrap'; +import { useState } from 'react'; + +const useScrap = (placeId: string, initialScrapId: number | null) => { + const [scrapId, setScrapId] = useState(initialScrapId); + + const { mutate: postScrap, isPending: postPending } = useMutation({ + mutationFn: postScrapShop, + onSuccess: (res) => setScrapId(res.data.id), + }); + + const { mutate: deleteScrap, isPending: deletePending } = useMutation({ + mutationFn: deleteScrapShop, + }); + + const toggleScrap = () => { + if (scrapId) { + deleteScrap(scrapId); + setScrapId(null); + } else { + postScrap(placeId); + } + }; + + const isPending = postPending || deletePending; + + return { scrapId, toggleScrap, isPending }; +}; + +export default useScrap;