From b09aa6e6a877589a44733b245334a02e85d92c43 Mon Sep 17 00:00:00 2001 From: Doyun Lee <108219121+doyn511@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:44:36 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=A0=80=EC=9E=A5=ED=95=9C=20=EC=97=AC?= =?UTF-8?q?=ED=96=89=EC=A7=80=20=EC=84=9C=EB=B2=84api,=20=EA=B3=B5?= =?UTF-8?q?=EA=B3=B5api=20=EC=97=B0=EA=B2=B0=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 파일명 수정 * feat: detail페이지 즐겨찾기 api 연결 * feat: 마이페이지 저장한 리스트 연결 * feat: Map 저장한 여행지 연결 * feat: 사용자 로그인X시 Map 기본 위치 설정 * feat: map 로그인 되어있을 때만 바텀시트 open --- .../{postAddFavorite.ts => toggleFavorite.ts} | 16 ++-- src/views/Detail/components/Header.tsx | 17 +++- src/views/Detail/pages/DetailPage.tsx | 41 +++++++++- .../Map/components/BottomSheetContent.tsx | 6 +- src/views/Map/pages/MapPage.tsx | 80 +++++++++++++------ src/views/Map/utils/createFavoritePin.ts | 2 +- src/views/Map/utils/getDetailCommon.ts | 8 +- src/views/Mypage/components/Favorite.tsx | 21 ++++- .../Mypage/components/FavoritePlaceList.tsx | 61 +++++++------- src/views/Mypage/pages/Mypage.tsx | 8 +- src/views/Mypage/utils/getPlaceCardInfo.ts | 40 ++++++++++ 11 files changed, 215 insertions(+), 85 deletions(-) rename src/apis/supabase/{postAddFavorite.ts => toggleFavorite.ts} (65%) create mode 100644 src/views/Mypage/utils/getPlaceCardInfo.ts diff --git a/src/apis/supabase/postAddFavorite.ts b/src/apis/supabase/toggleFavorite.ts similarity index 65% rename from src/apis/supabase/postAddFavorite.ts rename to src/apis/supabase/toggleFavorite.ts index 559c0b0..5274171 100644 --- a/src/apis/supabase/postAddFavorite.ts +++ b/src/apis/supabase/toggleFavorite.ts @@ -1,14 +1,8 @@ -import { useLocation } from 'react-router-dom'; - import { unitripSupabase } from '@/utils/supabaseClient'; -const toggleFavorite = async () => { +const toggleFavorite = async (contentId: number) => { const kakaoId = sessionStorage.getItem('kakao_id'); - const location = useLocation(); - const queryParams = new URLSearchParams(location.search); - const place = queryParams.get('contentId'); - const { data, error: fetchError } = await unitripSupabase .from('USER') .select('favorite_list') @@ -20,9 +14,11 @@ const toggleFavorite = async () => { const currentFavorites = data[0].favorite_list || []; //기존 배열에 해당 장소가 존재하면 제거, 존재하지 않으면 추가 - const updatedFavorites = currentFavorites.includes(place) - ? currentFavorites.filter((favorite: number) => favorite !== Number(place)) - : [...currentFavorites, Number(place)]; + const updatedFavorites = currentFavorites.includes(contentId) + ? currentFavorites.filter( + (favorite: number) => favorite !== Number(contentId), + ) + : [...currentFavorites, Number(contentId)]; const { error } = await unitripSupabase .from('USER') diff --git a/src/views/Detail/components/Header.tsx b/src/views/Detail/components/Header.tsx index d900870..234c05d 100644 --- a/src/views/Detail/components/Header.tsx +++ b/src/views/Detail/components/Header.tsx @@ -1,19 +1,28 @@ import { css } from '@emotion/react'; import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; +import toggleFavorite from '@/apis/supabase/toggleFavorite'; import { ArrowLeftIcon, HeartFilledIcon, HeartGrayIcon } from '@/assets/icon'; import LoginModal from '@/components/LoginModal'; -const Header = () => { - const [isFavorite, setIsFavorite] = useState(false); +interface headerProps { + isFavorite: boolean; + setIsFavorite: React.Dispatch>; +} + +const Header = (props: headerProps) => { + const { isFavorite, setIsFavorite } = props; + const { contentId } = useParams(); + const [activateModal, setActivateModal] = useState(false); const isLoggedIn = sessionStorage.getItem('kakao_id'); const navigate = useNavigate(); - const favoriteOnClick = () => { + const favoriteOnClick = async () => { if (isLoggedIn) { + await toggleFavorite(Number(contentId)); setIsFavorite(!isFavorite); } else { setActivateModal(true); diff --git a/src/views/Detail/pages/DetailPage.tsx b/src/views/Detail/pages/DetailPage.tsx index 484e782..31f8a25 100644 --- a/src/views/Detail/pages/DetailPage.tsx +++ b/src/views/Detail/pages/DetailPage.tsx @@ -2,7 +2,9 @@ import { css } from '@emotion/react'; import { useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import getUserData from '@/apis/supabase/getUserData'; import { DefaultImage } from '@/assets/image'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; import { COLORS, FONTS } from '@/styles/constants'; import ErrorReport from '../components/ErrorReport'; @@ -58,7 +60,11 @@ const DetailPage = () => { useFee: '', }); + const [isFavorite, setIsFavorite] = useState(false); + const contentTypeId = useRef('12'); + const contentIdList = useRef([]); //서버에서 받아온 contentnId List + const kakaoId = sessionStorage.getItem('kakao_id'); useEffect(() => { const fetchData = async () => { @@ -69,6 +75,20 @@ const DetailPage = () => { fetchData(); }, []); + /** 서버 통신 -> favorite_list 받아오기 */ + useAsyncEffect(async () => { + if (!kakaoId) return; + + const userData = await getUserData(Number(kakaoId)); + if (userData) { + contentIdList.current = userData.favorite_list; + + contentIdList.current.includes(Number(contentId)) + ? setIsFavorite(true) + : setIsFavorite(false); + } + }, [isFavorite]); + const getDetailCommon1Res = async () => { const res = await getDetailCommonRes(Number(contentId)); @@ -79,7 +99,7 @@ const DetailPage = () => { info: { addr: item[0].addr1 !== '' ? item[0].addr1 : '-', tel: item[0].tel !== '' ? item[0].tel : '-', - useTime: '', + useTime: '-', }, imageUrl: item[0].firstimage !== '' ? item[0].firstimage : DefaultImage, }); @@ -106,12 +126,25 @@ const DetailPage = () => { ...prev, info: { ...prev.info, - useTime: item[0].usetime !== '' ? item[0].usetime : '-', + useTime: + contentTypeId.current === '12' + ? item[0].usetime && item[0].usetime !== '' + ? item[0].usetime + : '-' + : contentTypeId.current === '14' + ? item[0].usetimeculture && item[0].usetimeculture !== '' + ? item[0].usetimeculture + : '-' + : '-', }, })); setDetailInfo({ - restDate: item[0].restdate !== '' ? item[0].restdate : '-', + restDate: item[0].restdate + ? item[0].restdate !== '' + ? item[0].restdate + : '-' + : '-', useTime: contentTypeId.current === '12' ? item[0].usetime && item[0].usetime !== '' @@ -140,7 +173,7 @@ const DetailPage = () => { return (
-
+
{placeInfo.title}
diff --git a/src/views/Map/components/BottomSheetContent.tsx b/src/views/Map/components/BottomSheetContent.tsx index 03355c3..ce9ed10 100644 --- a/src/views/Map/components/BottomSheetContent.tsx +++ b/src/views/Map/components/BottomSheetContent.tsx @@ -1,6 +1,7 @@ /** 바텀시트 내부 맵핑 할 내용들 */ import { css } from '@emotion/react'; +import { useNavigate } from 'react-router-dom'; import { DefaultImage } from '@/assets/image'; import { COLORS, FONTS } from '@/styles/constants'; @@ -16,11 +17,10 @@ const BottomSheetContent = (props: contentProps) => { const { title, address, image, contentId } = props; const isImageNone = image === ''; - // const navigate = useNavigate(); + const navigate = useNavigate(); const onClickContent = () => { - console.log(contentId); - // navigate('/detail'); + navigate(`/${contentId}`); }; return ( diff --git a/src/views/Map/pages/MapPage.tsx b/src/views/Map/pages/MapPage.tsx index 00bb3ea..afadcda 100644 --- a/src/views/Map/pages/MapPage.tsx +++ b/src/views/Map/pages/MapPage.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; import { useEffect, useRef, useState } from 'react'; +import getUserData from '@/apis/supabase/getUserData'; import { CloseBottomSheetIcon, MapFavoirteIcon, @@ -10,12 +11,12 @@ import { } from '@/assets/icon'; import LoginModal from '@/components/LoginModal'; import MenuBar from '@/components/MenuBar'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; import { COLORS, FONTS } from '@/styles/constants'; import { locationBasedList1Res } from '@/types/locationBasedList1'; import FavoriteBottomSheet from '../components/FavoriteBottomSheet'; import PinBottomSheet from '../components/PinBottomSheet'; -import { DUMMY } from '../constants/DUMMY'; import { createFavoritePin } from '../utils/createFavoritePin'; import { createKakaoMap } from '../utils/createKakaoMap'; import { createMapPin } from '../utils/createMapPin'; @@ -48,7 +49,7 @@ const MapPage = () => { const [getLocActive, setGetLocActive] = useState(false); // 위치 허용에 따른 아이콘 변화 const [activateModal, setActivateModal] = useState(false); - const isLoggedIn = sessionStorage.getItem('kako_id'); + const kakaoId = sessionStorage.getItem('kakao_id'); // 바텀시트 내용 const [bottomSheetContent, setBottomSheetContent] = useState( @@ -66,11 +67,15 @@ const MapPage = () => { const apiRes = useRef([]); // 주변 여행지 검색 플로우 결과 저장 const favoriteList = useRef([]); // 저장한 여행지(공통정보api) 플로우 결과 저장 + const contentIdList = useRef([]); + + /** 지도 진입 시, */ + useAsyncEffect(async () => { + await getUserLoc(); + }, []); /** 기본 사용자의 위치에 따른 위도, 경도 값 업데이트 */ useEffect(() => { - // 서버에서 사용자 위치 받아와 저장하는 set하는 로직 필요 - setRegion({ city: '서울특별시', town: '광진구' }); const currentTown = setDefaultLocation(region.city, region.town); setDefaultLoc({ lat: currentTown?.lat, @@ -84,28 +89,57 @@ const MapPage = () => { setMap(kakaoMap); }, [defaultLoc]); - /** 저장한 여행지 목록 버튼 클릭 */ - const onClickFavorite = async () => { - if (isLoggedIn) { - if (map) { - clearMarker(); - - const res = await createFavoritePin( - DUMMY, // 서버에서 받아온 contentId list로 대체 필요 - map, - setBottomSheetContent, - openPinBottomSheet, - ); - - if (res) { - favoriteList.current = res.favoriteList; - setFavMarkers(res.defaultMarker); - } - - openFavoriteBottomSheet(); + /** 지도 진입 시 서버에 요청 전송해 사용자 위치 받아오기 */ + const getUserLoc = async () => { + if (kakaoId) { + const response = await getUserData(Number(kakaoId)); + if (response) { + setRegion({ + city: response.region.split(' ')[0], + town: response.region.split(' ')[1], + }); } } else { + setRegion({ + city: '서울특별시', + town: '중구', + }); + } + }; + + /** 저장한 여행지 버튼 클릭 시 서버에 요청 전송 */ + const getFavList = async () => { + if (!kakaoId) { setActivateModal(true); + } else { + const response = await getUserData(Number(kakaoId)); + if (response) { + contentIdList.current = response.favorite_list; + } + } + }; + + /** 저장한 여행지 목록 버튼 클릭 */ + const onClickFavorite = async () => { + await getFavList(); // 서버에서 contentId 리스트 받아오기 + + if (map) { + clearMarker(); + + // 하트 마커 그리기 + const res = await createFavoritePin( + contentIdList.current, + map, + setBottomSheetContent, + openPinBottomSheet, + ); + + if (res) { + favoriteList.current = res.favoriteList; + setFavMarkers(res.defaultMarker); + } + + kakaoId && openFavoriteBottomSheet(); } }; diff --git a/src/views/Map/utils/createFavoritePin.ts b/src/views/Map/utils/createFavoritePin.ts index 4065e55..461d987 100644 --- a/src/views/Map/utils/createFavoritePin.ts +++ b/src/views/Map/utils/createFavoritePin.ts @@ -15,7 +15,7 @@ export interface bottomSheetInfoType { } export const createFavoritePin = async ( - idList: contentIdListType[], + idList: number[], kakaoMap: mapType | undefined, setBottomSheetContent: React.Dispatch>, openPinBottomSheet: (state: string) => void, diff --git a/src/views/Map/utils/getDetailCommon.ts b/src/views/Map/utils/getDetailCommon.ts index ddd7b90..f884ac5 100644 --- a/src/views/Map/utils/getDetailCommon.ts +++ b/src/views/Map/utils/getDetailCommon.ts @@ -1,7 +1,5 @@ import { getDetailCommon1 } from '@/apis/public/detailCommon1'; -import { contentIdListType } from './createFavoritePin'; - export interface favoriteListType { title: string; address: string; @@ -11,9 +9,7 @@ export interface favoriteListType { contentId: string; } -export const getDetailCommonRes = async ( - contentIdList: contentIdListType[], -) => { +export const getDetailCommonRes = async (contentIdList: number[]) => { const favoriteList: favoriteListType[] = []; const promises = contentIdList.map(async (id) => { @@ -21,7 +17,7 @@ export const getDetailCommonRes = async ( numOfRows: 20, pageNo: 1, MobileOS: 'ETC', - contentId: Number(id.contentId), + contentId: Number(id), defaultYN: 'Y', firstImageYN: 'Y', addrinfoYN: 'Y', diff --git a/src/views/Mypage/components/Favorite.tsx b/src/views/Mypage/components/Favorite.tsx index d1755ca..863ce84 100644 --- a/src/views/Mypage/components/Favorite.tsx +++ b/src/views/Mypage/components/Favorite.tsx @@ -1,17 +1,30 @@ import { css } from '@emotion/react'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; +import getUserData from '@/apis/supabase/getUserData'; import EmptyFavList from '@/components/EmptyFavList'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; import { COLORS, FONTS } from '@/styles/constants'; import FavoritePlaceList from './FavoritePlaceList'; -const favoriteList = []; - const Favorite = () => { + const [favoriteList, setFavoriteList] = useState([]); + + useAsyncEffect(async () => { + const kakaoId = sessionStorage.getItem('kakao_id'); + if (!kakaoId) return; + + const userData = await getUserData(Number(kakaoId)); + if (userData) { + setFavoriteList(userData.favorite_list); + } + }, []); + return ( <> - {favoriteList.length === 0 ? ( + {favoriteList.length <= 1 ? (
@@ -19,7 +32,7 @@ const Favorite = () => {
) : ( - + )} ); diff --git a/src/views/Mypage/components/FavoritePlaceList.tsx b/src/views/Mypage/components/FavoritePlaceList.tsx index bb56029..70245ac 100644 --- a/src/views/Mypage/components/FavoritePlaceList.tsx +++ b/src/views/Mypage/components/FavoritePlaceList.tsx @@ -1,49 +1,56 @@ import { css } from '@emotion/react'; +import { useState } from 'react'; -const FAVORITE_SAMPLE = [ - { name: '대전시립미술관', address: '대전 서구 둔산대로 155' }, - { - name: '이응노 미술관', - address: '대전 서구 둔산대로 117번지 157', - }, - { name: '대전시립미술관', address: '대전 서구 둔산대로 155' }, - { - name: '이응노 미술관', - address: '대전 서구 둔산대로 117번지 157', - }, -]; - -function FavoritePlaceList() { - // 서버에서 contentID 리스트 받아오기 - // 해당 리스트 가지고 map/getDetailCommon 돌려서 값 저장하기 - // placeCard 누르면 해당 장소 contentId를 가지고 detail 페이지로 라우팅 +import PlaceCard from '@/components/PlaceCard'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; + +import { cardInfoType, getDetailInfo } from '../utils/getPlaceCardInfo'; + +interface placeListProps { + favoriteList: number[]; +} + +const FavoritePlaceList = (props: placeListProps) => { + const { favoriteList } = props; + const [cardInfoList, setCardInfoList] = useState([]); + + useAsyncEffect(async () => { + const res = await getDetailInfo(favoriteList); + if (res) { + setCardInfoList(res.cardInfo); + } + }, []); return (
    - {FAVORITE_SAMPLE.map((item) => ( -
  • - {/* ( +
  • + console.log('click')} - /> */} + imgSrc={item.image} + isHeart={favoriteList.includes(Number(item.contentId))} + buttonDisabled + contentid={item.contentId} + />
  • ))}
); -} +}; export default FavoritePlaceList; const listContainer = css` display: flex; - gap: 2.2rem; + gap: 1.2rem; align-items: center; flex-direction: column; width: 100%; - padding: 2.8rem 0; + overflow-y: scroll; + + padding: 2.8rem 2rem; `; const itemContainer = css` width: 100%; diff --git a/src/views/Mypage/pages/Mypage.tsx b/src/views/Mypage/pages/Mypage.tsx index 920e973..67a995e 100644 --- a/src/views/Mypage/pages/Mypage.tsx +++ b/src/views/Mypage/pages/Mypage.tsx @@ -92,7 +92,7 @@ const Mypage = () => { leftFn={backToMainTab} /> )} -
{renderComponent(currentTab)}
+
{renderComponent(currentTab)}
{currentTab === 'main' && } ); @@ -100,12 +100,14 @@ const Mypage = () => { export default Mypage; -const mypageContainer = css` +const mypageContainer = (tab: string) => css` display: flex; flex-direction: column; width: 100dvw; - height: calc(100dvh - 8rem - 4.8rem); + height: ${tab === '저장한 여행지 목록' + ? 'calc(100dvh - 5rem)' + : 'calc(100dvh - 8rem - 4.8rem)'}; background-color: white; `; diff --git a/src/views/Mypage/utils/getPlaceCardInfo.ts b/src/views/Mypage/utils/getPlaceCardInfo.ts new file mode 100644 index 0000000..ea6263e --- /dev/null +++ b/src/views/Mypage/utils/getPlaceCardInfo.ts @@ -0,0 +1,40 @@ +import { getDetailCommon1 } from '@/apis/public/detailCommon1'; + +export interface cardInfoType { + title: string; + address: string; + image: string; + contentId: string; +} + +export const getDetailInfo = async (contentIdList: number[]) => { + const cardInfo: cardInfoType[] = []; + + const promises = contentIdList.map(async (id) => { + const response = await getDetailCommon1({ + numOfRows: 20, + pageNo: 1, + MobileOS: 'ETC', + contentId: Number(id), + defaultYN: 'Y', + firstImageYN: 'Y', + addrinfoYN: 'Y', + mapinfoYN: 'Y', + }); + + if (response) { + cardInfo.push({ + title: response.item[0].title, + address: response.item[0].addr1, + image: response.item[0].firstimage, + contentId: response.item[0].contentid, + }); + } + }); + + await Promise.all(promises); + + if (cardInfo.length !== 0) { + return { cardInfo }; + } +};