Skip to content

Commit 8f8cc49

Browse files
authored
feat: 검색 기능 구현 (#241)
2 parents cbc692f + 886ece2 commit 8f8cc49

File tree

20 files changed

+159
-59
lines changed

20 files changed

+159
-59
lines changed

.eslintrc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
"group": "internal",
4848
"position": "after"
4949
},
50+
{
51+
"pattern": "@atoms",
52+
"group": "internal",
53+
"position": "after"
54+
},
5055
{
5156
"pattern": "@layouts",
5257
"group": "internal",

src/apis/post/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type GetPostsReq = {
4242
category?: string
4343
sellerId?: number
4444
tradeStatus?: string
45+
searchKeyword?: string
4546
minPrice?: number
4647
maxPrice?: number
4748
lastId?: number | null

src/atoms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './search'

src/atoms/search.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { atom } from 'jotai'
2+
3+
export const searchKeywordAtom = atom('')

src/components/common/Header/SearchArea/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ import React from 'react'
22
import { Styled } from './styled'
33
import type { SearchAreaProps } from './types'
44

5-
export const SearchArea = ({ onClose }: SearchAreaProps) => {
5+
export const SearchArea = ({ onClose, onSubmitValue }: SearchAreaProps) => {
6+
const handleSubmitValue = (value: string) => {
7+
onSubmitValue(value)
8+
onClose()
9+
}
10+
611
return (
712
<Styled.SearchAreaWrapper>
813
<Styled.InputWrapper>
9-
<Styled.SearchInput placeholder="검색어를 입력하세요" />
14+
<Styled.SearchInput
15+
placeholder="검색어를 입력하세요"
16+
onSubmitValue={handleSubmitValue}
17+
/>
1018
<Styled.CancelButton styleType="ghost" onClick={onClose}>
1119
취소
1220
</Styled.CancelButton>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type SearchAreaProps = {
22
isOpen: boolean
33
onClose(): void
4+
onSubmitValue(value: string): void
45
}

src/components/common/Header/index.tsx

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Avatar, Divider, IconButton, Button } from '@offer-ui/react'
2+
import { useSetAtom } from 'jotai'
23
import Image from 'next/image'
34
import Link from 'next/link'
45
import { useRouter } from 'next/router'
@@ -9,45 +10,69 @@ import { SideBar } from './SideBar'
910
import { Styled } from './styled'
1011
import { CommonModal } from '../CommonModal'
1112
import { Dialog } from '../Dialog'
12-
import { OAUTH_URL } from '@constants/oauth'
13-
import { useAuth } from '@hooks/useAuth'
14-
import { IMAGE } from '@constants'
15-
import { useModal } from '@hooks'
13+
import { toQueryString } from '@utils/format'
14+
import { searchKeywordAtom } from '@atoms'
15+
import { IMAGE, OAUTH_URL } from '@constants'
16+
import { useModal, useAuth } from '@hooks'
1617

1718
const PREVENT_ACTIVE_PATHS = ['/post']
19+
const initDialog = {
20+
logout: false,
21+
search: false
22+
}
1823

1924
const Header = (): ReactElement => {
2025
const router = useRouter()
2126
const isActivePath = !PREVENT_ACTIVE_PATHS.includes(router.pathname)
2227
const { isLogin, user, handleLogout } = useAuth()
2328
const { isOpen, openModal, closeModal } = useModal()
2429
const [isOpenSideBar, setIsOpenSideBar] = useState(false)
25-
const [isOpenDialog, setIsOpenDialog] = useState({
26-
logout: false,
27-
search: false
28-
})
30+
const [isOpenDialog, setIsOpenDialog] = useState(initDialog)
31+
const setSearchKeyword = useSetAtom(searchKeywordAtom)
2932

3033
const handleOpenLoginModal = () => {
3134
openModal()
3235
}
3336

37+
const handleSubmitValue = (value?: string) => {
38+
if (value) {
39+
setSearchKeyword(value)
40+
router.push(
41+
`/result?${toQueryString({
42+
searchKeyword: value
43+
})}`
44+
)
45+
}
46+
}
47+
3448
const handleClickLogin = () => {
3549
router.replace(OAUTH_URL.KAKAO)
3650
}
3751

52+
const handleClickLogo = () => {
53+
setIsOpenDialog(initDialog)
54+
router.push('/')
55+
}
56+
57+
const handleClickSideBar = () => {
58+
setIsOpenDialog(initDialog)
59+
setIsOpenSideBar(true)
60+
}
61+
3862
return (
3963
<>
4064
<Styled.HeaderWrapper>
4165
<Styled.HeaderContent>
4266
<Styled.LogoInputSection>
43-
<Link href="/">
44-
<Styled.LogoButton styleType="ghost">
45-
<Image alt="Logo" height={40} src={IMAGE.LOGO} width={72} />
46-
</Styled.LogoButton>
47-
</Link>
67+
<Styled.LogoButton styleType="ghost" onClick={handleClickLogo}>
68+
<Image alt="Logo" height={40} src={IMAGE.LOGO} width={72} />
69+
</Styled.LogoButton>
4870
{isActivePath && (
4971
<Styled.InputWrapper>
50-
<Styled.SearchInput placeholder="검색어를 입력하세요" />
72+
<Styled.SearchInput
73+
placeholder="검색어를 입력하세요"
74+
onSubmitValue={handleSubmitValue}
75+
/>
5176
</Styled.InputWrapper>
5277
)}
5378
</Styled.LogoInputSection>
@@ -134,11 +159,7 @@ const Header = (): ReactElement => {
134159
})
135160
}
136161
/>
137-
<IconButton
138-
icon="menu"
139-
size={24}
140-
onClick={() => setIsOpenSideBar(true)}
141-
/>
162+
<IconButton icon="menu" size={24} onClick={handleClickSideBar} />
142163
</Styled.MenuSection>
143164
</Styled.HeaderContent>
144165
</Styled.HeaderWrapper>
@@ -151,6 +172,7 @@ const Header = (): ReactElement => {
151172
search: false
152173
})
153174
}
175+
onSubmitValue={handleSubmitValue}
154176
/>
155177
)}
156178
<SideBar

