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

[FE] feat: 상품 검색 자동완성 쿼리 추가 #447

Merged
merged 9 commits into from
Aug 15, 2023
33 changes: 15 additions & 18 deletions frontend/src/components/Search/RecommendList/RecommendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,40 @@ import styled from 'styled-components';
import { MarkedText } from '@/components/Common';
import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteSearchedProductsQuery } from '@/hooks/queries/search';
import { useInfiniteProductSearchAutocompleteQuery } from '@/hooks/queries/search';

interface RecommendListProps {
searchQuery: string;
}

const RecommendList = ({ searchQuery }: RecommendListProps) => {
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteSearchedProductsQuery(searchQuery);
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteProductSearchAutocompleteQuery(searchQuery);
const scrollRef = useRef<HTMLDivElement>(null);
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

if (!searchResponse) {
return null;
}

const products = searchResponse.pages
.flatMap((page) => page.products)
.map((product) => ({
id: product.id,
name: product.name,
category: product.category,
}));
const products = searchResponse.pages.flatMap((page) => page.products);

if (products.length === 0) {
return <ErrorText>검색어에 해당 하는 상품이 없습니다.</ErrorText>;
}

return (
<RecommendListContainer>
{products.map(({ id, name }) => (
<li key={id}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/food/${id}`} block>
<MarkedText text={name} mark={searchQuery} />
</Link>
</li>
))}
</RecommendListContainer>
<>
<RecommendListContainer>
{products.map(({ id, name, categoryType }) => (
<li key={id}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/${categoryType}/${id}`} block>
<MarkedText text={name} mark={searchQuery} />
</Link>
</li>
))}
</RecommendListContainer>
<div ref={scrollRef} aria-hidden />
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import styled from 'styled-components';
import { ProductItem } from '@/components/Product';
import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteSearchedProductsQuery } from '@/hooks/queries/search';
import { useInfiniteProductSearchResultsQuery } from '@/hooks/queries/search';

interface SearchedListProps {
interface SearchResultListProps {
searchQuery: string;
}

const SearchedList = ({ searchQuery }: SearchedListProps) => {
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteSearchedProductsQuery(searchQuery);
const SearchResultList = ({ searchQuery }: SearchResultListProps) => {
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteProductSearchResultsQuery(searchQuery);
const scrollRef = useRef<HTMLDivElement>(null);
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

Expand All @@ -29,23 +29,23 @@ const SearchedList = ({ searchQuery }: SearchedListProps) => {

return (
<>
<SearchedListContainer>
<SearchResultListContainer>
{products.map((product) => (
<li key={product.id}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/food/${product.id}`}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/${product.categoryType}/${product.id}`}>
<ProductItem product={product} />
</Link>
</li>
))}
</SearchedListContainer>
</SearchResultListContainer>
<div ref={scrollRef} aria-hidden />
</>
);
};

export default SearchedList;
export default SearchResultList;

const SearchedListContainer = styled.ul`
const SearchResultListContainer = styled.ul`
display: flex;
flex-direction: column;

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Search/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as SearchedList } from './SearchedList/SearchedList';
export { default as SearchResultList } from './SearchResultList/SearchResultList';
export { default as RecommendList } from './RecommendList/RecommendList';
3 changes: 2 additions & 1 deletion frontend/src/hooks/queries/search/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as useInfiniteSearchedProductsQuery } from './useInfiniteSearchedProductsQuery';
export { default as useInfiniteProductSearchResultsQuery } from './useInfiniteProductSearchResultsQuery';
export { default as useInfiniteProductSearchAutocompleteQuery } from './useInfiniteProductSearchAutocompleteQuery';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useInfiniteQuery } from '@tanstack/react-query';

import { searchApi } from '@/apis';
import type { ProductSearchAutocompleteResponse } from '@/types/response';

const fetchProductSearchAutocomplete = async (query: string, page: number) => {
const response = await searchApi.get({ params: '/products', queries: `?query=${query}&page=${page}` });
const data: ProductSearchAutocompleteResponse = await response.json();

return data;
};

const useInfiniteProductSearchAutocompleteQuery = (query: string) => {
return useInfiniteQuery({
queryKey: ['search', 'products', query],
queryFn: ({ pageParam = 0 }) => fetchProductSearchAutocomplete(query, pageParam),
getNextPageParam: (prevResponse: ProductSearchAutocompleteResponse) => {
const isLast = prevResponse.page.lastPage;
const nextPage = prevResponse.page.requestPage + 1;
return isLast ? undefined : nextPage;
},
suspense: true,
});
};

export default useInfiniteProductSearchAutocompleteQuery;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useInfiniteQuery } from '@tanstack/react-query';

import { searchApi } from '@/apis';
import type { ProductSearchResultResponse } from '@/types/response';

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

return data;
};

const useInfiniteProductSearchResultsQuery = (query: string) => {
return useInfiniteQuery({
queryKey: ['search', 'products', 'results', query],
queryFn: ({ pageParam = 0 }) => fetchProductSearchResults(query, pageParam),
getNextPageParam: (prevResponse: ProductSearchResultResponse) => {
const isLast = prevResponse.page.lastPage;
const nextPage = prevResponse.page.requestPage + 1;
return isLast ? undefined : nextPage;
},
suspense: true,
});
};

