Skip to content

Commit

Permalink
[Feature] - 검색 결과 페이지 내 필터링 옵션 적용 (#565)
Browse files Browse the repository at this point in the history
* refactor(useInfiniteSearchTravelogues): api 명세 통합으로 인한 변경

useInfiniteTravelogues 내에서 검색 조건 까지 모두 처리하는 것으로 변경

* feat(TravelogueList): 여행기 검색 결과 내 필터링 ui 추가

* refactor: travelogues가 0개인 경우에도 FixedLayout으로 감싸는 것으로 변경

* fix(TravelogueList): 메인 페이지에 추가한 태그들이 검색 결과 페이지에서도 보이는 문제 해결
  • Loading branch information
jinyoung234 authored Oct 23, 2024
1 parent ed2c51b commit 0b3b3ee
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";

import { SPACING } from "@styles/tokens";
import theme from "@styles/theme";
import { PRIMITIVE_COLORS, SPACING } from "@styles/tokens";

export const Layout = styled.div`
display: flex;
flex-direction: column;
margin-top: ${SPACING.xxl};
min-height: calc(100vh - 16rem);
padding-top: 15rem;
`;

export const FixedLayout = styled.div`
display: flex;
flex-direction: column;
position: fixed;
top: 11rem;
z-index: 10;
width: 100%;
max-width: 48rem;
margin: 0 auto;
padding: ${({ theme }) => theme.spacing.m};
background-color: ${PRIMITIVE_COLORS.white};
gap: ${({ theme }) => theme.spacing.m};
`;

export const searchResultTextStyle = css`
Expand Down Expand Up @@ -36,3 +54,54 @@ export const MainPageTraveloguesList = styled.ul`
padding: 0 ${SPACING.m};
gap: ${SPACING.m};
`;

export const TagsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.s};
`;

export const SingleSelectionTagsContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing.s};
& > li {
cursor: pointer;
}
`;

export const MultiSelectionTagsContainer = styled.ul`
display: flex;
overflow: scroll hidden;
gap: ${({ theme }) => theme.spacing.s};
height: 3rem;
margin: 0 -${({ theme }) => theme.spacing.m};
padding: 0 ${({ theme }) => theme.spacing.m};
white-space: nowrap;
-webkit-overflow-scrolling: touch;
::-webkit-scrollbar {
display: none;
}
& > li {
cursor: pointer;
}
`;

export const OptionContainer = styled.button`
display: flex;
justify-content: space-between;
width: 100%;
`;

export const selectedOptionStyle = css`
color: ${theme.colors.primary};
`;

export const unselectedOptionStyle = css`
color: ${theme.colors.text.secondary};
`;
213 changes: 205 additions & 8 deletions frontend/src/components/pages/search/TravelogueList/TravelogueList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import { useEffect, useMemo } from "react";

import { css } from "@emotion/react";

import type { SearchType } from "@type/domain/travelogue";

import useInfiniteSearchTravelogues from "@queries/useInfiniteSearchTravelogues";
import useInfiniteTravelogues from "@queries/useInfiniteTravelogues";

import { Text } from "@components/common";
import {
Chip,
Icon,
SearchFallback,
SingleSelectionTagModalBottomSheet,
Text,
} from "@components/common";
import {
SORTING_OPTIONS,
SORTING_OPTIONS_MAP,
TRAVEL_PERIOD_OPTIONS,
TRAVEL_PERIOD_OPTIONS_MAP,
} from "@components/pages/main/MainPage.constants";
import TravelogueCard from "@components/pages/main/TravelogueCard/TravelogueCard";
import TravelogueCardSkeleton from "@components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton";

import { useDragScroll } from "@hooks/useDragScroll";
import useIntersectionObserver from "@hooks/useIntersectionObserver";
import useMultiSelectionTag from "@hooks/useMultiSelectionTag";
import useSingleSelectionTag from "@hooks/useSingleSelectionTag";

import { ERROR_MESSAGE_MAP } from "@constants/errorMessage";
import { STORAGE_KEYS_MAP } from "@constants/storage";

import { removeEmoji } from "@utils/removeEmojis";

import theme from "@styles/theme";

import EmptySearchResult from "./EmptySearchResult";
import * as S from "./TravelogueList.styled";
Expand All @@ -23,14 +45,187 @@ interface TravelogueListProps {
}

const TravelogueList = ({ keyword, searchType }: TravelogueListProps) => {
const { travelogues, status, fetchNextPage, isPaused, error } = useInfiniteSearchTravelogues(
const {
selectedTagIDs,
sortedTags,
multiSelectionTagAnimationKey,
handleClickTag,
resetMultiSelectionTag,
} = useMultiSelectionTag(STORAGE_KEYS_MAP.searchPageSelectedTagIDs);

const {
sorting,
travelPeriod,
singleSelectionAnimationKey,
resetSingleSelectionTags,
increaseSingleSelectionAnimationKey,
} = useSingleSelectionTag(
STORAGE_KEYS_MAP.searchPageSort,
STORAGE_KEYS_MAP.searchPageTravelPeriod,
);

const { scrollRef, handleMouseDown, handleMouseMove, handleMouseUp } =
useDragScroll<HTMLUListElement>();

const isTagsSelected = useMemo(() => {
return (
selectedTagIDs.length !== 0 ||
sorting.selectedOption !== "likeCount" ||
travelPeriod.selectedOption !== ""
);
}, [selectedTagIDs, sorting.selectedOption, travelPeriod.selectedOption]);

useEffect(() => {
increaseSingleSelectionAnimationKey();
}, [isTagsSelected, increaseSingleSelectionAnimationKey]);

const handleClickResetButton = () => {
resetMultiSelectionTag();
resetSingleSelectionTags();
};

const { travelogues, status, fetchNextPage, isPaused, error } = useInfiniteTravelogues({
selectedTagIDs,
selectedSortingOption: sorting.selectedOption,
selectedTravelPeriodOption: travelPeriod.selectedOption,
keyword,
searchType,
);
});

const { lastElementRef } = useIntersectionObserver(fetchNextPage);

const renderTags = () => {
return (
<>
{sorting.isModalOpen && (
<SingleSelectionTagModalBottomSheet
isOpen={sorting.isModalOpen}
onClose={sorting.handleCloseModal}
mainText="여행기 정렬을 선택해 주세요!"
>
{SORTING_OPTIONS.map((option, index) => (
<S.OptionContainer key={index} onClick={() => sorting.handleClickOption(option)}>
{option === sorting.selectedOption ? (
<>
<Text textType="detailBold" css={S.selectedOptionStyle}>
{SORTING_OPTIONS_MAP[option]}
</Text>
<Icon iconType="down-arrow" size="12" color={theme.colors.primary} />
</>
) : (
<Text textType="detail" css={S.unselectedOptionStyle}>
{SORTING_OPTIONS_MAP[option]}
</Text>
)}
</S.OptionContainer>
))}
</SingleSelectionTagModalBottomSheet>
)}

{travelPeriod.isModalOpen && (
<SingleSelectionTagModalBottomSheet
isOpen={travelPeriod.isModalOpen}
onClose={travelPeriod.handleCloseModal}
mainText="여행 기간을 선택해 주세요!"
>
{TRAVEL_PERIOD_OPTIONS.map((option, index) => (
<S.OptionContainer key={index} onClick={() => travelPeriod.handleClickOption(option)}>
{option === travelPeriod.selectedOption ? (
<>
<Text textType="detailBold" css={S.selectedOptionStyle}>
{TRAVEL_PERIOD_OPTIONS_MAP[option]}
</Text>
<Icon iconType="down-arrow" size="12" color={theme.colors.primary} />
</>
) : (
<Text textType="detail" css={S.unselectedOptionStyle}>
{TRAVEL_PERIOD_OPTIONS_MAP[option]}
</Text>
)}
</S.OptionContainer>
))}
</SingleSelectionTagModalBottomSheet>
)}

<S.TagsContainer>
<S.SingleSelectionTagsContainer>
{isTagsSelected && (
<Chip
key={`reset-${singleSelectionAnimationKey}`}
label={`초기화`}
isSelected={false}
onClick={handleClickResetButton}
iconPosition="left"
iconType="reset-icon"
/>
)}
<Chip
as="button"
aria-label="여행기 정렬"
key={`sorting-${singleSelectionAnimationKey}`}
label={SORTING_OPTIONS_MAP[sorting.selectedOption]}
isSelected={true}
onClick={sorting.handleOpenModal}
iconPosition="left"
iconType="sort-icon"
/>
<Chip
as="button"
aria-label="여행기 필터"
key={`travelPeriod-${singleSelectionAnimationKey}`}
label={
travelPeriod.selectedOption
? TRAVEL_PERIOD_OPTIONS_MAP[travelPeriod.selectedOption]
: "여행 기간"
}
iconPosition="right"
isSelected={travelPeriod.selectedOption !== ""}
onClick={travelPeriod.handleOpenModal}
/>
</S.SingleSelectionTagsContainer>

<S.MultiSelectionTagsContainer
ref={scrollRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
>
{sortedTags.map((tag, index) => {
const isSelected = selectedTagIDs.includes(tag.id);
const tagName = removeEmoji(tag.tag);

return (
<li key={`${tag.id}-${multiSelectionTagAnimationKey}`}>
<Chip
as="button"
key={`${tag.id}-${multiSelectionTagAnimationKey}`}
index={index}
label={tag.tag}
isSelected={isSelected}
onClick={() => handleClickTag(tag.id)}
aria-label={`${tagName} 태그`}
/>
</li>
);
})}
</S.MultiSelectionTagsContainer>
</S.TagsContainer>
</>
);
};

if (travelogues.length === 0 && status === "success") {
return <EmptySearchResult keyword={keyword} />;
return (
<S.Layout>
<S.FixedLayout>
{keyword && <Text textType="title">{`"${keyword}" 검색 결과`}</Text>}
{renderTags()}
</S.FixedLayout>
<S.SearchFallbackWrapper>
<SearchFallback title="휑" text="검색 결과가 없어요." />
</S.SearchFallbackWrapper>
</S.Layout>
);
}

if (status === "error") {
Expand All @@ -43,9 +238,11 @@ const TravelogueList = ({ keyword, searchType }: TravelogueListProps) => {

return (
<S.Layout>
{keyword && (
<Text css={S.searchResultTextStyle} textType="title">{`"${keyword}" 검색 결과`}</Text>
)}
<S.FixedLayout>
{keyword && <Text textType="title">{`"${keyword}" 검색 결과`}</Text>}
{renderTags()}
</S.FixedLayout>

<S.MainPageTraveloguesList>
{status === "pending" && (
<S.MainPageTraveloguesList>
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@ export const QUERY_KEYS_MAP = {
searchType,
keyword,
],
tag: (
selectedTagIDs: number[],
selectedSortingOption: SortingOption,
selectedTravelPeriodOption: TravelPeriodOption,
) => [
tag: ({
selectedTagIDs,
selectedSortingOption,
selectedTravelPeriodOption,
keyword,
searchType,
}: {
selectedTagIDs: number[];
selectedSortingOption: SortingOption;
selectedTravelPeriodOption: TravelPeriodOption;
keyword?: string;
searchType?: SearchType;
}) => [
...QUERY_KEYS_MAP.travelogue.all,
...selectedTagIDs,
selectedSortingOption,
selectedTravelPeriodOption,
keyword,
searchType,
],
likes: () => [...QUERY_KEYS_MAP.travelogue.all, "likes"],
},
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ export const STORAGE_KEYS_MAP = {
mainPageSort: "mainPageSort",
mainPageTravelPeriod: "mainPageTravelPeriod",
mainPageSelectedTagIDs: "mainPageSelectedTagIDs",
searchPageSort: "searchPageSort",
searchPageTravelPeriod: "searchPageTravelPeriod",
searchPageSelectedTagIDs: "searchPageSelectedTagIDs",
} as const;
Loading

0 comments on commit 0b3b3ee

Please sign in to comment.