-
Notifications
You must be signed in to change notification settings - Fork 1
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
검색 기능 페이지 구현 #21
Merged
Merged
검색 기능 페이지 구현 #21
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
0773d0f
init: App.tsx 초기 설정
coggiee c842b71
feat: 검색 API 요청 함수 구현
coggiee e8b4e2d
style: 이전 아이콘 svg를 컴포넌트로 추가
coggiee 6688fa8
style: 검색바 템플릿 추가
coggiee 6c2f507
style: 검색 아이콘 svg 컴포넌트로 추가
coggiee 36a4631
feat: (임시) 검색 로직 추가
coggiee 041131a
refactor: 검색어 기능을 커스텀 훅으로 분리
coggiee 56edf2f
feat: 키워드가 없을 때 검색 시 예외 처리
coggiee b970309
modify: 인터페이스 수정
coggiee cdf350e
feat: 필터링 기능 추가
coggiee 2ba5132
style: Filter 스타일 수정
coggiee 3c2d796
style: 스타일 수정
coggiee 4ab7b42
style: 검색 결과 섹션 추가
coggiee c33cd58
feat: 사용자 검색 결과 렌더링
coggiee f3b2f21
feat: (임시) 전체 검색 결과 렌더링
coggiee d970945
modify: 기본 필터를 전체로 수정
coggiee 00a49fd
bug: title 값 오류
coggiee ec46744
style: 전체 검색 결과 아이템에 스타일을 추가
coggiee d7af397
style: 아이콘 svg를 컴포넌트로 추가
coggiee 02f60dc
style: Grid 스타일 설정
coggiee 46ece62
refactor: 코드 가독성 개선
coggiee fc0d2fa
feat: 필터에 따른 스타일 분기 처리 적용
coggiee 2cbfee7
comment: TODO 추가
coggiee 8fb2763
refactor: 사용하지 않는 import 삭제
coggiee f085f3a
style: 없는 결과 아이콘 svg 컴포넌트로 추가
coggiee af26cc5
style: 검색 결과 없을 시 렌더링되는 컴포넌트 템플릿 추가
coggiee 1b36e9c
feat: 검색 결과 분기 처리 추가
coggiee feac692
feat: 프로필 이미지가 없을 경우 분기 처리
coggiee 635ac7f
style: 스타일 수정
coggiee 7d3d21c
feat: 포스트 썸네일 없을 경우 분기 처리
coggiee d7c90bc
modify: 검색 결과 없을 시 분기 처리 수정
coggiee 26ae205
modify: 검색 결과 없을 시 분기 처리 수정
coggiee 6ec1177
refactor: 객체 구조 분해 할당 적용
coggiee fdbf515
Merge branch 'develop' into feature/search
coggiee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { SEARCH_ALL_PATH } from '@/utils/api_paths' | ||
import axiosInstance from '../api' | ||
|
||
interface SearchResponse { | ||
posts: Post[] | ||
users: User[] | ||
} | ||
|
||
interface SearchParams { | ||
query: string | ||
} | ||
|
||
async function searchAll({ query }: SearchParams): Promise<SearchResponse> { | ||
try { | ||
const response = await axiosInstance.get(`${SEARCH_ALL_PATH}/${query}`) | ||
const data = response.data | ||
|
||
const filteredPost: Post[] = data.filter( | ||
(item: Post | User): item is Post => { | ||
return item instanceof Object && 'title' in item | ||
} | ||
) | ||
|
||
const filteredUser: User[] = data.filter( | ||
(item: Post | User): item is User => { | ||
return item instanceof Object && 'role' in item | ||
} | ||
) | ||
|
||
return { | ||
posts: filteredPost, | ||
users: filteredUser | ||
} | ||
} catch (error) { | ||
throw new Error('Error while searching') | ||
} | ||
} | ||
|
||
export default searchAll |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { SEARCH_USERS_PATH } from '@/utils/api_paths' | ||
import axiosInstance from '../api' | ||
|
||
interface SearchResponse { | ||
users: User[] | ||
} | ||
|
||
interface SearchParams { | ||
query: string | ||
} | ||
|
||
async function searchUser({ query }: SearchParams): Promise<SearchResponse> { | ||
try { | ||
const response = await axiosInstance.get(`${SEARCH_USERS_PATH}/${query}`) | ||
|
||
return { | ||
users: response.data | ||
} | ||
} catch (error) { | ||
throw new Error('Error while searching') | ||
} | ||
} | ||
|
||
export default searchUser |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { useState } from 'react' | ||
|
||
interface IUseSearch { | ||
initialValue: string | ||
onSearch: (value: string) => Promise<void> | ||
validate?: (value: string) => string | ||
} | ||
|
||
const useSearch = ({ initialValue, onSearch, validate }: IUseSearch) => { | ||
const [value, setValue] = useState(initialValue) | ||
const [type, setType] = useState('all') // ['all', 'user'] | ||
const [isLoading, setIsLoading] = useState(false) | ||
const [error, setError] = useState('') | ||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const { value } = e.target | ||
if (value.length > 1) setError('') | ||
setValue(value) | ||
} | ||
|
||
const handleClickFilter = (type: string) => { | ||
setType(type) | ||
} | ||
|
||
const handleSearch = async ( | ||
value: string, | ||
e?: React.KeyboardEvent<HTMLInputElement> | ||
) => { | ||
setIsLoading(true) | ||
if (e) { | ||
if (e.key === 'Enter') { | ||
const newError = validate ? validate(value) : '' | ||
if (newError.length === 0) { | ||
await onSearch(value) | ||
setValue('') | ||
} | ||
setError(newError) | ||
setIsLoading(false) | ||
} | ||
return | ||
} | ||
|
||
const newError = validate ? validate(value) : '' | ||
if (newError.length === 0) { | ||
await onSearch(value) | ||
setValue('') | ||
} | ||
setError(newError) | ||
setIsLoading(false) | ||
} | ||
|
||
return { | ||
value, | ||
type, | ||
error, | ||
isLoading, | ||
handleChange, | ||
handleSearch, | ||
handleClickFilter | ||
} | ||
} | ||
|
||
export default useSearch |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import * as React from 'react' | ||
|
||
function CommentIcon(props: React.SVGProps<SVGSVGElement>) { | ||
return ( | ||
<svg | ||
viewBox="0 0 24 24" | ||
fill="currentColor" | ||
height="1em" | ||
width="1em" | ||
{...props}> | ||
<path d="M12 2A10 10 0 002 12a9.89 9.89 0 002.26 6.33l-2 2a1 1 0 00-.21 1.09A1 1 0 003 22h9a10 10 0 000-20zm0 18H5.41l.93-.93a1 1 0 000-1.41A8 8 0 1112 20z" /> | ||
</svg> | ||
) | ||
} | ||
|
||
export default CommentIcon |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import tw, { styled } from 'twin.macro' | ||
|
||
const FilterContainer = styled.div` | ||
${tw`flex items-center gap-3`} | ||
` | ||
|
||
const FilterItem = styled.button(({ isSelected }) => [ | ||
tw`rounded-3xl bg-[#eee] text-brand-primary py-2 px-4 font-bold`, | ||
isSelected && tw`bg-brand-primary text-white` | ||
]) | ||
|
||
interface FilterProps { | ||
selectedFilterItem: 'user' | 'all' | ||
onClickFilter: (type: 'user' | 'all') => void | ||
} | ||
|
||
const Filter = ({ selectedFilterItem, onClickFilter }: FilterProps) => { | ||
return ( | ||
<FilterContainer> | ||
<FilterItem | ||
onClick={() => onClickFilter('all')} | ||
isSelected={selectedFilterItem === 'all'}> | ||
전체 | ||
</FilterItem> | ||
<FilterItem | ||
onClick={() => onClickFilter('user')} | ||
isSelected={selectedFilterItem === 'user'}> | ||
사용자 | ||
</FilterItem> | ||
</FilterContainer> | ||
) | ||
} | ||
|
||
export default Filter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import tw, { styled } from 'twin.macro' | ||
import FilteredUser from './FilteredUser' | ||
import FilteredPost from './FilteredPost' | ||
import NoResult from './NoResult' | ||
|
||
interface FilteredListProps { | ||
type: 'user' | 'all' | ||
users?: User[] | ||
posts?: Post[] | ||
isSearched?: boolean | ||
} | ||
|
||
const Container = styled.div` | ||
${tw`flex flex-col gap-3 grow`} | ||
` | ||
|
||
const HorizontalSlide = styled.div` | ||
${tw`pb-3 flex items-center gap-3 overflow-x-scroll`} | ||
` | ||
|
||
const GridPostSection = styled.div` | ||
${tw`grid grid-cols-2 gap-2`} | ||
` | ||
|
||
const FilteredList = ({ | ||
type, | ||
users, | ||
posts, | ||
isSearched | ||
}: FilteredListProps) => { | ||
return ( | ||
<Container> | ||
{type === 'user' && | ||
users?.map(({ image, fullName, email }) => ( | ||
<FilteredUser | ||
image={image} | ||
fullName={fullName} | ||
email={email} | ||
/> | ||
))} | ||
{type === 'all' && ( | ||
<> | ||
<HorizontalSlide> | ||
{users?.map(({ image, fullName, email }) => ( | ||
<FilteredUser | ||
image={image} | ||
fullName={fullName} | ||
email={email} | ||
isHidden | ||
/> | ||
))} | ||
</HorizontalSlide> | ||
{users!.length > 0 || | ||
(posts!.length > 0 && ( | ||
<GridPostSection> | ||
{posts?.map(({ image, title, comments, likes }) => ( | ||
<FilteredPost | ||
thumbnail={image} | ||
title={title} | ||
commentsNum={comments.length} | ||
likesNum={likes.length} | ||
/> | ||
))} | ||
</GridPostSection> | ||
))} | ||
</> | ||
)} | ||
{isSearched && users!.length === 0 && posts!.length === 0 && <NoResult />} | ||
</Container> | ||
) | ||
} | ||
|
||
export default FilteredList |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import tw, { styled } from 'twin.macro' | ||
import CommentIcon from './CommentIcon' | ||
import HeartIcon from './HeartIcon' | ||
import { useCallback } from 'react' | ||
|
||
interface FilteredPostProps { | ||
thumbnail?: string | ||
title: string | ||
commentsNum: number | ||
likesNum: number | ||
} | ||
|
||
const Container = styled.section` | ||
${tw`h-[280px]`} | ||
` | ||
|
||
const Wrapper = styled.div` | ||
${tw`relative h-full rounded-xl overflow-hidden shadow-md`} | ||
` | ||
|
||
const Content = styled.div` | ||
${tw`absolute w-3/4 bottom-2 left-2 p-3 bg-white/30 backdrop-blur-md rounded-lg`} | ||
` | ||
|
||
const Title = styled.h1` | ||
${tw`w-full text-[14px] font-bold truncate`} | ||
` | ||
|
||
const Social = styled.div` | ||
${tw`flex items-center gap-2 text-xs`} | ||
` | ||
|
||
const SocialButton = styled.button` | ||
${tw`flex items-center gap-1`} | ||
` | ||
|
||
const Thumbnail = styled.div` | ||
${tw`w-full h-full flex justify-center items-center`} | ||
` | ||
|
||
const FilteredPost = ({ | ||
thumbnail, | ||
title, | ||
commentsNum, | ||
likesNum | ||
}: FilteredPostProps) => { | ||
const formatTitle = useCallback((title: string) => { | ||
if (title === '[object FormData]') { | ||
return 'FormData 형식 오류' | ||
} else { | ||
const tmp = JSON.parse(title) | ||
if (tmp === 'undefined') { | ||
return title | ||
} else { | ||
return tmp.title | ||
} | ||
} | ||
}, []) | ||
|
||
return ( | ||
// TODO: 추후 라우터를 이용하여 해당 포스트의 상세 페이지로 이동하는 기능 추가 | ||
<Container> | ||
<Wrapper> | ||
<Thumbnail> | ||
{thumbnail && ( | ||
<img | ||
src={thumbnail} | ||
alt="썸네일" | ||
className="bg-cover rounded-xl" | ||
/> | ||
)} | ||
{!thumbnail && ( | ||
<img | ||
src={'src/assets/NoThumbnail.png'} | ||
alt="썸네일" | ||
className="w-20 h-20 rounded-xl" | ||
/> | ||
)} | ||
</Thumbnail> | ||
<Content> | ||
<Title>{formatTitle(title)}</Title> | ||
<Social> | ||
<SocialButton> | ||
<CommentIcon /> | ||
<span>{commentsNum}</span> | ||
</SocialButton> | ||
<SocialButton> | ||
<HeartIcon /> | ||
<span>{likesNum}</span> | ||
</SocialButton> | ||
</Social> | ||
</Content> | ||
</Wrapper> | ||
</Container> | ||
) | ||
} | ||
|
||
export default FilteredPost |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3
users?.map 및 posts?.map에서 컴포넌트에 고유한 key 값을 사용하면 좋을 것 같아요!