src/components/home/ProductItem/styled.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import { Image, ToggleButton } from '@offer-ui/react'
55
const Container = styled.div`
66
flex: 1 0 22%;
77
8+
max-width: 22%;
9+
810
cursor: pointer;
911
1012
${({ theme }) => theme.mediaQuery.mobile} {
1113
flex: 1 0 44%;
14+
15+
max-width: 44%;
1216
}
1317
`
1418

src/components/home/ProductList/styled.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,13 @@ const ProductListWrapper = styled.div`
1818
display: flex;
1919
flex-wrap: wrap;
2020
gap: 30px;
21+
justify-content: start;
2122
2223
${({ theme }): string => theme.mediaQuery.tablet} {
23-
grid-template-columns: repeat(4, minmax(10%, 166px));
2424
gap: 18px 20px;
25-
justify-content: center;
2625
}
2726
${({ theme }): string => theme.mediaQuery.mobile} {
28-
grid-template-columns: repeat(2, minmax(30%, 200px));
2927
gap: 15px 50px;
30-
justify-content: center;
31-
}
32-
33-
@media (width <= 510px) {
34-
grid-template-columns: repeat(2, minmax(10%, 160px));
35-
gap: 8px 20px;
36-
justify-content: center;
3728
}
3829
`
3930

src/components/post/PriceOfferCard/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const PriceOfferCard = ({
110110
})
111111

