From b95985e661f272441b8bd184cb05eedbedf46f37 Mon Sep 17 00:00:00 2001 From: SeoHyun Kim Date: Sun, 29 Sep 2024 18:37:21 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20api=20=EC=97=B0=EA=B2=B0=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/public/main.ts | 48 ++++++++++++++ src/apis/public/search.ts | 4 +- src/components/PlaceCard.tsx | 22 ++++--- src/types/main.ts | 29 +++++++++ src/types/public.ts | 17 ++++- src/types/search.ts | 19 +----- src/views/Main/components/NearbyTravel.tsx | 58 ++++++++++------- src/views/Main/components/TravelCard.tsx | 63 ++++++++++++++----- src/views/Main/pages/MainPage.tsx | 25 ++++---- src/views/Main/styles/main.ts | 5 +- .../Search/components/Result/SearchResult.tsx | 4 +- .../components/SearchBar/RelatedWordList.tsx | 4 +- .../SearchBar/SearchBarContainer.tsx | 4 +- .../hooks/use-debounce-get-word-list.ts | 4 +- src/views/Search/pages/SearchResultPage.tsx | 4 +- 15 files changed, 216 insertions(+), 94 deletions(-) create mode 100644 src/apis/public/main.ts create mode 100644 src/types/main.ts diff --git a/src/apis/public/main.ts b/src/apis/public/main.ts new file mode 100644 index 0000000..31baff6 --- /dev/null +++ b/src/apis/public/main.ts @@ -0,0 +1,48 @@ +// 검색 관련 공공 데이터 API + +import { AreaCodeItem, PlaceBasedAreaItem } from '@/types/main'; +import { Response } from '@/types/public'; + +import { publicDataClient } from '..'; + +interface placeBasedAreaParams { + region: string; +} + +export const getAreaCode = async () => { + const params = `MobileApp=UNITRIP&_type=json&numOfRows=20&MobileOS=ETC&serviceKey=${import.meta.env.VITE_PUBLIC_DATA_SERVICE_KEY}`; + + const { + data: { + response: { + body: { items }, + }, + }, + } = await publicDataClient.get>( + `/areaCode1?${params}`, + ); + return items; +}; + +export const getPlaceBasedArea = async (paramsInfo: placeBasedAreaParams) => { + const areaItem = await getAreaCode(); + + const areaCode = + areaItem === '' + ? '1' + : areaItem.item.find(({ name }) => paramsInfo.region.startsWith(name)) + ?.code; + + const params = `MobileApp=UNITRIP&_type=json&arrange=Q&contentTypeId=12&areaCode=${areaCode || '1'}&MobileOS=ETC&serviceKey=${import.meta.env.VITE_PUBLIC_DATA_SERVICE_KEY}`; + + const { + data: { + response: { + body: { items }, + }, + }, + } = await publicDataClient.get>( + `/areaBasedList1?${params}`, + ); + return items; +}; diff --git a/src/apis/public/search.ts b/src/apis/public/search.ts index d64d6b1..ab11626 100644 --- a/src/apis/public/search.ts +++ b/src/apis/public/search.ts @@ -1,7 +1,7 @@ // 검색 관련 공공 데이터 API import { Response } from '@/types/public'; -import { SearchWord } from '@/types/search'; +import { SearchItem } from '@/types/search'; import { publicDataClient } from '..'; @@ -26,7 +26,7 @@ export const getSearchKeyword = async (paramsInfo: searchKeywordParams) => { body: { items }, }, }, - } = await publicDataClient.get>( + } = await publicDataClient.get>( `/searchKeyword1?${params}`, ); return items; diff --git a/src/components/PlaceCard.tsx b/src/components/PlaceCard.tsx index 18ae4da..0e23409 100644 --- a/src/components/PlaceCard.tsx +++ b/src/components/PlaceCard.tsx @@ -61,10 +61,10 @@ const cardContainerCss = (imgSrc: string, placeName: string) => css` height: 16.8rem; border-radius: 1.2rem; - background-image: url(${imgSrc}); - background-size: cover; - background-position: center center; background-color: ${placeName ? COLORS.gray4 : COLORS.gray2}; + background-position: center center; + background-size: cover; + background-image: url(${imgSrc}); `; const backgroundCss = css` @@ -76,24 +76,26 @@ const backgroundCss = css` height: 16.8rem; border-radius: 1.2rem; - color: ${COLORS.white}; - background: linear-gradient( 180deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.34) 100% + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 34%) 100% ); + + color: ${COLORS.white}; `; const titleCss = css` - margin: 9.4rem 0 0 1.6rem; - ${FONTS.H3}; + overflow: hidden; width: calc(100% - 1.6rem); + margin: 9.4rem 0 0 1.6rem; + text-align: left; white-space: nowrap; - overflow: hidden; text-overflow: ellipsis; + + ${FONTS.H3}; `; const addressCss = css` diff --git a/src/types/main.ts b/src/types/main.ts new file mode 100644 index 0000000..b6665cf --- /dev/null +++ b/src/types/main.ts @@ -0,0 +1,29 @@ +export interface PlaceBasedAreaItem { + mapy: string; + mlevel: string; + cpyrhtDivCd: string; + firstimage: string; + firstimage2: string; + mapx: string; + booktour: string; + contentid: string; + cat3: string; + cat2: string; + modifiedtime: string; + sigungucode: string; + tel: string; + title: string; + zipcode: string; + addr1: string; + addr2: string; + areacode: string; + cat1: string; + contenttypeid: string; + createdtime: string; +} + +export interface AreaCodeItem { + rnum: string; + code: string; + name: string; +} diff --git a/src/types/public.ts b/src/types/public.ts index 73e5308..caa38ba 100644 --- a/src/types/public.ts +++ b/src/types/public.ts @@ -1,3 +1,18 @@ export interface Response { - response: T; + response: { + header: { + resultCode: string; + resultMsg: string; + }; + body: { + numOfRows: number; + pageNo: number; + totalCount: number; + items: + | { + item: T; + } + | ''; + }; + }; } diff --git a/src/types/search.ts b/src/types/search.ts index c2f3f9e..c0e3fdf 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,4 +1,4 @@ -export interface SearchResItem { +export interface SearchItem { cat2: string; cat3: string; tel: string; @@ -20,20 +20,3 @@ export interface SearchResItem { createdtime: string; firstimage: string; } - -export interface SearchWord { - header: { - resultCode: string; - resultMsg: string; - }; - body: { - numOfRows: number; - pageNo: number; - totalCount: number; - items: - | { - item: SearchResItem[]; - } - | ''; - }; -} diff --git a/src/views/Main/components/NearbyTravel.tsx b/src/views/Main/components/NearbyTravel.tsx index 43d03aa..91deedd 100644 --- a/src/views/Main/components/NearbyTravel.tsx +++ b/src/views/Main/components/NearbyTravel.tsx @@ -2,19 +2,25 @@ import { css } from '@emotion/react'; import { useState } from 'react'; import { Link } from 'react-router-dom'; +import { getPlaceBasedArea } from '@/apis/public/main'; import LoginModal from '@/components/LoginModal'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; import { COLORS, FONTS } from '@/styles/constants'; +import { PlaceBasedAreaItem } from '@/types/main'; import { cardContainer, scrollContainer } from '../styles/main'; import TravelCard from './TravelCard'; interface NearbyTravelProps { isLoggedIn: boolean; - region?: string; // prop 타입 수정 + region?: string; + favoriteList?: number[]; } -const NearbyTravel = ({ isLoggedIn, region }: NearbyTravelProps) => { +const NearbyTravel = (props: NearbyTravelProps) => { + const { isLoggedIn, region, favoriteList } = props; const [activateModal, setActivateModal] = useState(false); + const [placeList, setPlaceList] = useState([]); const closeModal = () => { setActivateModal(false); @@ -24,33 +30,39 @@ const NearbyTravel = ({ isLoggedIn, region }: NearbyTravelProps) => { setActivateModal(true); }; + useAsyncEffect(async () => { + if (!region) return; + const placeList = await getPlaceBasedArea({ + region: region || '서울', + }); + setPlaceList(placeList === '' ? [] : placeList.item); + }, [region]); + return (
-

