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

검색 기능 페이지 구현 #21

Merged
merged 34 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0773d0f
init: App.tsx 초기 설정
coggiee Jan 3, 2024
c842b71
feat: 검색 API 요청 함수 구현
coggiee Jan 3, 2024
e8b4e2d
style: 이전 아이콘 svg를 컴포넌트로 추가
coggiee Jan 3, 2024
6688fa8
style: 검색바 템플릿 추가
coggiee Jan 3, 2024
6c2f507
style: 검색 아이콘 svg 컴포넌트로 추가
coggiee Jan 3, 2024
36a4631
feat: (임시) 검색 로직 추가
coggiee Jan 3, 2024
041131a
refactor: 검색어 기능을 커스텀 훅으로 분리
coggiee Jan 3, 2024
56edf2f
feat: 키워드가 없을 때 검색 시 예외 처리
coggiee Jan 3, 2024
b970309
modify: 인터페이스 수정
coggiee Jan 3, 2024
cdf350e
feat: 필터링 기능 추가
coggiee Jan 3, 2024
2ba5132
style: Filter 스타일 수정
coggiee Jan 3, 2024
3c2d796
style: 스타일 수정
coggiee Jan 3, 2024
4ab7b42
style: 검색 결과 섹션 추가
coggiee Jan 3, 2024
c33cd58
feat: 사용자 검색 결과 렌더링
coggiee Jan 3, 2024
f3b2f21
feat: (임시) 전체 검색 결과 렌더링
coggiee Jan 3, 2024
d970945
modify: 기본 필터를 전체로 수정
coggiee Jan 3, 2024
00a49fd
bug: title 값 오류
coggiee Jan 3, 2024
ec46744
style: 전체 검색 결과 아이템에 스타일을 추가
coggiee Jan 3, 2024
d7af397
style: 아이콘 svg를 컴포넌트로 추가
coggiee Jan 3, 2024
02f60dc
style: Grid 스타일 설정
coggiee Jan 3, 2024
46ece62
refactor: 코드 가독성 개선
coggiee Jan 4, 2024
fc0d2fa
feat: 필터에 따른 스타일 분기 처리 적용
coggiee Jan 4, 2024
2cbfee7
comment: TODO 추가
coggiee Jan 4, 2024
8fb2763
refactor: 사용하지 않는 import 삭제
coggiee Jan 4, 2024
f085f3a
style: 없는 결과 아이콘 svg 컴포넌트로 추가
coggiee Jan 4, 2024
af26cc5
style: 검색 결과 없을 시 렌더링되는 컴포넌트 템플릿 추가
coggiee Jan 4, 2024
1b36e9c
feat: 검색 결과 분기 처리 추가
coggiee Jan 4, 2024
feac692
feat: 프로필 이미지가 없을 경우 분기 처리
coggiee Jan 4, 2024
635ac7f
style: 스타일 수정
coggiee Jan 4, 2024
7d3d21c
feat: 포스트 썸네일 없을 경우 분기 처리
coggiee Jan 4, 2024
d7c90bc
modify: 검색 결과 없을 시 분기 처리 수정
coggiee Jan 4, 2024
26ae205
modify: 검색 결과 없을 시 분기 처리 수정
coggiee Jan 4, 2024
6ec1177
refactor: 객체 구조 분해 할당 적용
coggiee Jan 5, 2024
fdbf515
Merge branch 'develop' into feature/search
coggiee Jan 5, 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
39 changes: 39 additions & 0 deletions src/apis/search/searchAll.ts
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
24 changes: 24 additions & 0 deletions src/apis/search/searchUser.ts
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
Binary file added src/assets/NoThumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions src/hooks/useSearch.ts
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
16 changes: 16 additions & 0 deletions src/pages/search/components/CommentIcon.tsx
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
34 changes: 34 additions & 0 deletions src/pages/search/components/Filter.tsx
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
73 changes: 73 additions & 0 deletions src/pages/search/components/FilteredList.tsx
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}
/>
))}
Comment on lines +34 to +63
Copy link
Contributor

@Yoonkyoungme Yoonkyoungme Jan 5, 2024

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 값을 사용하면 좋을 것 같아요!

</GridPostSection>
))}
</>
)}
{isSearched && users!.length === 0 && posts!.length === 0 && <NoResult />}
</Container>
)
}

export default FilteredList
98 changes: 98 additions & 0 deletions src/pages/search/components/FilteredPost.tsx
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
Loading