Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions src/api/travel/places.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type SearchPlacesParams = {
pageNo?: number;
numOfRows?: number;
arrange?: 'O' | 'Q' | 'R' | 'S';
keyword?: string;
_type?: string;
};

Expand Down
120 changes: 66 additions & 54 deletions src/pages/home/TravelSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,62 @@ import Header from '@/component/Header';
import Sidebar from '@/component/SideBar';
import SearchIcon from '@/image/Search.svg';
import PlaceCard from '@/component/common/Card/PlaceCard';
import SearchingPage from '../explore/Searching';
import { searchPlaces, mapToCard, type PlaceDto } from '@/api/travel/places.api';
import SortPillSelect from '@/component/selector/SortPillSelect';

type NavState = {
region?: { areaCode?: number | string; sigunguCode?: number | string };
activity?: { cat1?: string; cat2?: string[] };
};
const PAGE_SIZE = 20;
const LEGACY_ARRANGE_OPTIONS = [
{ value: 'O', label: '기본순' },
{ value: 'Q', label: '수정일순' },
{ value: 'R', label: '등록일순' },
{ value: 'S', label: '한적함순' },
] as const;
type LegacyArrange = (typeof LEGACY_ARRANGE_OPTIONS)[number]['value'];

export default function TravelSearch() {
const navigate = useNavigate();
const { state } = useLocation();
const navState = (state || {}) as NavState;

const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [q, setQ] = useState('');
const [items, setItems] = useState<ReturnType<typeof mapToCard>[]>([]);
const [, setPageNo] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);

const [arrange, setArrange] = useState<LegacyArrange>('O');
const handleMenuClick = () => setIsSidebarOpen(true);
const handleCloseSidebar = () => setIsSidebarOpen(false);

useEffect(() => {
let alive = true;
(async () => {
setItems([]);
setPageNo(1);
setHasMore(true);
setErr(null);

setInitialLoading(true);
try {
await loadPage(1, true);
} finally {
if (alive) setInitialLoading(false);
} catch (e) {
console.error(e);
}
})();
return () => {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps

}, [
navState?.region?.areaCode,
navState?.region?.sigunguCode,
navState?.activity?.cat1,
JSON.stringify(navState?.activity?.cat2 || []),
arrange,
q,
]);

async function loadPage(p: number, replace = false) {
if (loading || (!hasMore && !replace)) return;
if (!replace) setLoading(true);
setLoading(true);
setErr(null);

try {
const params = {
areaCode: navState.region?.areaCode,
Expand All @@ -69,72 +68,85 @@ export default function TravelSearch() {
cat2: navState.activity?.cat2?.[0],
pageNo: p,
numOfRows: PAGE_SIZE,
arrange: 'O' as const,
};
arrange,
keyword: q.trim() || undefined,
} as const;
const list: PlaceDto[] = await searchPlaces(params);
const mapped = list.map(mapToCard);

setItems((prev) => (replace ? mapped : [...prev, ...mapped]));
setPageNo(p);
setHasMore(list.length >= PAGE_SIZE);
} catch {
setErr('여행지 목록을 불러오지 못했어요.');
} finally {
if (!replace) setLoading(false);
setLoading(false);
}
}

const filtered = useMemo(() => {
const visible = useMemo(() => {
const kw = q.trim().toLowerCase();
if (!kw) return items;
return items.filter((it) => it.title.toLowerCase().includes(kw));
}, [items, q]);

if (initialLoading) return <SearchingPage />;

return (
<div className="min-h-screen">
<Header onMenuClick={handleMenuClick} />
<Sidebar isOpen={isSidebarOpen} onClose={handleCloseSidebar} position="left" />
<div className="mx-auto flex w-full max-w-[430px] flex-col">
<div className="pt-14 pb-8">
<div className="bg-green3-light text-caption3 text-green1 h-10 w-full items-center py-[10px] text-center">
<div className="pt-14 pb-3">
<div className="bg-green3-light text-caption3 text-green1 h-10 w-full py-[10px] text-center">
여행지 탐색
</div>
</div>

<div className="relative mb-5 px-9">
<input
type="text"
placeholder="Search"
value={q}
onChange={(e) => setQ(e.target.value)}
className="bg-gray2 placeholder:text-green1 w-full rounded-full px-4 py-2 pl-10 text-sm text-black focus:outline-none"
/>
<span className="text-green-muted pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 px-9">
<img src={SearchIcon} alt="search" className="h-4 w-4" />
</span>
<div className="px-9">
<div className="relative">
<input
type="text"
placeholder="Search"
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);
}}
/>
<span className="text-green-muted pointer-events-none absolute top-1/2 left-0 -translate-y-1/2 px-9">
<img src={SearchIcon} alt="search" className="h-4 w-4" />
</span>
</div>
<div className="mt-2 flex justify-end">
<SortPillSelect
value={arrange}
options={LEGACY_ARRANGE_OPTIONS as any}
onChange={(v) => setArrange(v as LegacyArrange)}
size="sm"
/>
</div>
</div>

{err && (
{loading && (
<div className="py-10 text-center text-gray-500 animate-pulse">
검색 중입니다...
</div>
)}
{err && !loading && (
<div className="mb-3 rounded-md bg-red-100 px-3 py-2 text-sm text-red-700">{err}</div>
)}
{!err && filtered.length === 0 && (
{!err && !loading && visible.length === 0 && (
<div className="py-10 text-center text-gray-500">조건에 맞는 결과가 없어요.</div>
)}

<div className="flex flex-col gap-3 px-9">
{filtered.map((it) => (
<PlaceCard
key={String(it.id)}
title={it.title}
theme={it.theme}
likeCount={it.likeCount}
imgUrl={it.imgUrl}
quietLevel={it.quietLevel}
onClick={() => navigate(`/place/${it.id}`)}
/>
))}
<div className="flex flex-col gap-3 px-9 pb-8 mt-2">
{!loading &&
visible.map((it) => (
<PlaceCard
key={String(it.id)}
title={it.title}
theme={it.theme}
likeCount={it.likeCount}
imgUrl={it.imgUrl}
quietLevel={it.quietLevel}
onClick={() => navigate(`/place/${it.id}`)}
/>
))}
</div>
</div>
</div>
Expand Down