From 55a290c5936e40eb80e311752a7cd376ae291975 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 17:32:16 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9E=A5=EC=86=8C=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20API=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_constants/path.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/app/_constants/path.ts b/apps/web/app/_constants/path.ts index fa65cd6..ac9cfa8 100644 --- a/apps/web/app/_constants/path.ts +++ b/apps/web/app/_constants/path.ts @@ -26,6 +26,7 @@ export const API_PATH = { POST: (id: string) => `/places/${id}/like`, DELETE: (id: string) => `/places/${id}/like`, }, + SEARCH: (keyword: string) => `/places/search?keyword=${keyword}`, }, KAKAO: { SEARCH: (query: string, categoryCode: string, x: number, y: number) => From 4be3dbc67ac43a090e1ce9794353aeae6598d2ea Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 17:33:58 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20getPlacesBySearch=20api=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/services/place.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/app/_apis/services/place.ts b/apps/web/app/_apis/services/place.ts index bde879c..8b33f0a 100644 --- a/apps/web/app/_apis/services/place.ts +++ b/apps/web/app/_apis/services/place.ts @@ -82,6 +82,14 @@ export const getSearchPlaceByKakao = async ({ return data } +// 타입 변경 필요 +export const getPlacesBySearch = async ( + keyword: string, +): Promise => { + const { data } = await axiosInstance.get(API_PATH.PLACES.SEARCH(keyword)) + return data +} + export const getPlaceByPreview = async ( kakaoPlaceId: string, ): Promise => { From 70a4d0028ff471f2c5474badbf2c17dfe268ca3e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 18:10:10 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=EB=94=94=EB=B0=94=EC=9A=B4?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=A0=81=EC=9A=A9=ED=95=9C=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=EA=B2=80=EC=83=89=20API=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EC=9A=A9=20useSearch=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_hooks/useSearch.ts | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 apps/web/app/_hooks/useSearch.ts diff --git a/apps/web/app/_hooks/useSearch.ts b/apps/web/app/_hooks/useSearch.ts new file mode 100644 index 0000000..14515fa --- /dev/null +++ b/apps/web/app/_hooks/useSearch.ts @@ -0,0 +1,55 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +/** + * - 입력된 query를 기준으로 장소 검색 API 호출 + * - 300ms 디바운싱 적용 (빠른 입력 시 불필요한 요청 방지) + * - API 결과를 상태로 관리하여 컴포넌트에서 바로 사용 가능 + * + * @example + * const { searchResult: cafeResult, searchFunc: cafeSearchFunc } = useSearch< + * SearchPlace, + * KakaoSearchFuncParams + * >(getSearchPlaceByKakao) + * + * + * @template T 검색 결과 아이템의 타입 (예: SearchPlace) + * @template P 검색 함수에 들어갈 파라미터 타입 (예: KakaoSearchFuncParams) + * @param fetcher Promise를 반환하는 비동기 검색 함수 + * + * @returns searchResult - 검색된 장소 리스트 + * @returns searchFunc - 검색을 수행하는 함수 + */ + +const useSearch = (fetcher: (params: P) => Promise) => { + const [searchResult, setSearchResult] = useState([]) + const timeoutRef = useRef | null>(null) + + const searchFunc = useCallback( + (params: P) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + + timeoutRef.current = setTimeout(async () => { + try { + // 주입받은 fetcher 실행 + const result = await fetcher(params) + setSearchResult(result) + } catch (error) { + console.error('Search failed:', error) + setSearchResult([]) + } + }, 300) + }, + [fetcher], // fetcher가 변경되면 함수 재생성 + ) + + // 언마운트 시 타이머 클리어 + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + } + }, []) + + return { searchResult, searchFunc } +} + +export default useSearch From 5b4c56cf0c97d3af0642cd462c360d5e4bfc63dd Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 18:14:29 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor:=20useSearchPlaceByKakao=20hook?= =?UTF-8?q?=EC=9D=84=20useSearch=20hook=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20kakaoSearch=20api=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/schemas/kakaoSearch.ts | 21 +++++ apps/web/app/_apis/services/kakaoSearch.ts | 25 ++++++ apps/web/app/_apis/services/place.ts | 24 ----- apps/web/app/_hooks/useSearchPlaceByKakao.ts | 89 ------------------- .../Step/PlaceSearch/PlaceSearch.tsx | 27 +++--- 5 files changed, 59 insertions(+), 127 deletions(-) create mode 100644 apps/web/app/_apis/schemas/kakaoSearch.ts create mode 100644 apps/web/app/_apis/services/kakaoSearch.ts delete mode 100644 apps/web/app/_hooks/useSearchPlaceByKakao.ts diff --git a/apps/web/app/_apis/schemas/kakaoSearch.ts b/apps/web/app/_apis/schemas/kakaoSearch.ts new file mode 100644 index 0000000..45f4b96 --- /dev/null +++ b/apps/web/app/_apis/schemas/kakaoSearch.ts @@ -0,0 +1,21 @@ +export const KAKAO_CATEGORY_CODE = { + restaurant: 'FD6', + cafe: 'CE7', +} + +export type KakaoSearchFuncParams = { + query: string + categoryCode: KakaoCategoryCode + location: { x: number; y: number } +} + +export type SearchPlaceByKakao = { + id: string + place_name: string + x: string + y: string + road_address_name?: string + address_name: string +} + +export type KakaoCategoryCode = keyof typeof KAKAO_CATEGORY_CODE diff --git a/apps/web/app/_apis/services/kakaoSearch.ts b/apps/web/app/_apis/services/kakaoSearch.ts new file mode 100644 index 0000000..aa3ebdd --- /dev/null +++ b/apps/web/app/_apis/services/kakaoSearch.ts @@ -0,0 +1,25 @@ +import axios from 'axios' +import { API_PATH } from '@/_constants/path' +import { + KAKAO_CATEGORY_CODE, + KakaoSearchFuncParams, +} from '@/_apis/schemas/kakaoSearch' + +export const getSearchPlaceByKakao = async ({ + query, + categoryCode, + location, +}: KakaoSearchFuncParams) => { + const KAKAO_API_KEY = process.env.NEXT_PUBLIC_KAKAO_API || '' + const { x, y } = location + + const { data } = await axios.get( + API_PATH.KAKAO.SEARCH(query, KAKAO_CATEGORY_CODE[categoryCode], x, y), + { + headers: { + Authorization: `KakaoAK ${KAKAO_API_KEY}`, + }, + }, + ) + return data.documents +} diff --git a/apps/web/app/_apis/services/place.ts b/apps/web/app/_apis/services/place.ts index 8b33f0a..4e5e6aa 100644 --- a/apps/web/app/_apis/services/place.ts +++ b/apps/web/app/_apis/services/place.ts @@ -1,4 +1,3 @@ -import axios from 'axios' import axiosInstance from '@/_lib/axiosInstance' import { API_PATH } from '@/_constants/path' import type { CampusType } from '@/_constants/campus' @@ -16,10 +15,6 @@ import { PlaceDetailSchema, PlaceByPreviewSchema, } from '../schemas/place' -import { - type KakaoSearchFuncParams, - KAKAO_CATEGORY_CODE, -} from '@/_hooks/useSearchPlaceByKakao' export const getPlacesByRanking = async ( sort: RankingPlaceSort, @@ -63,25 +58,6 @@ export const getPlaceDetail = async (id: string): Promise => { return PlaceDetailSchema.parse(data) } -export const getSearchPlaceByKakao = async ({ - query, - categoryCode, - location, -}: KakaoSearchFuncParams) => { - const KAKAO_API_KEY = process.env.NEXT_PUBLIC_KAKAO_API || '' - const { x, y } = location - - const { data } = await axios.get( - API_PATH.KAKAO.SEARCH(query, KAKAO_CATEGORY_CODE[categoryCode], x, y), - { - headers: { - Authorization: `KakaoAK ${KAKAO_API_KEY}`, - }, - }, - ) - return data -} - // 타입 변경 필요 export const getPlacesBySearch = async ( keyword: string, diff --git a/apps/web/app/_hooks/useSearchPlaceByKakao.ts b/apps/web/app/_hooks/useSearchPlaceByKakao.ts deleted file mode 100644 index e28c76d..0000000 --- a/apps/web/app/_hooks/useSearchPlaceByKakao.ts +++ /dev/null @@ -1,89 +0,0 @@ -'use client' - -import { useState, useRef, useEffect, useCallback } from 'react' -import { getSearchPlaceByKakao } from '@/_apis/services/place' - -/** - * 검색된 장소 정보를 나타내는 인터페이스 - */ -export interface SearchPlace { - id: string - place_name: string - x: string - y: string - road_address_name?: string - address_name: string -} - -export const KAKAO_CATEGORY_CODE = { - restaurant: 'FD6', - cafe: 'CE7', -} - -export type KakaoCategoryCode = keyof typeof KAKAO_CATEGORY_CODE -export type KakaoSearchFuncParams = { - query: string - categoryCode: KakaoCategoryCode - location: { x: number; y: number } -} - -/** - * Kakao Local API를 이용한 장소 검색을 수행하는 커스텀 훅 - * - * - 입력된 query를 기준으로 장소 검색 API 호출 - * - 300ms 디바운싱 적용 (빠른 입력 시 불필요한 요청 방지) - * - API 결과를 상태로 관리하여 컴포넌트에서 바로 사용 가능 - * - * @returns 훅이 반환하는 값 - * @returns searchResult - 검색된 장소 리스트 - * @returns searchFunc - 검색을 수행하는 함수 - * - * @example - * const { searchResult, searchFunc } = useSearchData() - * - * // 인풋 변경 시 검색 실행 - * searchFunc(e.target.value)} /> - * - * // 검색된 결과 출력 - * {searchResult.map(place => ( - *
{place.place_name}
- * ))} - */ -export const useSearchPlaceByKakao = () => { - const [searchResult, setSearchResult] = useState([]) - const timeoutRef = useRef(null) - - /** - * 검색 함수 (300ms 디바운스 적용) - * @param query 검색 키워드 - */ - const searchFunc = useCallback( - async ({ query, categoryCode, location }: KakaoSearchFuncParams) => { - if (timeoutRef.current) clearTimeout(timeoutRef.current) - - timeoutRef.current = setTimeout(async () => { - try { - const result = await getSearchPlaceByKakao({ - query, - categoryCode, - location, - }) - setSearchResult([...result.documents]) - } catch (error) { - console.error(error) - setSearchResult([]) - } - }, 300) - }, - [], - ) - - // 언마운트 시 타이머 클리어 - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current) - } - }, []) - - return { searchResult, searchFunc } -} diff --git a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx index d0377f3..a3142e9 100644 --- a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx +++ b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx @@ -1,6 +1,11 @@ import { useCallback } from 'react' import { SearchPage } from '@/_components/SearchPage' -import { useSearchPlaceByKakao } from '@/_hooks/useSearchPlaceByKakao' +import useSearch from '@/_hooks/useSearch' +import { getSearchPlaceByKakao } from '@/_apis/services/kakaoSearch' +import type { + KakaoSearchFuncParams, + SearchPlaceByKakao, +} from '@/_apis/schemas/kakaoSearch' import type { UseFormSetValue } from 'react-hook-form' import type { NewPlaceRequest } from '@/_apis/schemas/place' import { type CampusType, CAMPUS_LOCATION } from '@/_constants/campus' @@ -13,9 +18,11 @@ type Props = { export const PlaceSearch = ({ campus, setValue, nextStep }: Props) => { const { searchResult: restaurantResult, searchFunc: restaurantSearchFunc } = - useSearchPlaceByKakao() - const { searchResult: cafeResult, searchFunc: cafeSearchFunc } = - useSearchPlaceByKakao() + useSearch(getSearchPlaceByKakao) + const { searchResult: cafeResult, searchFunc: cafeSearchFunc } = useSearch< + SearchPlaceByKakao, + KakaoSearchFuncParams + >(getSearchPlaceByKakao) const places = [...restaurantResult, ...cafeResult].map((item) => ({ id: item.id, @@ -26,17 +33,9 @@ export const PlaceSearch = ({ campus, setValue, nextStep }: Props) => { const searchFunc = useCallback( (query: string) => { const { longitude: x, latitude: y } = CAMPUS_LOCATION[campus] - const location = { - x, - y, - } - + const location = { x, y } cafeSearchFunc({ query, categoryCode: 'cafe', location }) - restaurantSearchFunc({ - query, - categoryCode: 'restaurant', - location, - }) + restaurantSearchFunc({ query, categoryCode: 'restaurant', location }) }, [cafeSearchFunc, campus, restaurantSearchFunc], ) From 49d4650abfb7d1700e15675179816328fc8a8f60 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 18:16:32 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=B0=8F=20=EA=B2=80=EC=83=89=20api=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_hooks/useSearch.ts | 4 +-- .../Step/PlaceSearch/PlaceSearch.tsx | 2 +- apps/web/app/places/search/page.tsx | 33 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 apps/web/app/places/search/page.tsx diff --git a/apps/web/app/_hooks/useSearch.ts b/apps/web/app/_hooks/useSearch.ts index 14515fa..c2d1aa9 100644 --- a/apps/web/app/_hooks/useSearch.ts +++ b/apps/web/app/_hooks/useSearch.ts @@ -20,7 +20,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' * @returns searchFunc - 검색을 수행하는 함수 */ -const useSearch = (fetcher: (params: P) => Promise) => { +export const useSearch = (fetcher: (params: P) => Promise) => { const [searchResult, setSearchResult] = useState([]) const timeoutRef = useRef | null>(null) @@ -51,5 +51,3 @@ const useSearch = (fetcher: (params: P) => Promise) => { return { searchResult, searchFunc } } - -export default useSearch diff --git a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx index a3142e9..8fa16cf 100644 --- a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx +++ b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { SearchPage } from '@/_components/SearchPage' -import useSearch from '@/_hooks/useSearch' +import { useSearch } from '@/_hooks/useSearch' import { getSearchPlaceByKakao } from '@/_apis/services/kakaoSearch' import type { KakaoSearchFuncParams, diff --git a/apps/web/app/places/search/page.tsx b/apps/web/app/places/search/page.tsx new file mode 100644 index 0000000..cc361b4 --- /dev/null +++ b/apps/web/app/places/search/page.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { SearchPage } from '@/_components/SearchPage' +import { getPlacesBySearch } from '@/_apis/services/place' +import { useSearch } from '@/_hooks/useSearch' +import { CLIENT_PATH } from '@/_constants/path' +import type { PlaceDetail } from '@/_apis/schemas/place' + +const Page = () => { + const { replace } = useRouter() + const { searchResult, searchFunc } = useSearch( + getPlacesBySearch, + ) + + const newPlaces = searchResult.map((place) => ({ + id: place.placeId, + name: place.placeName, + address: place.address, + })) + + return ( + { + replace(CLIENT_PATH.PLACE_DETAIL(id)) + }} + places={newPlaces} + /> + ) +} + +export default Page From 1e12a95a07c8c73cdb39a4d6853461395232ea3b Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 23 Nov 2025 18:17:49 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/places/[id]/PlaceDetailPage.tsx | 31 +++++++++---------- .../Step/PlacePreview/PlacePreview.tsx | 1 + packages/ui/src/components/Banner/Banner.tsx | 14 +++++++-- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/web/app/places/[id]/PlaceDetailPage.tsx b/apps/web/app/places/[id]/PlaceDetailPage.tsx index 9311d57..d7322de 100644 --- a/apps/web/app/places/[id]/PlaceDetailPage.tsx +++ b/apps/web/app/places/[id]/PlaceDetailPage.tsx @@ -23,23 +23,20 @@ export const PlaceDetailPage = ({ id }: { id: string }) => { right={} /> - {photos.length > 0 && ( - ( - place-photo - ))} - minHeight={180} - showIndicator={true} - /> - )} - + ( + place-photo + ))} + minHeight={180} + showIndicator={true} + /> diff --git a/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx b/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx index e696f22..e3ed6af 100644 --- a/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx +++ b/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx @@ -60,6 +60,7 @@ export const PlacePreview = ({ getValues, setValue, nextStep }: Props) => { className={'max-h-[180px] object-contain'} /> ))} + showIndicator={true} minHeight={180} /> diff --git a/packages/ui/src/components/Banner/Banner.tsx b/packages/ui/src/components/Banner/Banner.tsx index c95fda9..7dc6ecd 100644 --- a/packages/ui/src/components/Banner/Banner.tsx +++ b/packages/ui/src/components/Banner/Banner.tsx @@ -56,16 +56,21 @@ export const Banner = ({ (slider) => { let timeout: ReturnType let mouseOver = false + function clearNextTimeout() { clearTimeout(timeout) } + function nextTimeout() { clearTimeout(timeout) if (mouseOver) return timeout = setTimeout(() => { - slider.next() + if (slider.track && slider.track.details) { + slider.next() + } }, 2000) } + slider.on('created', () => { slider.container.addEventListener('mouseover', () => { mouseOver = true @@ -80,10 +85,15 @@ export const Banner = ({ slider.on('dragStarted', clearNextTimeout) slider.on('animationEnded', nextTimeout) slider.on('updated', nextTimeout) + slider.on('destroyed', clearNextTimeout) }, ], ) + if (contents.length === 0) { + return null + } + return (
{currentSlide + 1} {` `}/{` `} - {instanceRef.current?.track.details.slides.length ?? 0} + {instanceRef.current.track?.details?.slides.length ?? 0}
)} From 02bb1240be5e94115a11644d54fcebad05522116 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 12:41:31 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EB=B0=8F=20Promise.all=20=EC=82=AC=EC=9A=A9=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95=20-=20=EA=B0=9C=EB=B3=84?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8D=98=20=EC=B9=B4=ED=8E=98/=EC=8B=9D=EB=8B=B9=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EC=9D=98=20Fetcher=20=ED=95=A8=EC=88=98=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20-=20=EB=91=90=20=EB=B2=88=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=A6=AC=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=20=EB=B2=88=EC=9C=BC=EB=A1=9C=20=EC=A4=84?= =?UTF-8?q?=EC=97=AC=20UI=20=EA=B9=9C=EB=B9=A1=EC=9E=84=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Step/PlaceSearch/PlaceSearch.tsx | 25 ++++++------------- .../new/_utils/searchCafeAndRestaurant.ts | 14 +++++++++++ 2 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts diff --git a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx index 8fa16cf..df38405 100644 --- a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx +++ b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx @@ -1,14 +1,10 @@ import { useCallback } from 'react' import { SearchPage } from '@/_components/SearchPage' import { useSearch } from '@/_hooks/useSearch' -import { getSearchPlaceByKakao } from '@/_apis/services/kakaoSearch' -import type { - KakaoSearchFuncParams, - SearchPlaceByKakao, -} from '@/_apis/schemas/kakaoSearch' import type { UseFormSetValue } from 'react-hook-form' import type { NewPlaceRequest } from '@/_apis/schemas/place' import { type CampusType, CAMPUS_LOCATION } from '@/_constants/campus' +import { searchCafeAndRestaurant } from '@/places/new/_utils/searchCafeAndRestaurant' type Props = { campus: CampusType @@ -17,33 +13,26 @@ type Props = { } export const PlaceSearch = ({ campus, setValue, nextStep }: Props) => { - const { searchResult: restaurantResult, searchFunc: restaurantSearchFunc } = - useSearch(getSearchPlaceByKakao) - const { searchResult: cafeResult, searchFunc: cafeSearchFunc } = useSearch< - SearchPlaceByKakao, - KakaoSearchFuncParams - >(getSearchPlaceByKakao) + const { searchResult, searchFunc } = useSearch(searchCafeAndRestaurant) - const places = [...restaurantResult, ...cafeResult].map((item) => ({ + const places = [...searchResult].map((item) => ({ id: item.id, name: item.place_name, address: item.address_name, })) - const searchFunc = useCallback( + const handleSearch = useCallback( (query: string) => { const { longitude: x, latitude: y } = CAMPUS_LOCATION[campus] - const location = { x, y } - cafeSearchFunc({ query, categoryCode: 'cafe', location }) - restaurantSearchFunc({ query, categoryCode: 'restaurant', location }) + searchFunc({ query, location: { x, y } }) }, - [cafeSearchFunc, campus, restaurantSearchFunc], + [campus, searchFunc], ) return ( { setValue('kakaoPlaceId', id) nextStep() diff --git a/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts b/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts new file mode 100644 index 0000000..586f589 --- /dev/null +++ b/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts @@ -0,0 +1,14 @@ +import { getSearchPlaceByKakao } from '@/_apis/services/kakaoSearch' + +export const searchCafeAndRestaurant = async (params: { + query: string + location: { x: number; y: number } +}) => { + const [cafes, restaurants] = await Promise.all([ + getSearchPlaceByKakao({ ...params, categoryCode: 'cafe' }), + getSearchPlaceByKakao({ ...params, categoryCode: 'restaurant' }), + ]) + + // 두 결과를 합쳐서 반환 + return [...cafes, ...restaurants] +} From b597cabec4e6f4018006e39da1d819c9fab0e6c2 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 14:33:46 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20PlaceBySearch=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/schemas/place.ts | 7 +++++++ apps/web/app/_apis/services/place.ts | 17 +++++++++-------- apps/web/app/places/search/page.tsx | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/web/app/_apis/schemas/place.ts b/apps/web/app/_apis/schemas/place.ts index c273c24..df1cfd9 100644 --- a/apps/web/app/_apis/schemas/place.ts +++ b/apps/web/app/_apis/schemas/place.ts @@ -37,6 +37,12 @@ export const PlaceByMapSchema = BasePlaceSchema.extend({ photos, }) +export const PlaceBySearchSchema = BasePlaceSchema.pick({ + placeId: true, + placeName: true, + address: true, +}) + export const PlaceDetailSchema = BasePlaceSchema.extend({ location, isLiked: z.boolean(), @@ -75,6 +81,7 @@ export type MapBounds = { export type BasePlace = z.infer export type PlaceByMap = z.infer +export type PlaceBySearch = z.infer export type PlaceDetail = z.infer export type PlaceByPreview = z.infer export type NewPlaceRequest = z.infer diff --git a/apps/web/app/_apis/services/place.ts b/apps/web/app/_apis/services/place.ts index 4e5e6aa..a46dae6 100644 --- a/apps/web/app/_apis/services/place.ts +++ b/apps/web/app/_apis/services/place.ts @@ -7,11 +7,13 @@ import { type PlaceDetail, type MapBounds, type PlaceByMap, + type PlaceBySearch, type PlaceByPreview, type NewPlaceRequest, type NewPlaceResponse, BasePlaceSchema, PlaceByMapSchema, + PlaceBySearchSchema, PlaceDetailSchema, PlaceByPreviewSchema, } from '../schemas/place' @@ -53,17 +55,16 @@ export const getPlacesByMap = async ({ return PlaceByMapSchema.array().parse(data) } -export const getPlaceDetail = async (id: string): Promise => { - const { data } = await axiosInstance.get(API_PATH.PLACES.DETAIL(id)) - return PlaceDetailSchema.parse(data) -} - -// 타입 변경 필요 export const getPlacesBySearch = async ( keyword: string, -): Promise => { +): Promise => { const { data } = await axiosInstance.get(API_PATH.PLACES.SEARCH(keyword)) - return data + return PlaceBySearchSchema.array().parse(data) +} + +export const getPlaceDetail = async (id: string): Promise => { + const { data } = await axiosInstance.get(API_PATH.PLACES.DETAIL(id)) + return PlaceDetailSchema.parse(data) } export const getPlaceByPreview = async ( diff --git a/apps/web/app/places/search/page.tsx b/apps/web/app/places/search/page.tsx index cc361b4..fb07d96 100644 --- a/apps/web/app/places/search/page.tsx +++ b/apps/web/app/places/search/page.tsx @@ -5,11 +5,11 @@ import { SearchPage } from '@/_components/SearchPage' import { getPlacesBySearch } from '@/_apis/services/place' import { useSearch } from '@/_hooks/useSearch' import { CLIENT_PATH } from '@/_constants/path' -import type { PlaceDetail } from '@/_apis/schemas/place' +import type { PlaceBySearch } from '@/_apis/schemas/place' const Page = () => { const { replace } = useRouter() - const { searchResult, searchFunc } = useSearch( + const { searchResult, searchFunc } = useSearch( getPlacesBySearch, ) From c628c5dc5f77916af1bc0a33cf5acdac5f13d8fa Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 14:56:19 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=EC=9E=A5=EC=86=8C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=8A=A4=ED=94=BC=EB=84=88=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20=EC=9E=A5=EC=86=8C=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=8B=9C=EA=B0=81=EC=A0=81=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=EC=9D=84=20=EC=9C=84=ED=95=B4=20`Sp?= =?UTF-8?q?inner`=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20handleInputChange=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/_components/SearchPage/SearchPage.tsx | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index 0fbd519..3fde4e4 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' +import { Spinner } from '@heroui/react' import { Icon } from '@repo/ui/components/Icon' import { Flex, VerticalScrollArea } from '@repo/ui/components/Layout' import { SearchPlaceListItem } from './SearchPlaceListItem' @@ -47,15 +48,31 @@ export const SearchPage = ({ useBackHandler = false, }: Props) => { const [inputValue, setInputValue] = useState('') + const [isNavigating, setIsNavigating] = useState(false) - useEffect(() => { - if (inputValue.length > 0) { - searchFunc(inputValue) + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setInputValue(value) + if (value.length > 0) { + searchFunc(value) } - }, [inputValue, searchFunc]) + } + + const handleSelect = (id: string) => { + setIsNavigating(true) + try { + onSelectPlace(id) + } catch (error) { + console.error(error) + setIsNavigating(false) + } + } return ( <> + {isNavigating && ( + + )} {useBackHandler ? ( @@ -64,7 +81,7 @@ export const SearchPage = ({ )} setInputValue(e.target.value)} + onChange={handleInputChange} className={'w-full text-lg font-medium outline-none'} placeholder={placeholder || '장소 또는 주소를 검색하세요'} /> @@ -76,7 +93,9 @@ export const SearchPage = ({ key={place.id} inputValue={inputValue} place={place} - onClick={() => onSelectPlace(place.id)} + onClick={() => { + handleSelect(place.id) + }} /> ))}
From 68b71af37ded147dccb427344a17ffdab23ca306 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 15:12:29 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EA=B2=80=EC=83=89=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/services/kakaoSearch.ts | 32 ++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/web/app/_apis/services/kakaoSearch.ts b/apps/web/app/_apis/services/kakaoSearch.ts index aa3ebdd..aefc83b 100644 --- a/apps/web/app/_apis/services/kakaoSearch.ts +++ b/apps/web/app/_apis/services/kakaoSearch.ts @@ -2,24 +2,34 @@ import axios from 'axios' import { API_PATH } from '@/_constants/path' import { KAKAO_CATEGORY_CODE, - KakaoSearchFuncParams, + type KakaoSearchFuncParams, + type SearchPlaceByKakao, } from '@/_apis/schemas/kakaoSearch' export const getSearchPlaceByKakao = async ({ query, categoryCode, location, -}: KakaoSearchFuncParams) => { +}: KakaoSearchFuncParams): Promise => { const KAKAO_API_KEY = process.env.NEXT_PUBLIC_KAKAO_API || '' const { x, y } = location - - const { data } = await axios.get( - API_PATH.KAKAO.SEARCH(query, KAKAO_CATEGORY_CODE[categoryCode], x, y), - { - headers: { - Authorization: `KakaoAK ${KAKAO_API_KEY}`, + try { + const { data } = await axios.get( + API_PATH.KAKAO.SEARCH(query, KAKAO_CATEGORY_CODE[categoryCode], x, y), + { + headers: { + Authorization: `KakaoAK ${KAKAO_API_KEY}`, + }, }, - }, - ) - return data.documents + ) + + if (!data?.documents || !Array.isArray(data.documents)) { + return [] + } + + return data.documents + } catch (error) { + console.error('카카오 장소 검색 실패:', error) + throw error + } } From 564870e757a09f33922d6648e70d3e8728aa489c Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 15:24:36 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=EC=B9=B4=ED=8E=98=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=9D=EB=8B=B9=20=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=97=90=20Promise.allSettled=20=EC=A0=81=EC=9A=A9=20-=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EC=9A=94=EC=B2=AD=EC=9D=B4=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=8B=9C=20=EB=B6=80=EB=B6=84=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EB=9D=BC=EB=8F=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=97=90=EA=B2=8C=20=EC=A0=9C=EA=B3=B5=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts b/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts index 586f589..db1b762 100644 --- a/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts +++ b/apps/web/app/places/new/_utils/searchCafeAndRestaurant.ts @@ -4,11 +4,15 @@ export const searchCafeAndRestaurant = async (params: { query: string location: { x: number; y: number } }) => { - const [cafes, restaurants] = await Promise.all([ + const [cafesResult, restaurantsResult] = await Promise.allSettled([ getSearchPlaceByKakao({ ...params, categoryCode: 'cafe' }), getSearchPlaceByKakao({ ...params, categoryCode: 'restaurant' }), ]) + const cafes = cafesResult.status === 'fulfilled' ? cafesResult.value : [] + const restaurants = + restaurantsResult.status === 'fulfilled' ? restaurantsResult.value : [] + // 두 결과를 합쳐서 반환 return [...cafes, ...restaurants] } From c5eac60f34b2f65b4ebeb840ce25b8ac6de55e1f Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 24 Nov 2025 15:28:23 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20onSelectPlace=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/_components/SearchPage/SearchPage.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index 3fde4e4..bd54d06 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -48,7 +48,7 @@ export const SearchPage = ({ useBackHandler = false, }: Props) => { const [inputValue, setInputValue] = useState('') - const [isNavigating, setIsNavigating] = useState(false) + const [isLoading, setIsLoading] = useState(false) const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value @@ -58,19 +58,9 @@ export const SearchPage = ({ } } - const handleSelect = (id: string) => { - setIsNavigating(true) - try { - onSelectPlace(id) - } catch (error) { - console.error(error) - setIsNavigating(false) - } - } - return ( <> - {isNavigating && ( + {isLoading && ( )} @@ -94,7 +84,8 @@ export const SearchPage = ({ inputValue={inputValue} place={place} onClick={() => { - handleSelect(place.id) + setIsLoading(true) + onSelectPlace(place.id) }} /> ))}