{isLoggedIn && region} 주변 갈 만한 여행지 🗺️

+

+ {isLoggedIn && (region || '서울')} 주변 갈 만한 여행지 🗺️ +

{isLoggedIn ? ( <>
-
  • - - - - -
  • +
      + {placeList.map( + ({ title, addr1, addr2, contentid, firstimage }) => ( + + ), + )} +
    - - {region} 여행지 둘러보기 + + {region || '서울'} 여행지 둘러보기 ) : ( diff --git a/src/views/Main/components/TravelCard.tsx b/src/views/Main/components/TravelCard.tsx index b6675d5..8fd618e 100644 --- a/src/views/Main/components/TravelCard.tsx +++ b/src/views/Main/components/TravelCard.tsx @@ -1,41 +1,68 @@ import { css } from '@emotion/react'; +import { Link } from 'react-router-dom'; -import { HeartMonoIcon, PinLocationMonoIcon } from '@/assets/icon'; +import { HeartFillMonoIcon, PinLocationMonoIcon } from '@/assets/icon'; import { COLORS, FONTS } from '@/styles/constants'; interface TravelCardProps { + contentid: string; name: string; address: string; + imgUrl: string; + isHeart: boolean; } const TravelCard = (props: TravelCardProps) => { - const { name, address } = props; + const { contentid, name, address, imgUrl, isHeart } = props; + return ( -
      - -

      {name}

      -
      - -
      {address}
      + +
      +
      {isHeart && }
      +

      {name}

      +
      + +
      {address}
      +
      -
    + ); }; export default TravelCard; -const card = css` +const card = (imgUrl: string) => css` + position: relative; + + height: 24.8rem; + border-radius: 1.2rem; + + background-color: ${COLORS.gray4}; + background-position: center center; + background-size: cover; + background-image: url(${imgUrl}); + + min-width: 23.2rem; +`; + +const background = css` display: flex; flex-direction: column; + position: absolute; + top: 0; + left: 0; - width: 21.2rem; height: 24.8rem; padding: 1.6rem; border-radius: 1.2rem; - background-color: ${COLORS.brand1}; + background: linear-gradient( + 180deg, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 34%) 100% + ); + + min-width: 23.2rem; `; const heart = css` @@ -50,11 +77,19 @@ const nameCss = css` `; const locationCss = css` + overflow: hidden; + color: ${COLORS.white}; + white-space: nowrap; + text-overflow: ellipsis; + ${FONTS.Small2}; `; const addressContainer = css` display: flex; gap: 0.3rem; + align-items: center; + + margin-top: 0.2rem; `; diff --git a/src/views/Main/pages/MainPage.tsx b/src/views/Main/pages/MainPage.tsx index 1cbff82..4566093 100644 --- a/src/views/Main/pages/MainPage.tsx +++ b/src/views/Main/pages/MainPage.tsx @@ -1,8 +1,9 @@ import { css } from '@emotion/react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import getUserData from '@/apis/supabase/getUserData'; import MenuBar from '@/components/MenuBar'; +import { useAsyncEffect } from '@/hooks/use-async-effect'; import { COLORS, FONTS } from '@/styles/constants'; import { UserDataResponse } from '@/types/userAPI'; @@ -12,22 +13,17 @@ import RecommendedTravel from '../components/RecommendedTravel'; const MainPage = () => { const [userData, setUserData] = useState(null); - const isLoggedIn = sessionStorage.getItem('kakao_id'); - useEffect(() => { - const fetchData = async () => { - if (!isLoggedIn) return; - - try { - const response = await getUserData(Number(isLoggedIn)); - setUserData(response); - } catch (err) { - throw new Error('오류가 발생했습니다'); - } - }; + useAsyncEffect(async () => { + if (!isLoggedIn) return; - fetchData(); + try { + const response = await getUserData(Number(isLoggedIn)); + setUserData(response); + } catch (err) { + throw new Error('오류가 발생했습니다'); + } }, [isLoggedIn]); return ( @@ -46,6 +42,7 @@ const MainPage = () => {
    diff --git a/src/views/Main/styles/main.ts b/src/views/Main/styles/main.ts index 88b2129..bd47de4 100644 --- a/src/views/Main/styles/main.ts +++ b/src/views/Main/styles/main.ts @@ -2,13 +2,14 @@ import { css } from '@emotion/react'; export const scrollContainer = css` width: 100%; - overflow-x: scroll; + overflow-x: auto; `; + export const cardContainer = css` display: flex; gap: 1.2rem; - width: fit-content; + width: 100%; margin-top: 1.6rem; margin-left: 2rem; `; diff --git a/src/views/Search/components/Result/SearchResult.tsx b/src/views/Search/components/Result/SearchResult.tsx index f463b94..7b66a45 100644 --- a/src/views/Search/components/Result/SearchResult.tsx +++ b/src/views/Search/components/Result/SearchResult.tsx @@ -4,10 +4,10 @@ import { MutableRefObject } from 'react'; import { BigInfoIcon } from '@/assets/icon'; import PlaceCard from '@/components/PlaceCard'; import { COLORS, FONTS } from '@/styles/constants'; -import { SearchResItem } from '@/types/search'; +import { SearchItem } from '@/types/search'; interface SearchResultProps { - placeList: SearchResItem[]; + placeList: SearchItem[]; targetElement: MutableRefObject; loading: boolean; } diff --git a/src/views/Search/components/SearchBar/RelatedWordList.tsx b/src/views/Search/components/SearchBar/RelatedWordList.tsx index 4ee6965..73f38aa 100644 --- a/src/views/Search/components/SearchBar/RelatedWordList.tsx +++ b/src/views/Search/components/SearchBar/RelatedWordList.tsx @@ -3,11 +3,11 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { SearchMonoIcon } from '@/assets/icon'; import { COLORS, FONTS } from '@/styles/constants'; -import { SearchResItem } from '@/types/search'; +import { SearchItem } from '@/types/search'; interface RelatedWordListProps { searchWord: string; - relatedWordList: SearchResItem[]; + relatedWordList: SearchItem[]; handleSearchInputValue: (value: string) => void; } diff --git a/src/views/Search/components/SearchBar/SearchBarContainer.tsx b/src/views/Search/components/SearchBar/SearchBarContainer.tsx index e5a4c91..fd5d664 100644 --- a/src/views/Search/components/SearchBar/SearchBarContainer.tsx +++ b/src/views/Search/components/SearchBar/SearchBarContainer.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { SearchResItem } from '@/types/search'; +import { SearchItem } from '@/types/search'; import { useDebounceGetWordList } from '../../hooks/use-debounce-get-word-list'; import RelatedWordList from './RelatedWordList'; @@ -16,7 +16,7 @@ const SearchBarContainer = (props: SearchBarContainerProps) => { const searchInputRef = useRef(null); - const [relatedWordList, setRelatedWordList] = useState([]); + const [relatedWordList, setRelatedWordList] = useState([]); const debounceGetWordList = useDebounceGetWordList(setRelatedWordList); diff --git a/src/views/Search/hooks/use-debounce-get-word-list.ts b/src/views/Search/hooks/use-debounce-get-word-list.ts index 301fbc1..aaf5642 100644 --- a/src/views/Search/hooks/use-debounce-get-word-list.ts +++ b/src/views/Search/hooks/use-debounce-get-word-list.ts @@ -2,10 +2,10 @@ import { debounce } from 'lodash'; import { Dispatch, SetStateAction } from 'react'; import { getSearchKeyword } from '@/apis/public/search'; -import { SearchResItem } from '@/types/search'; +import { SearchItem } from '@/types/search'; export const useDebounceGetWordList = ( - setRelatedWordList: Dispatch>, + setRelatedWordList: Dispatch>, ) => debounce(async (searchWord: string) => { const wordList = await getSearchKeyword({ diff --git a/src/views/Search/pages/SearchResultPage.tsx b/src/views/Search/pages/SearchResultPage.tsx index 6003ef2..4d752de 100644 --- a/src/views/Search/pages/SearchResultPage.tsx +++ b/src/views/Search/pages/SearchResultPage.tsx @@ -7,7 +7,7 @@ import { SearchSetIcon } from '@/assets/icon'; import MenuBar from '@/components/MenuBar'; import { useInfiniteScroll } from '@/hooks/use-infinite-scroll'; import { COLORS, FONTS } from '@/styles/constants'; -import { SearchResItem } from '@/types/search'; +import { SearchItem } from '@/types/search'; import { isGuideShown } from '@/utils/storageHideGuide'; import Guide from '../components/Result/Guide'; @@ -30,7 +30,7 @@ const SearchResultPage = () => { ); // modal, bottom sheet state - const [placeList, setPlaceList] = useState([]); + const [placeList, setPlaceList] = useState([]); const [showGuide, setShowGuide] = useState(() => isGuideShown(STORAGE_KEY.hideSearchGuide), );