diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a1e4524..28ffeaa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -13,6 +13,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'prettier', + 'plugin:react-hooks/recommended', ], overrides: [ { diff --git a/package.json b/package.json index 4859c4d..a00f670 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "stylelint-config-standard": "^36.0.1", "stylelint-order": "^6.0.4", "terser": "^5.31.1", - "typescript": "^5.2.2", + "typescript": "^5.5.4", "vite": "^5.3.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.3.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4deeff8..0ba6160 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ importers: specifier: ^5.31.1 version: 5.31.3 typescript: - specifier: ^5.2.2 + specifier: ^5.5.4 version: 5.5.4 vite: specifier: ^5.3.1 diff --git a/src/components/MenuBar.tsx b/src/components/MenuBar.tsx index 1132fb5..b615283 100644 --- a/src/components/MenuBar.tsx +++ b/src/components/MenuBar.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { useState } from 'react'; +import { memo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { @@ -20,7 +20,7 @@ const PATH_MATCH = [ { url: '/mypage', name: '마이', icon: }, ]; -const MenuBar = () => { +const MenuBar = memo(() => { const { pathname } = useLocation(); const firstPathname = `/${pathname.split('/')[1]}`; const navigate = useNavigate(); @@ -67,7 +67,9 @@ const MenuBar = () => { {activateModal && } ); -}; +}); + +MenuBar.displayName = 'MenuBar'; export default MenuBar; @@ -75,6 +77,7 @@ const navCss = css` display: flex; position: fixed; bottom: 1.6rem; + z-index: 999; width: 100vw; `; diff --git a/src/components/PageLoading.tsx b/src/components/PageLoading.tsx index 5992ccc..66bca8a 100644 --- a/src/components/PageLoading.tsx +++ b/src/components/PageLoading.tsx @@ -1,18 +1,9 @@ import { css } from '@emotion/react'; -import { useEffect } from 'react'; import { WhiteSpinnerGIF } from '@/assets/image'; import { COLORS, FONTS } from '@/styles/constants'; const PageLoading = () => { - useEffect(() => { - // 스크롤 방지 - document.body.style.overflow = 'hidden'; - - return () => { - document.body.style.overflow = 'auto'; - }; - }, []); return (
spinner @@ -32,10 +23,10 @@ const dataContainer = css` top: 0; z-index: 999; - width: 100dvw; width: 100%; - height: 100dvh; + width: 100dvw; height: 100%; + height: 100dvh; background-color: rgb(82 82 82 / 72%); `; diff --git a/src/components/PlaceCard.tsx b/src/components/PlaceCard.tsx index eaeb52a..3927867 100644 --- a/src/components/PlaceCard.tsx +++ b/src/components/PlaceCard.tsx @@ -10,6 +10,7 @@ import { import { COLORS, FONTS } from '@/styles/constants'; interface PlaceCardProps { + idx: number; placeName: string; address: string; imgSrc: string; @@ -30,6 +31,7 @@ interface PlaceCardProps { const PlaceCard = (props: PlaceCardProps) => { const { + idx, placeName, address, imgSrc, @@ -47,7 +49,13 @@ const PlaceCard = (props: PlaceCardProps) => { }; return ( - + + 10 ? 'lazy' : 'eager'} + css={imgCss} + />
@@ -83,7 +81,7 @@ const section2Css = css` `; const buttonCss = css` - position: absolute; + position: fixed; top: 2.5rem; right: 2.4rem; diff --git a/src/views/Detail/components/review/ReviewCard.tsx b/src/views/Detail/components/review/ReviewCard.tsx index b1a7a9e..aa26036 100644 --- a/src/views/Detail/components/review/ReviewCard.tsx +++ b/src/views/Detail/components/review/ReviewCard.tsx @@ -21,10 +21,10 @@ const ReviewCard = (props: ReviewResponse) => { const descriptionRef = useRef(null); useEffect(() => { + if (!descriptionRef.current) return; + if ( - descriptionRef.current && - descriptionRef.current?.scrollHeight > - descriptionRef.current?.offsetHeight + descriptionRef.current.scrollHeight > descriptionRef.current.offsetHeight ) { setIsMoreButton(true); } diff --git a/src/views/Main/components/ReviewCard.tsx b/src/views/Main/components/ReviewCard.tsx index 6407930..dbee823 100644 --- a/src/views/Main/components/ReviewCard.tsx +++ b/src/views/Main/components/ReviewCard.tsx @@ -60,8 +60,6 @@ const card = css` const placeImg = css` height: 16.4rem; - background-color: pink; - border-top-left-radius: 1.2rem; border-top-right-radius: 1.2rem; `; diff --git a/src/views/Mypage/components/FavoritePlaceList.tsx b/src/views/Mypage/components/FavoritePlaceList.tsx index 8a175ba..f31b04b 100644 --- a/src/views/Mypage/components/FavoritePlaceList.tsx +++ b/src/views/Mypage/components/FavoritePlaceList.tsx @@ -24,9 +24,10 @@ const FavoritePlaceList = (props: placeListProps) => { return (
    - {cardInfoList.map((item) => ( + {cardInfoList.map((item, idx) => (
  • { handleSetShowGuide(false); }; + useEffect(() => { + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.overflow = 'auto'; + }; + }, []); + return (
    diff --git a/src/views/Search/components/Result/RenderResult.tsx b/src/views/Search/components/Result/RenderResult.tsx new file mode 100644 index 0000000..6e12591 --- /dev/null +++ b/src/views/Search/components/Result/RenderResult.tsx @@ -0,0 +1,94 @@ +import { css } from '@emotion/react'; + +import { BigInfoIcon } from '@/assets/icon'; +import { DefaultImage } from '@/assets/image'; +import PlaceCard from '@/components/PlaceCard'; +import { MAP_FACILITIES_API_KEY } from '@/constants/facilities'; +import { COLORS, FONTS } from '@/styles/constants'; +import { SearchItem } from '@/types/search'; + +import { getFilterList } from '../../constants/category'; +import { FilterFacilities, filterState } from '../../types/category'; + +const NoResultView = () => ( +
    + +
    검색 결과가 없어요
    +

    + 검색 필터를 바꾸거나 +
    + 다른 여행지를 검색해보세요! +

    +
    +); + +interface RenderResultProps { + filterState: filterState; + heartList: number[]; + filterIndexInfo: Record; + placeList: Record; + loading: boolean; +} + +const RenderResult = (props: RenderResultProps) => { + const { filterState, filterIndexInfo, placeList, loading, heartList } = props; + + const filterList = getFilterList(filterState); + const renderPlaceList = + filterList.length > 0 + ? Array.from( + filterList.reduce((acc, filter, idx) => { + if (idx === 0) return acc; + const curSet = new Set( + filterIndexInfo[MAP_FACILITIES_API_KEY[filter]], + ); + return new Set([...acc].filter((item) => curSet.has(item))); + }, new Set(filterIndexInfo[MAP_FACILITIES_API_KEY[filterList[0]]])), + ) + : Object.keys(placeList); + + if (!loading && renderPlaceList.length === 0) return ; + + return renderPlaceList.map((contentid, idx) => { + const { title, addr1, addr2, firstimage, firstimage2 } = + placeList[contentid]; + return ( +
  • + +
  • + ); + }); +}; + +export default RenderResult; + +const noResultContainerCss = css` + display: flex; + align-items: center; + flex-direction: column; + + margin: 6rem 0 1.2rem; +`; + +const noResultTitleCss = css` + margin: 2rem 0 0.8rem; + + color: ${COLORS.gray9}; + text-align: center; + + ${FONTS.Body2}; +`; + +const noResultInfoCss = css` + color: ${COLORS.brand1}; + text-align: center; + ${FONTS.Small1}; +`; diff --git a/src/views/Search/components/Result/ResultContainer.tsx b/src/views/Search/components/Result/ResultContainer.tsx new file mode 100644 index 0000000..04fb9d6 --- /dev/null +++ b/src/views/Search/components/Result/ResultContainer.tsx @@ -0,0 +1,146 @@ +import { css } from '@emotion/react'; +import _ from 'lodash'; +import { memo, MutableRefObject, useRef, useState } from 'react'; + +import { getBarrierFreeInfo, getSearchKeyword } from '@/apis/public/search'; +import Loading from '@/components/Loading'; +import PageLoading from '@/components/PageLoading'; +import { useInfiniteScroll } from '@/hooks/use-infinite-scroll'; +import { SearchItem } from '@/types/search'; + +import { INITIAL_FILTER_INDEX_INFO } from '../../constants/category'; +import { FilterFacilities, filterState } from '../../types/category'; +import RenderResult from './RenderResult'; + +interface ResultContainerProps { + searchWord: string; + filterState: filterState; + heartList: number[]; +} + +const ResultContainer = memo((props: ResultContainerProps) => { + const { searchWord, ...restProps } = props; + + const [loading, setLoading] = useState(false); + + const [placeList, setPlaceList] = useState>({}); + const [filterIndexInfo, setFilterIndexInfo] = useState< + Record + >(_.cloneDeep(INITIAL_FILTER_INDEX_INFO)); + + const placeListRef = useRef(null); + + // 무한스크롤 + const handleObserver = async ( + observer: IntersectionObserver, + target: MutableRefObject, + page: MutableRefObject, + ) => { + setLoading(true); + const pageNo = page.current; + + try { + const items = await getSearchKeyword({ + pageNo, + numOfRows: 50, + MobileOS: 'ETC', + keyword: searchWord || '', + contentTypeId: 14, + }); + + if (items === '') { + if (pageNo === 0) { + setPlaceList({}); + } + target.current && observer.unobserve(target.current); + } else { + const newPlaceList = items.item.reduce( + (acc, item) => { + acc[item.contentid] = item; + return acc; + }, + {} as Record, + ); + + const promises = items.item.map(({ contentid }) => + getBarrierFreeInfo({ + MobileOS: 'ETC', + contentId: Number(contentid), + }), + ); + const promiseResult = await Promise.allSettled(promises); + + const updatedFilterIndexInfo = { ...filterIndexInfo }; + + promiseResult.forEach((result) => { + if (result.status === 'fulfilled' && result.value !== '') { + const item = result.value.item[0]; + const contentid = item.contentid; + + Object.entries(item).forEach(([facility, value]) => { + if (facility !== 'contentid' && value !== '') { + updatedFilterIndexInfo[facility as FilterFacilities].push( + contentid, + ); + } + }); + } + }); + setFilterIndexInfo(updatedFilterIndexInfo); + setPlaceList((prev) => ({ ...prev, ...newPlaceList })); + + page.current++; + } + } finally { + setLoading(false); + } + }; + + const targetElement = useInfiniteScroll({ + handleObserver, + deps: [], + }); + + return ( + <> + {/* 최초 로딩 */} + {loading && Object.keys(placeList).length === 0 && } + +
      + +
      + + {/* 무한스크롤 로딩 */} + {loading && Object.keys(placeList).length > 0 && } +
    + + ); +}); + +ResultContainer.displayName = 'ResultContainer'; + +export default ResultContainer; + +const containerCss = (placeLength: number) => css` + display: flex; + gap: 1.2rem; + flex-direction: column; + + height: ${placeLength > 0 ? 'calc(100vh - 11rem)' : 'fit-content'}; + padding: 1.6rem 2rem 0; + padding-bottom: 7rem; + overflow-y: scroll; +`; + +const lastTargetCss = (loading: boolean) => css` + position: ${loading ? 'fixed' : 'static'}; + bottom: ${loading ? '-10px' : ''}; + + width: 100%; + height: 1px; +`; diff --git a/src/views/Search/components/Result/SearchResult.tsx b/src/views/Search/components/Result/SearchResult.tsx deleted file mode 100644 index 88067bc..0000000 --- a/src/views/Search/components/Result/SearchResult.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { css } from '@emotion/react'; -import { MutableRefObject, useEffect, useRef, useState } from 'react'; - -import { BigInfoIcon } from '@/assets/icon'; -import { DefaultImage } from '@/assets/image'; -import Loading from '@/components/Loading'; -import PlaceCard from '@/components/PlaceCard'; -import { MAP_FACILITIES_API_KEY } from '@/constants/facilities'; -import { COLORS, FONTS } from '@/styles/constants'; -import { BarrierFreeItem, SearchItem } from '@/types/search'; - -import { getFilterList } from '../../constants/category'; -import { filterState } from '../../types/category'; - -interface SearchResultProps { - placeData: (SearchItem & BarrierFreeItem)[]; - targetElement: MutableRefObject; - loading: boolean; - filterState: filterState; - heartList: number[]; -} - -const SearchResult = (props: SearchResultProps) => { - const { placeData, targetElement, loading, filterState, heartList } = props; - const placeListRef = useRef(null); - - const [renderPlaceList, setRenderPlaceList] = useState< - (SearchItem & BarrierFreeItem)[] - >([]); - - useEffect(() => { - const filterList = getFilterList(filterState); - const renderPlaceList = placeData.filter((placeInfo) => { - return filterList.every( - (facility) => placeInfo[MAP_FACILITIES_API_KEY[facility]] !== '', - ); - }); - setRenderPlaceList(renderPlaceList); - }, [filterState, placeData]); - - return ( - <> -
      - {!loading && renderPlaceList.length === 0 ? ( -
      - -
      검색 결과가 없어요
      -

      - 검색 필터를 바꾸거나 -
      - 다른 여행지를 검색해보세요! -

      -
      - ) : ( - renderPlaceList.map( - ({ contentid, title, addr1, addr2, firstimage, firstimage2 }) => { - return ( -
    • - -
    • - ); - }, - ) - )} -
      - {loading && placeData.length > 0 && } -
    - - ); -}; - -export default SearchResult; - -const containerCss = (placeLength: number) => css` - display: flex; - gap: 1.2rem; - flex-direction: column; - - height: ${placeLength > 0 ? 'calc(100vh - 11rem)' : 'fit-content'}; - padding: 1.6rem 2rem 0; - padding-bottom: 7rem; - overflow-y: scroll; -`; - -const lastTargetCss = css` - width: 100%; - height: 1px; -`; - -const noResultContainerCss = css` - display: flex; - align-items: center; - flex-direction: column; - - margin: 6rem 0 1.2rem; -`; - -const noResultTitleCss = css` - margin: 2rem 0 0.8rem; - - color: ${COLORS.gray9}; - text-align: center; - - ${FONTS.Body2}; -`; - -const noResultInfoCss = css` - color: ${COLORS.brand1}; - text-align: center; - ${FONTS.Small1}; -`; diff --git a/src/views/Search/components/SearchBar/SearchBar.tsx b/src/views/Search/components/SearchBar/SearchBar.tsx index c350938..203daa0 100644 --- a/src/views/Search/components/SearchBar/SearchBar.tsx +++ b/src/views/Search/components/SearchBar/SearchBar.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { DebouncedFunc } from 'lodash'; -import { ChangeEvent, KeyboardEvent, RefObject, useState } from 'react'; +import { ChangeEvent, KeyboardEvent } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { ChevronLeftIcon, ResetXIcon } from '@/assets/icon'; @@ -8,7 +8,7 @@ import { COLORS, FONTS } from '@/styles/constants'; import { setStorageSearchWord } from '@/utils/storageSearchWord'; interface SearchBarProps { - searchInputRef: RefObject; + searchInput: string; debounceGetWordList: DebouncedFunc<(searchWord: string) => Promise>; resetRelatedWordList: () => void; initialWord?: string; @@ -17,7 +17,7 @@ interface SearchBarProps { const SearchBar = (props: SearchBarProps) => { const { - searchInputRef, + searchInput, debounceGetWordList, resetRelatedWordList, initialWord, @@ -27,10 +27,6 @@ const SearchBar = (props: SearchBarProps) => { const navigate = useNavigate(); const { pathname } = useLocation(); - const [showResetButton, setShowResetButton] = useState( - !!searchInputRef.current?.value || !!initialWord, - ); - const handleOnClickPrevButton = () => { navigate(-1); resetRelatedWordList(); @@ -38,29 +34,26 @@ const SearchBar = (props: SearchBarProps) => { const handleOnChange = (e: ChangeEvent) => { const { value } = e.currentTarget; + handleSearchInputValue(value); if (!value || initialWord === value) { resetRelatedWordList(); - setShowResetButton(false); return; } debounceGetWordList(value); - setShowResetButton(true); }; const handleOnClick = () => { - setShowResetButton(false); resetRelatedWordList(); handleSearchInputValue(''); }; const handleOnKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && searchInputRef.current) { - const { value } = searchInputRef.current; - setStorageSearchWord(value); + if (e.key === 'Enter' && searchInput) { + setStorageSearchWord(searchInput); resetRelatedWordList(); - navigate(`/search/${value}`, { + navigate(`/search/${searchInput}`, { replace: pathname.startsWith('/search/'), }); } @@ -72,14 +65,13 @@ const SearchBar = (props: SearchBarProps) => { - {showResetButton && ( + {searchInput && ( diff --git a/src/views/Search/components/SearchBar/SearchBarContainer.tsx b/src/views/Search/components/SearchBar/SearchBarContainer.tsx index fd5d664..a9d867a 100644 --- a/src/views/Search/components/SearchBar/SearchBarContainer.tsx +++ b/src/views/Search/components/SearchBar/SearchBarContainer.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { ReactNode, useCallback, useState } from 'react'; import { SearchItem } from '@/types/search'; @@ -14,46 +14,38 @@ interface SearchBarContainerProps { const SearchBarContainer = (props: SearchBarContainerProps) => { const { children, initialWord } = props; - const searchInputRef = useRef(null); - + const [searchInput, setSearchInput] = useState(initialWord ?? ''); const [relatedWordList, setRelatedWordList] = useState([]); const debounceGetWordList = useDebounceGetWordList(setRelatedWordList); const handleSearchInputValue = (value: string) => { - if (!searchInputRef.current) return; - searchInputRef.current.value = value; + setSearchInput(value); }; const resetRelatedWordList = useCallback(() => { setRelatedWordList([]); }, []); - useEffect(() => { - if (!searchInputRef.current || !initialWord) return; - searchInputRef.current.value = initialWord; - }, [initialWord]); - return ( <> - {initialWord !== searchInputRef.current?.value && - searchInputRef.current?.value && ( - - )} - {!initialWord && relatedWordList.length === 0 && children} - {initialWord && children} + {initialWord !== searchInput && searchInput && ( + + )} + + {children} ); }; diff --git a/src/views/Search/constants/category.ts b/src/views/Search/constants/category.ts index 2746e00..7d2b262 100644 --- a/src/views/Search/constants/category.ts +++ b/src/views/Search/constants/category.ts @@ -58,6 +58,37 @@ export const INITIAL_FILTER_STATE: filterState = { }, }; +export const INITIAL_FILTER_INDEX_INFO = { + wheelchair: [], + exit: [], + elevator: [], + restroom: [], + guidesystem: [], + blindhandicapetc: [], + signguide: [], + videoguide: [], + hearingroom: [], + hearinghandicapetc: [], + stroller: [], + lactationroom: [], + babysparechair: [], + infantsfamilyetc: [], + auditorium: [], + room: [], + handicapetc: [], + braileblock: [], + helpdog: [], + guidehuman: [], + audioguide: [], + bigprint: [], + brailepromotion: [], + parking: [], + route: [], + publictransport: [], + ticketoffice: [], + promotion: [], +}; + export const getFilterList = (filterState: filterState) => { return Object.values(filterState).flatMap((obj) => Object.entries(obj) @@ -67,19 +98,19 @@ export const getFilterList = (filterState: filterState) => { }; export const createInitialFilterState = (initialCategory: string[]) => { + const tempFilterObj = { ...INITIAL_FILTER_STATE }; + initialCategory.forEach((categoryItem) => { const targetCategory = Object.keys(MAP_UNIVERSAL_TYPE).find( (key) => MAP_UNIVERSAL_TYPE[key as category] === categoryItem, ); if (targetCategory) { - Object.keys(INITIAL_FILTER_STATE[targetCategory as category]).forEach( - (key) => { - INITIAL_FILTER_STATE[targetCategory as category][key] = true; - }, - ); + Object.keys(tempFilterObj[targetCategory as category]).forEach((key) => { + tempFilterObj[targetCategory as category][key] = true; + }); } }); - return INITIAL_FILTER_STATE; + return tempFilterObj; }; diff --git a/src/views/Search/pages/SearchResultPage.tsx b/src/views/Search/pages/SearchResultPage.tsx index e597c01..d02a462 100644 --- a/src/views/Search/pages/SearchResultPage.tsx +++ b/src/views/Search/pages/SearchResultPage.tsx @@ -1,21 +1,17 @@ import { css } from '@emotion/react'; -import { MutableRefObject, useEffect, useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { useCallback, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; -import { getBarrierFreeInfo, getSearchKeyword } from '@/apis/public/search'; import getUserData from '@/apis/supabase/getUserData'; import { SearchSetIcon } from '@/assets/icon'; import MenuBar from '@/components/MenuBar'; -import PageLoading from '@/components/PageLoading'; import { useAsyncEffect } from '@/hooks/use-async-effect'; -import { useInfiniteScroll } from '@/hooks/use-infinite-scroll'; import { COLORS, FONTS } from '@/styles/constants'; -import { BarrierFreeItem, SearchItem } from '@/types/search'; import { UserDataResponse } from '@/types/userAPI'; import { isGuideShown } from '@/utils/storageHideGuide'; import Guide from '../components/Result/Guide'; -import SearchResult from '../components/Result/SearchResult'; +import ResultContainer from '../components/Result/ResultContainer'; import FilterBottomSheet from '../components/Search/FilterBottomSheet'; import SearchBarContainer from '../components/SearchBar/SearchBarContainer'; import { @@ -27,16 +23,13 @@ import { STORAGE_KEY } from '../constants/localStorageKey'; import { category, filterState } from '../types/category'; const SearchResultPage = () => { - const { word: initialWord } = useParams(); + const { word: searchWord } = useParams(); - const { pathname } = useLocation(); const [userData, setUserData] = useState(null); - const [placeData, setPlaceData] = useState<(SearchItem & BarrierFreeItem)[]>( - [], - ); - const [filterState, setFilterState] = - useState(INITIAL_FILTER_STATE); + const [filterState, setFilterState] = useState({ + ...INITIAL_FILTER_STATE, + }); // modal, bottom sheet state const [showGuide, setShowGuide] = useState(() => @@ -44,13 +37,6 @@ const SearchResultPage = () => { ); const [isFilterOpen, setIsFilterOpen] = useState(false); - // state handling func - const [loading, setLoading] = useState(false); - - useEffect(() => { - setPlaceData([]); - }, [pathname]); - useAsyncEffect(async () => { const kakaoId = sessionStorage.getItem('kakao_id'); if (!kakaoId) return; @@ -60,71 +46,10 @@ const SearchResultPage = () => { setFilterState(createInitialFilterState(userData?.universal_type || [])); }, []); - // 무한스크롤 - const options = { - root: null, - rootMargin: '0px', - threshold: 0, - }; - - const handleObserver = async ( - observer: IntersectionObserver, - target: MutableRefObject, - page: MutableRefObject, - ) => { - setLoading(true); - const pageNo = page.current; - - try { - const items = await getSearchKeyword({ - pageNo, - numOfRows: 50, - MobileOS: 'ETC', - keyword: pathname.split('/')[2], - contentTypeId: 12, - }); - - if (items === '') { - if (pageNo === 0) setPlaceData([]); - target.current && observer.unobserve(target.current); - } else { - const placeData: (SearchItem & BarrierFreeItem)[] = []; - const promises = items.item.map(({ contentid }) => - getBarrierFreeInfo({ - MobileOS: 'ETC', - contentId: Number(contentid), - }), - ); - const promiseResult = await Promise.allSettled(promises); - promiseResult.forEach((result) => { - if (result.status === 'fulfilled' && result.value !== '') { - const item = result.value.item; - const targetPlace = items.item.find( - ({ contentid }) => contentid === item[0].contentid, - ); - if (!targetPlace) return; - placeData.push({ ...targetPlace, ...result.value.item[0] }); - } - }); - - setPlaceData((prev) => [...prev, ...placeData]); - page.current++; - } - } finally { - setLoading(false); - } - }; - - const targetElement = useInfiniteScroll({ - options, - handleObserver, - deps: [pathname], - }); - // 검색 가이드 - const handleSetShowGuide = (value: boolean) => { + const handleSetShowGuide = useCallback((value: boolean) => { setShowGuide(value); - }; + }, []); const openFilter = () => { setIsFilterOpen(true); @@ -139,7 +64,7 @@ const SearchResultPage = () => { }; // render - const selectedCategory = () => { + const selectedCategory = useMemo(() => { const category: string[] = []; Object.entries(filterState).forEach(([key, entries]) => { @@ -148,21 +73,19 @@ const SearchResultPage = () => { }); return category.join(', '); - }; + }, [filterState]); return ( <> - {loading && placeData.length === 0 && }
    - + - diff --git a/src/views/Search/types/category.ts b/src/views/Search/types/category.ts index 4c86424..ed3fc8b 100644 --- a/src/views/Search/types/category.ts +++ b/src/views/Search/types/category.ts @@ -1,2 +1,5 @@ +import { BarrierFreeItem } from '@/types/search'; + export type category = 'physical' | 'visual' | 'hearing' | 'infant'; export type filterState = Record>; +export type FilterFacilities = keyof Omit;