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: 태그 검색 기능 구현 #64

Merged
merged 25 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
92c3b11
refactor: 검색 api에서 태그 검색일때의 endpoint 추가
xodms0309 Mar 23, 2024
34a3314
feat: 태그 검색 기능 구현
xodms0309 Mar 23, 2024
95c3679
Merge remote-tracking branch 'origin/feat/v2' into feat/issue-60
xodms0309 Mar 27, 2024
6f9c62e
feat: 검색결과 없을 때 화면 구현
xodms0309 Mar 27, 2024
c5626f1
feat: 태그 검색 결과 리스트 컴포넌트 구현
xodms0309 Mar 27, 2024
902dda0
feat: Text 컴포넌트에서 외부 className을 받을 수 있게 수정
xodms0309 Mar 30, 2024
aa3bc0d
feat: 검색 페이지에서 태그 검색 결과일 경우 분기처리
xodms0309 Mar 30, 2024
8709619
feat: 더보기 버튼을 리스트 컴포넌트로 위치 이동
xodms0309 Mar 30, 2024
453b953
feat: 태그 검색을 일반 검색과 분리
xodms0309 Mar 30, 2024
c1f3f7d
feat: 상품 검색 더보기 페이지 구현
xodms0309 Mar 30, 2024
56bcc3b
feat: 태그 검색을 했을 때 input value 스타일 변경
xodms0309 Mar 31, 2024
d66ad0e
style: 사용 안하는 코드 import 제거
xodms0309 Mar 31, 2024
a3922e8
feat: ProductSearchResultList 스토리북 추가
xodms0309 Mar 31, 2024
e67a69a
fix: 검색이 되었을 때 resetQuery를 하도록 수정
xodms0309 Mar 31, 2024
f7199a5
style: fix lint error
xodms0309 Apr 7, 2024
fbc062e
feat: 태그 검색 페이지 분리
xodms0309 Apr 7, 2024
d0c4582
feat: 말풍선 아이콘 교체
xodms0309 Apr 7, 2024
fbdb3aa
feat: ProductItem 디자인 수정
xodms0309 Apr 7, 2024
185455e
Merge remote-tracking branch 'origin/feat/v2' into feat/issue-60
xodms0309 Apr 7, 2024
281395c
Merge remote-tracking branch 'origin/feat/v2' into feat/issue-60
xodms0309 Apr 8, 2024
ca2d24f
feat: ProductSearchResultList -> ProductSearchResultPreviewList로 이름 변경
xodms0309 Apr 8, 2024
e40319b
feat: 버튼 스타일 변경
xodms0309 Apr 8, 2024
5a08969
feat: ProductOverviewList로 교체
xodms0309 Apr 8, 2024
e423552
fix: 상품 상세 경로 수정
xodms0309 Apr 9, 2024
12546c7
refactor: 인자로 endpoint를 넘겨주게끔 변경
xodms0309 Apr 11, 2024
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
Binary file added src/assets/search-notfound.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/components/Common/Text/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import cx from 'classnames';

import { text } from './text.css';
import type { TextElement, TextVariants, OverridableComponentPropsWithoutRef } from './text.types';

