Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 식품 필터 선택 후 선택한 필터 보여주는 기능 추가 #461

Merged
merged 15 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions frontend/src/components/@common/Header/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { styled } from 'styled-components';

import BottomDropIcon from '@/assets/svg/bottom_drop_icon.svg';
import ZipgoLogo from '@/assets/svg/zipgo_logo_light.svg';
import { usePetProfile } from '@/context/petProfile/PetProfileContext';
import { useAuth } from '@/hooks/auth';
Expand Down Expand Up @@ -29,7 +28,6 @@ const UserProfile = () => {
) : (
<RegisterPetText>여기를 눌러 반려견을 등록해주세요.</RegisterPetText>
)}
<Chevron src={BottomDropIcon} alt="반려견 등록 화살표" />
</UserInfoContainer>
</Dialog.Trigger>
) : (
Expand Down Expand Up @@ -75,8 +73,3 @@ const Logo = styled.img`
width: 11.3rem;
height: 3.6rem;
`;

const Chevron = styled.img`
width: 1.2rem;
height: 0.6rem;
`;
2 changes: 1 addition & 1 deletion frontend/src/components/Food/BrandBlock/BrandBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const BrandBlock = (brandBlockProps: BrandBlockProps) => {
{onResearchCenterTip && (
<ToolTip
showBubbleOnly
content="반려동물 업계는 사료 품질과 판매량간의 상관관계가 없는데요, 마케팅 만으로는 살아남을 수 없기 때문에 역사가 오래됐는지가 중요한 지표랍니다!"
content="브랜드에 전용 연구센터가 있다는 것은 사후 AS에서도 단순히 교환 처리나 보상 처리를 하는 것이 아니라 안전성 검사, 독성 검사, 영양소 분석을 통해 문제의 원인까지 면밀히 파악해서 재발을 방지할 수 있는 능력까지 갖췄다는 것을 의미해요!"
title="왜 연구센터 여부가 중요한가요?"
left="-24.5rem"
edgeLeft="23.5rem"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,33 @@ import type { KeywordEn } from '@/types/food/client';
import { translateKeyword } from '@/utils/food';
import { getComputedStyleOfSC } from '@/utils/styled-components';

import FilterSelectionDisplay from '../FilterSelectionDisplay/FilterSelectionDisplay';
import BrandFilterList from './BrandFilterList/BrandFilterList';
import FunctionalityFilterList from './FunctionalityFilterList/FunctionalityFilterList';
import MainIngredientsFilterList from './MainIngredientsFilterList/MainIngredientsFilterList';
import NutritionStandardsFilterList from './NutritionStandardsFilterList/NutritionStandardsFilterList';

const FilterBottomSheet = () => (
<FoodFilterProvider>
<Dialog>
<Dialog.Trigger asChild>
<DialogTrigger type="button">
<FilterTriggerIcon src={SettingsIcon} alt="필터 버튼 아이콘" />
<span>필터</span>
</DialogTrigger>
</Dialog.Trigger>
<Dialog.Portal>
<QueryBoundary>
<Dialog.BackDrop />
<Dialog.Content asChild>
{({ openHandler }) => <KeywordContent toggleDialog={openHandler} />}
</Dialog.Content>
</QueryBoundary>
</Dialog.Portal>
</Dialog>
<FilterDialogAndFilterDisplayContainer>
<Dialog>
<Dialog.Trigger asChild>
<DialogTrigger type="button">
<FilterTriggerIcon src={SettingsIcon} alt="필터 버튼 아이콘" />
<span>필터</span>
</DialogTrigger>
</Dialog.Trigger>
<Dialog.Portal>
<QueryBoundary>
<Dialog.BackDrop />
<Dialog.Content asChild>
{({ openHandler }) => <KeywordContent toggleDialog={openHandler} />}
</Dialog.Content>
</QueryBoundary>
</Dialog.Portal>
</Dialog>
<FilterSelectionDisplay />
</FilterDialogAndFilterDisplayContainer>
</FoodFilterProvider>
);

Expand Down Expand Up @@ -117,6 +121,10 @@ const KeywordContent = (props: KeywordContentProps) => {

export default FilterBottomSheet;

const FilterDialogAndFilterDisplayContainer = styled.div`
display: flex;
`;

const Layout = styled.div`
position: fixed;
z-index: 1001;
Expand All @@ -141,6 +149,7 @@ const DialogTrigger = styled.button`
gap: 0.4rem;
align-items: center;

width: 8.8rem;
padding: 1rem 1.6rem;

background-color: transparent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { styled } from 'styled-components';

import { useFilterSelectionDisplay } from '@/hooks/food/useFilterSelectionDisplay';
import { KeywordEn } from '@/types/food/client';

const FilterSelectionDisplay = () => {
const { filterListQueryString, removeFilter } = useFilterSelectionDisplay();

return (
<SelectedFilterList>
{Object.entries(filterListQueryString).map(([category, values]) =>
values.split(',').map(value => (
<SelectedFilterItem key={value}>
{value}
<FilterToggleButton
type="button"
aria-label={`${value}필터 선택 해제`}
onClick={() => removeFilter(category as KeywordEn, value)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x 버튼이 너무 작아서 누르기 불편해요 😭
SelectedFilterItem에 등록하는건 어떨까요!?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 사용하고 오버로딩중인 Object.keys 처럼

global.d.tsObject.entries도 추가하고 아래와 같이 사용한다면

Object.entries(invariantOf(filterListQueryString)).map(([category, values]) =>{
  ...
})

타입 단언 없이 추론이 가능할 것 같아요!

>
x
</FilterToggleButton>
</SelectedFilterItem>
)),
)}
</SelectedFilterList>
);
};

export default FilterSelectionDisplay;

const SelectedFilterList = styled.ul`
scrollbar-width: none;

overflow-x: scroll;
display: flex;
gap: 0.4rem;
align-items: center;

width: calc(100% - 8.8rem - 1rem);
margin-left: 1rem;

&::-webkit-scrollbar {
width: 0;
height: 0;
}
`;

const SelectedFilterItem = styled.li`
overflow: hidden;
flex-shrink: 0;

height: 3.2rem;
padding: 0.4rem;

font-size: 1.2rem;
font-weight: 500;
line-height: 2.4rem;
color: ${({ theme }) => theme.color.grey400};
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
`;

const FilterToggleButton = styled.button`
cursor: pointer;

display: inline-block;

margin-left: 0.4rem;

color: ${({ theme }) => theme.color.grey300};

background: none;
border: none;
`;
2 changes: 1 addition & 1 deletion frontend/src/components/Food/FoodList/FoodList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { styled } from 'styled-components';

import { useInfiniteFoodListScroll } from '@/hooks/food';
import { useInfiniteFoodListScroll } from '@/hooks/food/useInfiniteFoodListScroll';

import FoodItem from '../FoodItem/FoodItem';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const BannerWrapper = styled.div`
font-weight: 500;
line-height: 2.4rem;
color: ${({ theme }) => theme.color.white};
letter-spacing: -0.7px;

img {
width: 100px;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/context/food.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable no-spaced-func */
import { createContext, useContext, useMemo } from 'react';

import { useFoodListFilter } from '@/hooks/food';
import { useFoodListFilter } from '@/hooks/food/useFoodListFilter';
import type { KeywordEn } from '@/types/food/client';
import { getValidProps, PropsWithRenderProps } from '@/utils/compound';

Expand Down
33 changes: 33 additions & 0 deletions frontend/src/hooks/food/useFilterSelectionDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { KEYWORD_EN } from '@/constants/food';
import { useFoodFilterContext } from '@/context/food';
import { generateQueryString } from '@/router/routes';
import { KeywordEn } from '@/types/food/client';

import useEasyNavigate from '../@common/useEasyNavigate';
import useValidQueryString from '../common/useValidQueryString';

export const useFilterSelectionDisplay = () => {
const { replaceQueryString } = useEasyNavigate();
const { toggleFilter } = useFoodFilterContext();
const filterListQueryString = useValidQueryString(KEYWORD_EN);

const removeFilter = (category: KeywordEn, value: string) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입명과 일치하도록 categorykeyword로 변경하는 것은 어떨까요?

let newFilterString;

if (filterListQueryString[category]?.includes(`,${value}`)) {
newFilterString = filterListQueryString[category]?.replace(`,${value}`, '');
} else if (filterListQueryString[category]?.includes(`${value},`)) {
newFilterString = filterListQueryString[category]?.replace(`${value},`, '');
} else newFilterString = filterListQueryString[category]?.replace(value, '');

const newQueryString = generateQueryString({
...filterListQueryString,
[category]: newFilterString,
});

toggleFilter(category, value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 필터에서 선택된 상태와 url의 쿼리스트링이 일치하지 않는 현상이 발생하는데요!

쿼리스트링이 변경될 때 마다 그에 따라 필터 상태를 업데이트 하도록 변경하면 문제점도 해결되면서 28번 라인도 제거할 수 있을 것 같습니다!

replaceQueryString(newQueryString, { exclude: [] });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let newFilterString;
if (filterListQueryString[category]?.includes(`,${value}`)) {
newFilterString = filterListQueryString[category]?.replace(`,${value}`, '');
} else if (filterListQueryString[category]?.includes(`${value},`)) {
newFilterString = filterListQueryString[category]?.replace(`${value},`, '');
} else newFilterString = filterListQueryString[category]?.replace(value, '');
const newQueryString = generateQueryString({
...filterListQueryString,
[category]: newFilterString,
});
toggleFilter(category, value);
replaceQueryString(newQueryString, { exclude: [] });
const filterList = filterListQueryString[category]?.split(',');
if (!filterList) return;
if (filterList.includes(value)) {
filterList.splice(filterList.indexOf(value), 1);
const updatedQueryString = filterList.join(',');
const newQueryString = generateQueryString({
...filterListQueryString,
[category]: updatedQueryString,
});
replaceQueryString(newQueryString, { exclude: [] });
}

else... if문을 개선해 보았어요!

};

return { filterListQueryString, removeFilter };
};
26 changes: 26 additions & 0 deletions frontend/src/hooks/food/useFoodListFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState } from 'react';

import { initialSelectedFilterList } from '@/context/food';
import type { KeywordEn } from '@/types/food/client';
import { parseCheckList } from '@/utils/parseCheckList';

export const useFoodListFilter = () => {
const [selectedFilterList, setSelectedFilterList] = useState(initialSelectedFilterList);

const parsedSelectedFilterList = parseCheckList(selectedFilterList);

const toggleFilter = (keyword: KeywordEn, filter: string) => {
const targetFilterList = structuredClone(selectedFilterList)[keyword];
const selected = targetFilterList.has(filter);

selected ? targetFilterList.delete(filter) : targetFilterList.add(filter);

setSelectedFilterList(prev => ({ ...prev, [keyword]: new Set(targetFilterList) }));
};

const resetSelectedFilterList = () => {
setSelectedFilterList(initialSelectedFilterList);
};

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useEffect(() => {
    const newFilterList = Object.entries(invariantOf(filterListQueryString)).reduce(
      (newFilterList, [category, queryString]) => ({
        ...newFilterList,
        [category]: new Set(queryString?.split(',')),
      }),
      initialSelectedFilterList,
    );

    setSelectedFilterList(newFilterList);
  }, [Object.values(filterListQueryString).join()]);

이렇게 쿼리스트링이 변경될 때 필터 상태도 동기화하면 어떨까요?

return { selectedFilterList, parsedSelectedFilterList, toggleFilter, resetSelectedFilterList };
};
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef } from 'react';

import { initialSelectedFilterList } from '@/context/food';
import useValidQueryString from '@/hooks/common/useValidQueryString';
import { useFoodListInfiniteQuery } from '@/hooks/query/food';
import type { KeywordEn } from '@/types/food/client';
import { parseCheckList } from '@/utils/parseCheckList';

import useValidQueryString from './common/useValidQueryString';
import { useFoodListInfiniteQuery } from './query/food';

export const useFoodListFilter = () => {
const [selectedFilterList, setSelectedFilterList] = useState(initialSelectedFilterList);

const parsedSelectedFilterList = parseCheckList(selectedFilterList);

const toggleFilter = (keyword: KeywordEn, filter: string) => {
const targetFilterList = structuredClone(selectedFilterList)[keyword];
const selected = targetFilterList.has(filter);

selected ? targetFilterList.delete(filter) : targetFilterList.add(filter);

setSelectedFilterList(prev => ({ ...prev, [keyword]: new Set(targetFilterList) }));
};

const resetSelectedFilterList = () => {
setSelectedFilterList(initialSelectedFilterList);
};

return { selectedFilterList, parsedSelectedFilterList, toggleFilter, resetSelectedFilterList };
};

export const useInfiniteFoodListScroll = () => {
const queries = useValidQueryString<KeywordEn>([
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/getValidQueries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getArrayMutationMethod } from './getArrayMutationMethod';

export const getValidQueries = <T extends string>(search: string, queryKeys: Readonly<T[]>) =>
search
decodeURIComponent(search)
.replace(/^\?/, '')
.split('&')
.reduce<Partial<Record<T, string>>>((queries, keyValue, _, search) => {
Expand Down