-
Notifications
You must be signed in to change notification settings - Fork 0
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: search suggestion api #48
Merged
Merged
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
f59bf48
feat: add haversine-distance
poiu694 fef3803
feat: add debounce util
poiu694 9511480
feat: map bound session storage
poiu694 47d09a1
feat: scaffold search api
poiu694 fb0d375
refactor: debounce to lodash lib
poiu694 3f1f3ca
chore: delete haversin distance lib
poiu694 cc70f2e
feat: message with data response api type
poiu694 c7788f9
feat: location util functions, getDistance, formatDistance
poiu694 4625544
feat: add search api(suggestKeyword, searchPlace)
poiu694 1406144
feat: use user geo location hook
poiu694 6051d16
feat: focus input on mount timing
poiu694 8a468a9
feat: attach api to auto suggestion search
poiu694 8d1e826
chore: resolve build error by api, component
poiu694 d9c9a32
chore: add eol in package json
poiu694 30e473c
fix: change api host to api origin
poiu694 c63563a
chore: delete console log in service
poiu694 37b0f54
refactor: add unique logic in place-auto-search-item
poiu694 db53d95
refactor: show 0m in place-auto-search-item
poiu694 6d44e65
chore: rename numOfReview to numOfReviews
poiu694 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
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,145 @@ | ||
'use client' | ||
|
||
import debounce from 'lodash.debounce' | ||
import { useCallback, useState } from 'react' | ||
import { usePathname, useRouter, useSearchParams } from 'next/navigation' | ||
|
||
import { api } from '@/utils/api' | ||
import SearchForm from './search-form' | ||
import { Typography } from '@/components' | ||
import RecentKeywords from './recent-keywords' | ||
import SuggestPlaceList from './suggest-place-list' | ||
import { formatBoundToRect } from '@/utils/location' | ||
import type { KakaoPlaceItem } from '@/types/map/kakao-raw-type' | ||
import { mapBoundSessionStorage, recentSearchStorage } from '@/utils/storage' | ||
import { useIsomorphicLayoutEffect } from '@/hooks/use-isomorphic-layout-effect' | ||
|
||
const SearchBox = () => { | ||
const router = useRouter() | ||
const pathname = usePathname() | ||
const searchParams = useSearchParams() | ||
const search = searchParams.get('search') ?? '' | ||
|
||
const mapBounds = mapBoundSessionStorage.getValueOrNull() | ||
const [recentKeywords, setRecentKeywords] = useState( | ||
recentSearchStorage.getValueOrNull() ?? [], | ||
) | ||
const [query, setQuery] = useState(search) | ||
const [suggestedPlaces, setSuggestedPlaces] = useState<KakaoPlaceItem[]>([]) | ||
const isShowRecentKeywords = | ||
query === '' && !!recentKeywords.length && search === '' | ||
const isShowResultPlaces = search !== '' | ||
const isShowSuggestionPlaces = !isShowRecentKeywords && !isShowResultPlaces | ||
|
||
const createQueryString = (key: string, value: string) => { | ||
const params = new URLSearchParams(searchParams.toString()) | ||
params.set(key, value) | ||
|
||
return params.toString() | ||
} | ||
|
||
const addUniqueKeyword = (keyword: string) => { | ||
const existRecentKeywords = [...(recentKeywords || [])] | ||
|
||
const keywordIndex = existRecentKeywords.indexOf(keyword) | ||
if (keywordIndex !== -1) { | ||
existRecentKeywords.splice(keywordIndex, 1) | ||
} | ||
recentSearchStorage.set([...existRecentKeywords, keyword]) | ||
setRecentKeywords([keyword, ...existRecentKeywords]) | ||
} | ||
|
||
const searchByKeyword = (formDataOrKeyword: FormData | string) => { | ||
const searchKeyword = | ||
typeof formDataOrKeyword === 'string' | ||
? formDataOrKeyword | ||
: (formDataOrKeyword.get('query') as string | null) | ||
if (searchKeyword) { | ||
createQueryString('search', searchKeyword) | ||
addUniqueKeyword(searchKeyword) | ||
setQuery(searchKeyword) | ||
router.push(`${pathname}?${createQueryString('search', searchKeyword)}`) | ||
} | ||
} | ||
|
||
const deleteRecentKeyword = (targetIndex: number) => { | ||
const existRecentKeywords = [...(recentKeywords || [])] | ||
|
||
if (targetIndex !== -1) { | ||
existRecentKeywords.splice(targetIndex, 1) | ||
recentSearchStorage.set(existRecentKeywords) | ||
setRecentKeywords(existRecentKeywords) | ||
} | ||
} | ||
|
||
const handleResetQuery = () => { | ||
setQuery('') | ||
createQueryString('search', '') | ||
router.push(`${pathname}?${createQueryString('search', '')}`) | ||
} | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
// search와 query 동기화 (삭제, 브라우저 뒤로가기/앞으로가기 등 대응) | ||
setQuery(search) | ||
}, [search]) | ||
|
||
const getSuggestPlaces = useCallback(async () => { | ||
if (!query) return | ||
|
||
try { | ||
const res = await api.search.searchPlaces({ | ||
q: query, | ||
rect: formatBoundToRect(mapBounds), | ||
}) | ||
setSuggestedPlaces(res.data) | ||
} catch (err) { | ||
// TODO: Error 처리 | ||
} | ||
}, [mapBounds, query]) | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
const debounceGetSuggestPlaces = debounce(getSuggestPlaces, 500) | ||
|
||
debounceGetSuggestPlaces() | ||
|
||
return () => { | ||
debounceGetSuggestPlaces.cancel() | ||
} | ||
}, [query]) | ||
|
||
return ( | ||
<div className="w-full min-h-dvh bg-neutral-700 px-5 py-2"> | ||
<SearchForm | ||
value={query} | ||
onChange={(e) => setQuery(e.target.value)} | ||
onResetValue={handleResetQuery} | ||
onSubmit={searchByKeyword} | ||
/> | ||
|
||
{isShowRecentKeywords && ( | ||
<RecentKeywords | ||
recentKeywords={recentKeywords} | ||
onSearchKeyword={searchByKeyword} | ||
onDeleteKeyword={deleteRecentKeyword} | ||
/> | ||
)} | ||
|
||
{isShowSuggestionPlaces && | ||
(suggestedPlaces.length > 0 ? ( | ||
<SuggestPlaceList places={suggestedPlaces} query={query} /> | ||
) : ( | ||
<Typography | ||
size="body2" | ||
color="neutral-200" | ||
className="flex justify-center mt-[112px]" | ||
> | ||
검색 결과가 없습니다. | ||
</Typography> | ||
))} | ||
|
||
{isShowResultPlaces && <div>Result</div>} | ||
</div> | ||
) | ||
} | ||
|
||
export default SearchBox |
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
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,40 @@ | ||
import { formatDistance, getDistance } from '@/utils/location' | ||
import useUserGeoLocation from '@/hooks/use-user-geo-location' | ||
import type { KakaoPlaceItem } from '@/types/map/kakao-raw-type' | ||
import PlaceAutoSearchItem from '@/components/place/place-auto-search-item' | ||
|
||
interface SuggestPlaceListProps { | ||
places: KakaoPlaceItem[] | ||
query: string | ||
} | ||
|
||
const SuggestPlaceList = ({ places, query }: SuggestPlaceListProps) => { | ||
const userLocation = useUserGeoLocation() | ||
|
||
return ( | ||
<ul className="flex flex-col space-y-[13px] divide-y divide-neutral-600 mx-[-20px]"> | ||
{places.map((place) => { | ||
console.log(userLocation, place.x, place.y) | ||
poiu694 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const diffDistance = getDistance( | ||
userLocation.latitude, | ||
userLocation.longitude, | ||
place.y, | ||
place.x, | ||
) | ||
|
||
return ( | ||
<PlaceAutoSearchItem | ||
key={place.id} | ||
{...place} | ||
className="px-5" | ||
query={query} | ||
distance={diffDistance === 0 ? '' : formatDistance(diffDistance)} | ||
poiu694 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
numOfReview={312} | ||
/> | ||
) | ||
})} | ||
</ul> | ||
) | ||
} | ||
|
||
export default SuggestPlaceList |
Oops, something went wrong.
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.
react.dev에서
useLayoutEffect
사용을 지양하라고 해서, 언제 사용해야 성능 이슈보다 이점이 더 클지 잘 감이 안 오더라구요useLayoutEffect
사용 기준을 따로 갖고 계신지 궁금합니다!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.
useLayoutEffect는 레이아웃 관련 작업이나 깜박임 또는 레이아웃이 깨지는 문제를 방지할 때 말고 잘 안쓰긴 했는데, 여기선 Next라 useIsomorphicLayoutEffect로 넣긴 했네요!
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.
결국
useIsomorphicLayoutEffect
를 쓴다는 건 client side에서useLayoutEffect
를 사용하겠다는 건데, 사용했을 때 깜빡임이 확실하게 사라지거나 더 빠르게 그려지는게 확실하지 않다보니, 성능 이슈를 고려해서 사용 기준을 명확하게 정해보는 것도 좋을 것 같아요!