112112
router.push(
113-
`/messagebox${toQueryString({
113+
`/messagebox?${toQueryString({
114114
roomId: res.id
115115
})}`
116116
)

src/components/result/PriceDialog/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export type PriceRange = {
2-
min: number
2+
min?: number
33
max?: number
44
}
55

src/components/result/SearchOptions/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type SearchOptionsState = {
55
category?: string
66
tradeType?: TradeTypeCodes
77
priceRange: {
8-
min: number
8+
min?: number
99
max?: number
1010
}
1111
}

src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './images'
33
export * from './app'
44
export * from './env'
55
export * from './message'
6+
export * from './oauth'

src/constants/oauth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const OAUTH_ENDPOINT = {
66
}
77

88
export const OAUTH_URL = {
9-
KAKAO: `${OAUTH_ENDPOINT.KAKAO}${toQueryString({
9+
KAKAO: `${OAUTH_ENDPOINT.KAKAO}?${toQueryString({
1010
response_type: 'code',
1111
client_id: env.KAKAO_REST_API_KEY || '',
1212
redirect_uri: env.KAKAO_REDIRECT_URI || ''

src/pages/messagebox/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const MessageBoxPage = ({ roomId: defaultRoomId }: Props): ReactElement => {
5555
setRoomId(id)
5656

5757
router.push(
58-
`/messagebox${toQueryString({
58+
`/messagebox?${toQueryString({
5959
roomId: String(id)
6060
})}`
6161
)

src/pages/post/[postId]/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ const PostDetailPage = ({ postId }: Props): ReactElement => {
113113
onClose={tradeStatusDialog.closeModal}>
114114
<DialogButtonContainer>
115115
<Link
116-
href={`/post${toQueryString({
116+
href={`/post?${toQueryString({
117117
type: 'edit',
118118
postId
119119
})}`}>

src/pages/result/index.tsx

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,99 @@
11
import styled from '@emotion/styled'
2-
import type { NextPage } from 'next'
2+
import { useAtomValue } from 'jotai'
3+
import type { GetServerSideProps, NextPage } from 'next'
4+
import { useRouter } from 'next/router'
35
import { useState } from 'react'
46
import type {
57
SearchOptionsState,
68
OnChangeSearchOptions
79
} from '@components/result/SearchOptions/types'
810
import { useGetCategoriesQuery, useGetInfinitePostsQuery } from '@apis'
11+
import { searchKeywordAtom } from '@atoms'
912
import {
1013
SearchOptions,
1114
ResultHeader,
1215
CategorySlideFilter,
1316
ProductList
1417
} from '@components'
18+
import type { SortOptionCodes, TradeTypeCodes } from '@types'
19+
import { toQueryString, removeNullish } from '@utils'
1520

1621
const DEFAULT_POST_PAGE_NUMBER = 8
1722

18-
const ResultPage: NextPage = () => {
23+
type ResultPageProps = {
24+
keyword?: string
25+
category?: string | null
26+
sort?: SortOptionCodes
27+
minPrice?: number
28+
maxPrice?: number
29+
tradeType?: TradeTypeCodes
30+
}
31+
export const getServerSideProps: GetServerSideProps<ResultPageProps> = async ({
32+
query
33+
}) => ({
34+
props: {
35+
keyword: (query.keyword as string) || '',
36+
category: (query.category as string) || null,
37+
sort: (query.sort as SortOptionCodes) || 'CREATED_DATE_DESC',
38+
minPrice: Number(query.min_price),
39+
maxPrice: Number(query.max_price),
40+
tradeType: (query.tradeType as TradeTypeCodes) || null
41+
}
42+
})
43+
44+
const ResultPage: NextPage = ({
45+
keyword,
46+
category,
47+
sort,
48+
minPrice,
49+
maxPrice,
50+
tradeType
51+
}: ResultPageProps) => {
1952
const getCategoriesQuery = useGetCategoriesQuery()
20-
const categories =
21-
getCategoriesQuery.data?.map(({ code, name }) => ({ code, name })) || []
53+
const router = useRouter()
54+
const searchKeyword = useAtomValue(searchKeywordAtom)
2255
const [searchOptions, setSearchOptions] = useState<SearchOptionsState>({
2356
sort: 'CREATED_DATE_DESC',
24-
priceRange: {
25-
min: 0
26-
}
57+
priceRange: {}
2758
})
59+
const currentKeyword = searchKeyword ?? keyword
60+
const searchParams = removeNullish({
61+
category: searchOptions?.category ?? category,
62+
minPrice: searchOptions.priceRange?.min ?? minPrice,
63+
maxPrice: searchOptions.priceRange?.max ?? maxPrice,
64+
tradeType: searchOptions.tradeType ?? tradeType,
65+
sort: searchOptions.sort ?? sort,
66+
searchKeyword: currentKeyword
67+
})
68+
69+
const categories =
70+
getCategoriesQuery.data?.map(({ code, name }) => ({ code, name })) || []
71+
2872
const infinitePosts = useGetInfinitePostsQuery({
2973
lastId: null,
3074
limit: DEFAULT_POST_PAGE_NUMBER,
31-
category: searchOptions?.category,
32-
minPrice: searchOptions.priceRange?.min,
33-
maxPrice: searchOptions.priceRange?.max,
34-
tradeType: searchOptions.tradeType,
35-
sort: searchOptions.sort
75+
...searchParams
3676
})
3777
// TODO: 포스트 전체 갯수 내려달라고 요청해놓았습니다
3878
const postsCount = 0
3979

4080
const handleChangeSearchOptions: OnChangeSearchOptions = (name, value) => {
41-
setSearchOptions(prev => ({
42-
...prev,
81+
const nextSearchOptions = {
82+
...searchOptions,
4383
[name]: value
44-
}))
84+
}
85+
setSearchOptions(nextSearchOptions)
86+
87+
router.push(`/result?${toQueryString(searchParams)}`)
4588
}
4689

4790
return (
4891
<Layout>
4992
<ResultWrapper>
50-
<ResultHeader postsCount={postsCount} searchResult="###" />
93+
<ResultHeader
94+
postsCount={postsCount}
95+
searchResult={currentKeyword || ''}
96+
/>
5197
<CategorySliderWrapper>
5298
<CategorySlideFilter
5399
categories={categories}

src/utils/format/format.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ describe('format/toQueryString', () => {
2525
const result = toQueryString(object)
2626

2727
// Then
28-
expect(result).toBe('?content_id=123&type=user')
28+
expect(result).toBe('content_id=123&type=user')
2929
})
3030
})

0 commit comments

Comments
 (0)