export default useInfiniteProductSearchResultsQuery;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"id": 1,
"name": "꼬북칩",
"price": 1500,
"category": "food",
"categoryType": "food",
"image": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34",
"averageRating": 4.5,
"reviewCount": 100
Expand All @@ -21,7 +21,7 @@
"id": 2,
"name": "새우깡",
"price": 1000,
"category": "food",
"categoryType": "food",
"image": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34",
"averageRating": 4.0,
"reviewCount": 55
Expand All @@ -30,7 +30,7 @@
"id": 11,
"name": "PB 꼬북칩",
"price": 1500,
"category": "store",
"categoryType": "store",
"image": "https://t3.ftcdn.net/jpg/06/06/91/70/240_F_606917032_4ujrrMV8nspZDX8nTgGrTpJ69N9JNxOL.jpg",
"averageRating": 4.5,
"reviewCount": 100
Expand All @@ -39,7 +39,7 @@
"id": 12,
"name": "PB 새우깡",
"price": 1000,
"category": "store",
"categoryType": "store",
"image": "https://cdn.pixabay.com/photo/2016/03/23/15/00/ice-cream-1274894_1280.jpg",
"averageRating": 4.0,
"reviewCount": 100
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/mocks/data/searchingProducts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"page": {
"totalDataCount": 99,
"totalPages": 10,
"firstPage": true,
"lastPage": false,
"requestPage": 1,
"requestSize": 10
},
"products": [
{
"id": 1,
"name": "꼬북칩",
"categoryType": "food"
},
{
"id": 2,
"name": "새우깡",
"categoryType": "food"
},
{
"id": 11,
"name": "PB 꼬북칩",
"categoryType": "store"
},
{
"id": 12,
"name": "PB 새우깡",
"categoryType": "store"
}
]
}
42 changes: 37 additions & 5 deletions frontend/src/mocks/handlers/searchHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
import { rest } from 'msw';

import searchedProducts from '../data/searchedProducts.json';
import productSearchResults from '../data/productSearchResults.json';
import searchingProducts from '../data/searchingProducts.json';

export const searchHandlers = [
rest.get('/api/search/:searchId/results', (req, res, ctx) => {
const { searchId } = req.params;
const query = req.url.searchParams.get('query');
const page = Number(req.url.searchParams.get('page'));

if (query === null) {
return res(ctx.status(400));
}

if (searchId === 'products') {
const filteredProducts = {
page: { ...productSearchResults.page },
products: productSearchResults.products
.filter((product) => product.name.includes(query))
.slice(page * 5, (page + 1) * 5),
};
return res(ctx.status(200), ctx.json(filteredProducts), ctx.delay(1000));
}

// TODO: 꿀조합 목 데이터 만들기
if (searchId === 'recipes') {
const filteredProducts = {
page: { ...productSearchResults.page },
products: productSearchResults.products.filter((product) => product.name.includes(query)),
};
return res(ctx.status(200), ctx.json(filteredProducts));
}

return res(ctx.status(400));
}),

rest.get('/api/search/:searchId', (req, res, ctx) => {
const { searchId } = req.params;
const query = req.url.searchParams.get('query');
Expand All @@ -14,8 +46,8 @@ export const searchHandlers = [

if (searchId === 'products') {
const filteredProducts = {
page: { ...searchedProducts.page },
products: searchedProducts.products
page: { ...searchingProducts.page },
products: searchingProducts.products
.filter((product) => product.name.includes(query))
.slice(page * 5, (page + 1) * 5),
};
Expand All @@ -25,8 +57,8 @@ export const searchHandlers = [
// TODO: 꿀조합 목 데이터 만들기
if (searchId === 'recipes') {
const filteredProducts = {
page: { ...searchedProducts.page },
products: searchedProducts.products.filter((product) => product.name.includes(query)),
page: { ...searchingProducts.page },
products: searchingProducts.products.filter((product) => product.name.includes(query)),
};
return res(ctx.status(200), ctx.json(filteredProducts));
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Suspense, useState } from 'react';
import styled from 'styled-components';

import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon, TabMenu } from '@/components/Common';
import { RecommendList, SearchedList } from '@/components/Search';
import { RecommendList, SearchResultList } from '@/components/Search';
import { useDebounce } from '@/hooks/common';
import { useSearch } from '@/hooks/search';

Expand Down Expand Up @@ -55,7 +55,7 @@ const SearchPage = () => {
<Mark>&apos;{searchQuery}&apos;</Mark>에 대한 검색결과입니다.
</Heading>
<Spacing size={20} />
<SearchedList searchQuery={debouncedSearchQuery} />
<SearchResultList searchQuery={debouncedSearchQuery} />
</Suspense>
</ErrorBoundary>
) : (
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/types/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Product } from './product';
import type { ProductRanking, ReviewRanking } from './ranking';
import type { Recipe } from './recipe';
import type { Review } from './review';
import type { SearchedProduct } from './search';
import type { ProductSearchResult, ProductSearchAutocomplete } from './search';

export interface Page {
totalDataCount: number;
Expand Down Expand Up @@ -34,10 +34,15 @@ export interface RecipeResponse {
page: Page;
recipes: Recipe[];
}

export interface SearchedProductResponse {

export interface ProductSearchAutocompleteResponse {
page: Page;
products: ProductSearchAutocomplete[];
}

export interface ProductSearchResultResponse {
page: Page;
products: SearchedProduct[];
products: ProductSearchResult[];
}

export interface MemberReviewResponse {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/types/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Product } from './product';

export interface SearchedProduct extends Product {
category: string;
export interface ProductSearchResult extends Product {
categoryType: string;
}

export type ProductSearchAutocomplete = Pick<ProductSearchResult, 'id' | 'name' | 'categoryType'>;
Loading