diff --git a/src/api/like/like.api.ts b/src/api/like/like.api.ts index 9c9be75..4529c1e 100644 --- a/src/api/like/like.api.ts +++ b/src/api/like/like.api.ts @@ -33,6 +33,6 @@ export const unlikePlace = (contentId: string) => }); export const getLikeStatus = (contentId: string) => - api.get>('places/like/status', { + api.get>('/places/like/status', { params: { contentId }, }); diff --git a/src/api/travel/places.api.ts b/src/api/travel/places.api.ts index c143b60..84a5c52 100644 --- a/src/api/travel/places.api.ts +++ b/src/api/travel/places.api.ts @@ -8,7 +8,7 @@ export type SearchPlacesParams = { contentTypeId?: number; pageNo?: number; numOfRows?: number; - arrange?: 'O' | 'Q' | 'R' | 'S'; + arrange?: 'A' | 'C' | 'Q' | 'R'; keyword?: string; _type?: string; }; @@ -37,10 +37,22 @@ function toArray(raw: any): T[] { } export async function searchPlaces(params: SearchPlacesParams): Promise { - const res = await api.get('/places', { params }); + const cleanParams = { ...params }; + if (typeof cleanParams.keyword === 'string' && cleanParams.keyword.trim() === '') { + delete (cleanParams as any).keyword; + } + const res = await api.get('/places', { params: cleanParams }); return toArray(unwrap(res.data)); } +//-1(데이터 없음)은 undefined 처리 +function toNumberOrUndef(v: unknown): number | undefined { + if (v === null || v === undefined) return undefined; + const n = typeof v === 'number' ? v : Number(v); + if (Number.isNaN(n)) return undefined; + return n === -1 ? undefined : n; +} + export function mapToCard(p: PlaceDto) { const raw = p.firstimage || ''; const imgUrl = @@ -48,12 +60,16 @@ export function mapToCard(p: PlaceDto) { ? raw.replace(/^http:\/\//, 'https://') : raw || undefined; + const quiet = + typeof p.quietnessLevel === 'number' ? p.quietnessLevel : toNumberOrUndef(p.cnctrRate); + return { id: p.contentid, title: p.title, theme: p.catName ?? '-', likeCount: p.likeCount ?? 0, imgUrl, - quietLevel: typeof p.quietnessLevel === 'number', + quietLevel: quiet, + cnctrRate: quiet, }; } diff --git a/src/component/common/Button/Button.styled.ts b/src/component/common/Button/Button.styled.ts index d08e738..1c55350 100644 --- a/src/component/common/Button/Button.styled.ts +++ b/src/component/common/Button/Button.styled.ts @@ -1,4 +1,4 @@ -export const BUTTON_BASE='disabled:cursor-not-allowed active:scale-95 transition-color'; +export const BUTTON_BASE = 'disabled:cursor-not-allowed active:scale-95 transition-color'; export const BUTTON_VARIANT = { lg: { @@ -20,13 +20,13 @@ export const BUTTON_SIZE = { sm: 'w-18 h-6 text-caption2', } as const; -export const BUTTON_COLOR={ +export const BUTTON_COLOR = { green3: 'bg-green3 hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed', 'green-muted': 'bg-green-muted hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed', -} +}; export const BUTTON_ROUNDED = { lg: 'rounded-l', md: 'rounded-l', sm: 'rounded-[10px]', -} as const; \ No newline at end of file +} as const; diff --git a/src/component/common/Card/PlaceCard.tsx b/src/component/common/Card/PlaceCard.tsx index ee9722b..d6803c2 100644 --- a/src/component/common/Card/PlaceCard.tsx +++ b/src/component/common/Card/PlaceCard.tsx @@ -5,7 +5,7 @@ type PlaceCardProps = { imgUrl?: string; title: string; theme: string; - quietLevel: number | boolean; + quietLevel: number | undefined; likeCount: number; showRemoveButton?: boolean; onRemove?: () => void; diff --git a/src/component/selector/Selector.tsx b/src/component/selector/Selector.tsx index 14f2c67..61ffd72 100644 --- a/src/component/selector/Selector.tsx +++ b/src/component/selector/Selector.tsx @@ -64,7 +64,7 @@ const Selector = ({ @@ -277,7 +280,10 @@ const TravelSpotDetail = () => { {/*좋아요, 저장*/}
- { {formatCount(likeCount)} -
diff --git a/src/pages/explore/Filter.tsx b/src/pages/explore/Filter.tsx index defea24..def1010 100644 --- a/src/pages/explore/Filter.tsx +++ b/src/pages/explore/Filter.tsx @@ -37,25 +37,14 @@ export default function FilterPage() { }, [phase, step]); const handleFinish = () => { - // 1. 먼저 /searching 으로 이동 - navigate('/searching'); - - // 2. 3초(3000ms) 후 /search/result로 이동 - setTimeout(() => { navigate('/search/result', { state: { - region: { - areaCode: regionCodes.areaCode, - sigunguCode: regionCodes.sigunguCode, - }, - activity: { - cat1: activityCodes.cat1, - cat2: activityCodes.cat2, - }, + region: { areaCode: regionCodes.areaCode, sigunguCode: regionCodes.sigunguCode }, + activity: { cat1: activityCodes.cat1, cat2: activityCodes.cat2 }, + fromFilter: true, }, }); - }, 3000); -}; + }; const resetAndSelectAgain = () => { setPhase('select'); diff --git a/src/pages/home/Homepage.tsx b/src/pages/home/Homepage.tsx index a749186..be9e1df 100644 --- a/src/pages/home/Homepage.tsx +++ b/src/pages/home/Homepage.tsx @@ -30,7 +30,7 @@ export default function HomePage() {
지금 가장 여유롭고 한적한 여행지를 찾아드립니다.
- @@ -47,7 +47,11 @@ export default function HomePage() { 내가 원하는 조용한 여행지,
이제 손쉽게 찾을 수 있어요. - @@ -71,7 +75,11 @@ export default function HomePage() {
주차장 위치까지 한눈에 확인해보세요. - diff --git a/src/pages/home/MyPage.tsx b/src/pages/home/MyPage.tsx index 9d6b08d..b3a7411 100644 --- a/src/pages/home/MyPage.tsx +++ b/src/pages/home/MyPage.tsx @@ -211,7 +211,7 @@ export default function MyPage() { @@ -251,7 +251,7 @@ export default function MyPage() { type="button" disabled={!canSave} onClick={handleSaveNickname} - className="rounded-m border-green3 bg-green3-light text-caption2 text-green1 h-10 shrink-0 border px-5 hover:brightness-95 disabled:cursor-not-allowed disabled:opacity-50" + className="rounded-m border-green3 bg-green3-light text-caption2 text-green1 h-10 shrink-0 cursor-pointer border px-5 hover:brightness-95 disabled:cursor-not-allowed disabled:opacity-50" aria-disabled={!canSave} > {saving ? '저장 중...' : '저장'} diff --git a/src/pages/home/TravelSearch.tsx b/src/pages/home/TravelSearch.tsx index 7d0d8a6..3121f9b 100644 --- a/src/pages/home/TravelSearch.tsx +++ b/src/pages/home/TravelSearch.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import Header from '@/component/Header'; import Sidebar from '@/component/SideBar'; @@ -6,6 +6,8 @@ import SearchIcon from '@/image/Search.svg'; import PlaceCard from '@/component/common/Card/PlaceCard'; import { searchPlaces, mapToCard, type PlaceDto } from '@/api/travel/places.api'; import SortPillSelect from '@/component/selector/SortPillSelect'; +import SearchingPage from '../explore/Searching'; +import { Loader } from '@/component'; type NavState = { region?: { areaCode?: number | string; sigunguCode?: number | string }; @@ -13,10 +15,10 @@ type NavState = { }; const PAGE_SIZE = 20; const LEGACY_ARRANGE_OPTIONS = [ - { value: 'O', label: '기본순' }, + { value: 'A', label: '기본순' }, { value: 'Q', label: '수정일순' }, { value: 'R', label: '등록일순' }, - { value: 'S', label: '한적함순' }, + { value: 'C', label: '한적함순' }, ] as const; type LegacyArrange = (typeof LEGACY_ARRANGE_OPTIONS)[number]['value']; @@ -24,42 +26,34 @@ export default function TravelSearch() { const navigate = useNavigate(); const { state } = useLocation(); const navState = (state || {}) as NavState; + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const fromFilter = (state as any)?.fromFilter === true; + const showOverlayRef = useRef(fromFilter); const [q, setQ] = useState(''); + const [debouncedQ, setDebouncedQ] = useState(''); const [items, setItems] = useState[]>([]); const [, setPageNo] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); - const [arrange, setArrange] = useState('O'); + const [arrange, setArrange] = useState('A'); + const handleMenuClick = () => setIsSidebarOpen(true); const handleCloseSidebar = () => setIsSidebarOpen(false); useEffect(() => { - (async () => { - setItems([]); - setPageNo(1); - setHasMore(true); - setErr(null); - try { - await loadPage(1, true); - } catch (e) { - console.error(e); - } - })(); - - }, [ - navState?.region?.areaCode, - navState?.region?.sigunguCode, - navState?.activity?.cat1, - JSON.stringify(navState?.activity?.cat2 || []), - arrange, - q, - ]); + const h = setTimeout(() => setDebouncedQ(q.trim()), 300); + return () => clearTimeout(h); + }, [q]); + + const reqIdRef = useRef(0); + async function loadPage(p: number, replace = false) { if (loading || (!hasMore && !replace)) return; setLoading(true); setErr(null); + const myReqId = ++reqIdRef.current; try { const params = { areaCode: navState.region?.areaCode, @@ -69,25 +63,43 @@ export default function TravelSearch() { pageNo: p, numOfRows: PAGE_SIZE, arrange, - keyword: q.trim() || undefined, + keyword: debouncedQ || undefined, } as const; + const list: PlaceDto[] = await searchPlaces(params); + if (myReqId !== reqIdRef.current) return; + const mapped = list.map(mapToCard); setItems((prev) => (replace ? mapped : [...prev, ...mapped])); setPageNo(p); setHasMore(list.length >= PAGE_SIZE); } catch { + if (myReqId !== reqIdRef.current) return; setErr('여행지 목록을 불러오지 못했어요.'); } finally { - setLoading(false); + if (myReqId === reqIdRef.current) setLoading(false); + showOverlayRef.current = false; } } - const visible = useMemo(() => { - const kw = q.trim().toLowerCase(); - if (!kw) return items; - return items.filter((it) => it.title.toLowerCase().includes(kw)); - }, [items, q]); + useEffect(() => { + setPageNo(1); + setHasMore(true); + setErr(null); + loadPage(1, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + navState?.region?.areaCode, + navState?.region?.sigunguCode, + navState?.activity?.cat1, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(navState?.activity?.cat2 || []), + arrange, + debouncedQ, + ]); + if (loading && showOverlayRef.current) { + return ; + } return (
@@ -106,9 +118,6 @@ export default function TravelSearch() { value={q} onChange={(e) => setQ(e.target.value)} className="bg-gray2 placeholder:text-green1 w-full rounded-full px-4 py-2 pl-16 text-sm text-black focus:outline-none" - onKeyDown={(e) => { - if (e.key === 'Enter') loadPage(1, true); - }} /> search @@ -123,20 +132,21 @@ export default function TravelSearch() { />
- {loading && ( -
- 검색 중입니다... -
- )} + {err && !loading && (
{err}
)} - {!err && !loading && visible.length === 0 && ( + {!err && !loading && items.length === 0 && (
조건에 맞는 결과가 없어요.
)} -
- {!loading && - visible.map((it) => ( + +
+ {loading ? ( +
+ +
+ ) : ( + items.map((it) => ( navigate(`/place/${it.id}`)} /> - ))} + )) + )}