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 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import { useInfiniteProductSearchResultsQuery } from '@/hooks/queries/search';

interface ProductSearchResultListProps {
searchQuery: string;
isTagSearch: boolean;
}

const ProductSearchResultList = ({ searchQuery }: ProductSearchResultListProps) => {
const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteProductSearchResultsQuery(searchQuery);
const ProductSearchResultList = ({ searchQuery, isTagSearch }: ProductSearchResultListProps) => {
const {
data: searchResponse,
fetchNextPage,
hasNextPage,
} = useInfiniteProductSearchResultsQuery(searchQuery, isTagSearch);
const scrollRef = useRef<HTMLDivElement>(null);
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

Expand Down
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: boolean) => {
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
18 changes: 13 additions & 5 deletions src/hooks/search/useSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const useSearch = () => {
const [searchQuery, setSearchQuery] = useState(currentSearchQuery || '');
const [isSubmitted, setIsSubmitted] = useState(!!currentSearchQuery);
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(searchQuery.length > 0);
const [isTagSearch, setIsTagSearch] = useState(false);

const { toast } = useToastActionContext();

Expand All @@ -41,7 +42,7 @@ const useSearch = () => {
setSearchParams({ query: value });
};

const handleSearch: FormEventHandler<HTMLFormElement> = (event) => {
const handleSearchForm: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
gaEvent({ category: 'submit', action: '검색 페이지에서 검색', label: '검색' });

Expand All @@ -62,7 +63,7 @@ const useSearch = () => {
setLocalStorage('recentSearchedKeywords', [trimmedSearchQuery, ...recentSearchedKeywords.slice(0, 7)]);
};

const handleSearchClick: MouseEventHandler<HTMLButtonElement> = (event) => {
const handleSearchByClick: MouseEventHandler<HTMLButtonElement> = (event) => {
const { value } = event.currentTarget;
searchKeyword(value);
};
Expand All @@ -71,6 +72,12 @@ const useSearch = () => {
setIsAutocompleteOpen(false);
};

const handleTagSearch: MouseEventHandler<HTMLButtonElement> = (event) => {
const { value } = event.currentTarget;
searchKeyword(value);
setIsTagSearch(true);
};

const resetSearchQuery = () => {
setSearchQuery('');
setIsSubmitted(false);
Expand All @@ -84,10 +91,11 @@ const useSearch = () => {
isSubmitted,
isAutocompleteOpen,
handleSearchQuery,
handleSearch,
handleSearchClick,
handleSearchForm,
handleSearchByClick,
handleAutocompleteClose,
searchKeyword,
isTagSearch,
handleTagSearch,
resetSearchQuery,
};
};
Expand Down
2 changes: 1 addition & 1 deletion src/mocks/handlers/searchHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const searchHandlers = [
return res(ctx.status(400));
}

if (searchId === 'products') {
if (searchId === 'products' || searchId === 'tags') {
const filteredProducts = {
page: { ...productSearchResults.page },
products: productSearchResults.products
Expand Down
41 changes: 23 additions & 18 deletions src/pages/SearchPage/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export const SearchPage = () => {
isSubmitted,
isAutocompleteOpen,
handleSearchQuery,
handleSearch,
handleSearchClick,
handleSearchForm,
handleSearchByClick,
handleAutocompleteClose,
searchKeyword,
isTagSearch,
handleTagSearch,
resetSearchQuery,
} = useSearch();
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery || '');
Expand All @@ -47,15 +48,15 @@ export const SearchPage = () => {
<section className={searchSection}>
<PageHeader title="검색" hasBackLink />
<div style={{ height: '16px' }} />
<form onSubmit={isSubmitted ? resetSearchQuery : handleSearch}>
<form onSubmit={isSubmitted ? resetSearchQuery : handleSearchForm}>
<SearchInput value={searchQuery} onChange={handleSearchQuery} isInputSubmitted={isSubmitted} ref={inputRef} />
</form>
{!isSubmitted && debouncedSearchQuery && isAutocompleteOpen && (
<ErrorBoundary fallback={ErrorComponent} handleReset={reset}>
<Suspense fallback={<Loading />}>
<RecommendList
searchQuery={debouncedSearchQuery}
handleSearchClick={handleSearchClick}
handleSearchClick={handleSearchByClick}
handleAutocompleteClose={handleAutocompleteClose}
/>
</Suspense>
Expand All @@ -64,30 +65,34 @@ export const SearchPage = () => {
</section>
<section className={searchSection}>
{isSubmitted && searchQuery ? (
<div>
<>
<p className={searchResultTitle}>상품 바로가기</p>
<ErrorBoundary fallback={ErrorComponent}>
<Suspense fallback={<Loading />}>
<ProductSearchResultList searchQuery={searchQuery} />
<ProductSearchResultList searchQuery={searchQuery} isTagSearch={isTagSearch} />
</Suspense>
</ErrorBoundary>
<button className={showMoreButton}>더보기</button>
{/* divider 컴포넌트 */}
<hr style={{ border: '1px solid #e6e6e6' }} />
<div style={{ height: '12px' }} />
<p className={searchResultTitle}>꿀!조합 바로가기</p>
<ErrorBoundary fallback={ErrorComponent}>
<Suspense fallback={<Loading />}>
<RecipeSearchResultList searchQuery={searchQuery} />
</Suspense>
</ErrorBoundary>
</div>
{!isTagSearch && (
<>
<p className={searchResultTitle}>꿀!조합 바로가기</p>
<ErrorBoundary fallback={ErrorComponent}>
<Suspense fallback={<Loading />}>
<RecipeSearchResultList searchQuery={searchQuery} />
</Suspense>
</ErrorBoundary>
</>
)}
</>
) : (
<div>
<>
<p className={subTitle}>최근 검색어</p>
<div className={badgeContainer}>
{recentSearchedKeywords?.map((keyword, index) => (
<button key={index} onClick={() => searchKeyword(keyword)}>
<button type="button" key={index} value={keyword} onClick={handleSearchByClick}>
<Badge color="#e6e6e6" textColor="#808080" outlined>
{keyword}
</Badge>
Expand All @@ -97,14 +102,14 @@ export const SearchPage = () => {
<p className={subTitle}>추천 태그</p>
<div className={badgeContainer}>
{RECOMMENDED_TAGS.map(({ id, name }) => (
<button key={id}>
<button type="button" key={id} value={name} onClick={handleTagSearch}>
<Badge color="#e6e6e6" textColor="#808080">
{name}
</Badge>
</button>
))}
</div>
</div>
</>
)}
</section>
</>
Expand Down
Loading