Expand All @@ -8,10 +10,11 @@ const Text = <T extends TextElement = 'p'>({
size = 'body',
weight = 'regular',
color = 'default',
className,
...props
}: TextProps<T>) => {
return (
<p className={text({ color, size, weight })} {...props}>
<p className={cx(text({ color, size, weight }), className)} {...props}>
{children}
</p>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/react';
import ProductSearchResultList from './ProductSearchResultList';

const meta: Meta<typeof ProductSearchResultList> = {
title: 'search/ProductSearchResultList',
component: ProductSearchResultList,
args: {
searchQuery: '꼬북칩',
},
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useRef } from 'react';
import { Link } from 'react-router-dom';

import { container } from './productSearchResultList.css';
import { container, showMoreButton } from './productSearchResultList.css';
import SearchNotFound from '../SearchNotFound/SearchNotFound';

import { ProductOverviewItem } from '@/components/Product';
import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteProductSearchResultsQuery } from '@/hooks/queries/search';
import displaySlice from '@/utils/displaySlice';

interface ProductSearchResultListProps {
searchQuery: string;
Expand All @@ -22,23 +24,26 @@ const ProductSearchResultList = ({ searchQuery }: ProductSearchResultListProps)
}

const products = searchResponse.pages.flatMap((page) => page.products);
const productToDisplay = displaySlice(true, products);

if (products.length === 0) {
return <p>검색한 상품을 찾을 수 없습니다.</p>;
return <SearchNotFound />;
}

return (
<>
<ul className={container}>
{products.map(({ id, categoryType, image, name, price, averageRating }) => (
{productToDisplay.map(({ id, categoryType, image, name, price, averageRating }) => (
<li key={id}>
<Link to={`${PATH.PRODUCT_LIST}/${categoryType}/${id}`}>
<ProductOverviewItem image={image} name={name} price={price} rate={averageRating} />
</Link>
</li>
))}
</ul>
Copy link
Contributor

@hae-on hae-on Apr 7, 2024

Choose a reason for hiding this comment

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

이 컴포넌트의 이름을 보고 떠오른거는 전체 상품 검색한 리스트를 다 뽑아오는건가 생각이 드는데
실제로는 전체중에 2개, 더보기 버튼 이렇게 나오더라구요.

그래서 아예 이 컴포넌트 이름을 변경했으면 좋겠어요!

밑에도 적어놨는데 이거 제 레시피 PR에 있는 ProductOverviewList 재사용하면 좋을 거 같은데,
중간에 자르는게 들어가있는게 걸리네요...

Copy link
Member Author

Choose a reason for hiding this comment

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

그냥 products에 productsToDisplay 넘겨주는거 어때여?! 컴포넌트 이름은 변경할게요!

<div ref={scrollRef} aria-hidden />
<Link to={`${PATH.SEARCH}/products?query=${searchQuery}`} className={showMoreButton}>
더보기
</Link>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,16 @@ export const container = style({
flexDirection: 'column',
gap: 20,
});

export const showMoreButton = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: 38,
padding: '9px 0',
margin: '20px 0',
background: '#efefef',
fontSize: 14,
borderRadius: 6,
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Link, Text } from '@fun-eat/design-system';
import { useRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { styled } from 'styled-components';

import SearchNotFound from '../SearchNotFound/SearchNotFound';

import { RecipeItem } from '@/components/Recipe';
import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
Expand All @@ -20,15 +21,15 @@ const RecipeSearchResultList = ({ searchQuery }: RecipeSearchResultListProps) =>
const recipes = searchResponse.pages.flatMap((page) => page.recipes);

if (recipes.length === 0) {
return <Text>검색한 꿀조합을 찾을 수 없습니다.</Text>;
return <SearchNotFound />;
}

return (
<>
<RecipeSearchResultListContainer>
{recipes.map((recipe) => (
<li key={recipe.id}>
<Link as={RouterLink} to={`${PATH.RECIPE}/${recipe.id}`}>
<Link to={`${PATH.RECIPE}/${recipe.id}`}>
<RecipeItem recipe={recipe} />
</Link>
</li>
Expand Down
29 changes: 22 additions & 7 deletions src/components/Search/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { ComponentPropsWithRef, ForwardedRef } from 'react';
import { forwardRef } from 'react';

import { iconWrapperButton, inputContainer, searchInput } from './searchInput.css';
import { iconWrapperButton, inputContainer, searchInput, tagInputWrapper } from './searchInput.css';

import { SvgIcon } from '@/components/Common';
import { Text, SvgIcon } from '@/components/Common';

interface SearchInputProps extends ComponentPropsWithRef<'input'> {
isInputSubmitted: boolean;
isTagSearch: boolean;
}

const SearchInput = forwardRef(
({ value, isInputSubmitted, ...props }: SearchInputProps, ref: ForwardedRef<HTMLInputElement>) => {
({ value, isInputSubmitted, isTagSearch, ...props }: SearchInputProps, ref: ForwardedRef<HTMLInputElement>) => {
return (
<div className={inputContainer}>
<input
Expand All @@ -20,11 +21,25 @@ const SearchInput = forwardRef(
value={value}
{...props}
/>
{isTagSearch && isInputSubmitted && (
<div className={tagInputWrapper}>
<Text color="info" size="caption3">
{value}
</Text>
<button>
<SvgIcon variant="close2" stroke="#6B6B6B" width={8} height={8} />
</button>
</div>
)}
<button className={iconWrapperButton}>
{isInputSubmitted ? (
<SvgIcon variant="close2" width={13} height={13} stroke="#232527" />
) : (
<SvgIcon variant="search2" width={20} height={20} stroke="#808080" />
{!isTagSearch && (
<>
{isInputSubmitted ? (
<SvgIcon variant="close2" width={13} height={13} stroke="#232527" />
) : (
<SvgIcon variant="search2" width={20} height={20} stroke="#808080" />
)}
</>
)}
</button>
</div>
Expand Down
16 changes: 15 additions & 1 deletion src/components/Search/SearchInput/searchInput.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const inputContainer = style({
});

export const searchInput = style({
position: 'absolute',
width: '90%',
height: 40,
padding: '10px 0 10px 18px',
Expand All @@ -22,14 +23,27 @@ export const searchInput = style({
},
});

export const tagInputWrapper = style({
position: 'relative',
top: 6,
left: 6,
display: 'flex',
alignItems: 'baseline',
gap: 6,
width: 'fit-content',
padding: '5px 12px',
background: '#FFFFFF',
borderRadius: 20,
});

export const iconWrapperButton = style({
position: 'absolute',
top: 0,
right: 0,
display: 'flex',
alignItems: 'center',
width: '10%',
height: '100%',
height: 40,
paddingLeft: 4,
background: '#efefef',
borderRadius: '0 20px 20px 0',
Expand Down
13 changes: 13 additions & 0 deletions src/components/Search/SearchNotFound/SearchNotFound.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';

import SearchNotFound from './SearchNotFound';

const meta: Meta<typeof SearchNotFound> = {
title: 'search/SearchNotFound',
component: SearchNotFound,
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
24 changes: 24 additions & 0 deletions src/components/Search/SearchNotFound/SearchNotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { container } from './searchNotFound.css';

import SearchNotFoundImage from '@/assets/search-notfound.png';
import { Text } from '@/components/Common';


const SearchNotFound = () => {
return (
<>
<div className={container}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="sub" size="headline" weight="semiBold">
검색 결과가 없어요
</Text>
<div style={{ height: '6px' }} />
<Text color="disabled" size="caption4">
다른 키워드로 검색해보세요!
</Text>
</div>
</>
);
};

export default SearchNotFound;
7 changes: 7 additions & 0 deletions src/components/Search/SearchNotFound/searchNotFound.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { style } from '@vanilla-extract/css';

export const container = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';

import TagSearchResultList from './TagSearchResultList';

const meta: Meta<typeof TagSearchResultList> = {
title: 'search/TagSearchResultList',
component: TagSearchResultList,
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
48 changes: 48 additions & 0 deletions src/components/Search/TagSearchResultList/TagSearchResultList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useRef } from 'react';
import { Link } from 'react-router-dom';

import { container } from './tagSearchResult.css';
import SearchNotFound from '../SearchNotFound/SearchNotFound';

import { ProductItem } from '@/components/Product';
import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteProductSearchResultsQuery } from '@/hooks/queries/search';

interface TagSearchResultListProps {
searchQuery: string;
}

const TagSearchResultList = ({ searchQuery }: TagSearchResultListProps) => {
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteProductSearchResultsQuery(searchQuery, true);

const scrollRef = useRef<HTMLDivElement>(null);
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

if (!searchResponse) {
return null;
}

const products = searchResponse.pages.flatMap((page) => page.products);

if (products.length === 0) {
return <SearchNotFound />;
}

return (
<>
<ul className={container}>
{products.map((product) => (
<li key={product.id}>
<Link to={`${PATH.PRODUCT_LIST}/${product.categoryType}/${product.id}`}>
Copy link
Contributor

Choose a reason for hiding this comment

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

타미 이거 혹시 상품 상세로 넘어가는건가요?
만약 맞다면 ${product.categoryType} 이부분을 detail로 수정해야함니다~

Copy link
Member Author

Choose a reason for hiding this comment

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

아 맞다!!!! 이거 고친다해놓고 깜빡했네요

<ProductItem product={product} />
</Link>
</li>
))}
</ul>
<div ref={scrollRef} aria-hidden />
</>
);
};

export default TagSearchResultList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';

export const container = style({
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
rowGap: 20,
columnGap: 10,
});
1 change: 1 addition & 0 deletions src/components/Search/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as ProductSearchResultList } from './ProductSearchResultList/ProductSearchResultList';
export { default as RecommendList } from './RecommendList/RecommendList';
export { default as RecipeSearchResultList } from './RecipeSearchResultList/RecipeSearchResultList';
export { default as TagSearchResultList } from './TagSearchResultList/TagSearchResultList';
export { default as SearchInput } from './SearchInput/SearchInput';
12 changes: 7 additions & 5 deletions src/hooks/queries/search/useInfiniteProductSearchResultsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import { useSuspendedInfiniteQuery } from '..';
import { searchApi } from '@/apis';
import type { ProductSearchResultResponse } from '@/types/response';

const fetchProductSearchResults = async (query: string, pageParam: number) => {
const fetchProductSearchResults = async (query: string, endpoint: 'tags' | 'products', pageParam: number) => {
const response = await searchApi.get({
params: '/products/results',
params: `/${endpoint}/results`,
queries: `?query=${query}&lastProductId=${pageParam}`,
});
const data: ProductSearchResultResponse = await response.json();

return data;
};

const useInfiniteProductSearchResultsQuery = (query: string) => {
const useInfiniteProductSearchResultsQuery = (query: string, isTagSearch = false) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

true, false는 좀 추상적이여서 endpoint 자체를 인자로 받는건 어떤가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

수정하고 머지하겠슴니당

const endpoint = isTagSearch ? 'tags' : 'products';

return useSuspendedInfiniteQuery(
['search', 'products', 'results', query],
({ pageParam = 0 }) => fetchProductSearchResults(query, pageParam),
['search', endpoint, 'results', query],
({ pageParam = 0 }) => fetchProductSearchResults(query, endpoint, pageParam),
{
getNextPageParam: (prevResponse: ProductSearchResultResponse) => {
const lastCursor = prevResponse.products.length
Expand Down
Loading
Loading