Skip to content

Commit 67820ea

Browse files
authored
fix: map page qa (#109)
* refactor: update toast message when no store data * chore: update ui when no search history data * feat: add search execution when clicking recent search item * refactor: temporarily sync filter button and bottom sheet height * chore: add use client
1 parent 790d33f commit 67820ea

11 files changed

Lines changed: 125 additions & 50 deletions

File tree

web/src/app/(main)/map/page.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { useEffect } from "react";
4+
import { useState } from "react";
55
import {
66
useLocation,
77
useMapCoordinate,
@@ -20,7 +20,6 @@ import {
2020
} from "@/features/map/ui";
2121
import { SearchBar } from "@/features/search";
2222
import { CUSTOM_EVENTS, useGATimeSpent } from "@/shared/lib";
23-
import { useToast } from "@/shared/lib/hooks";
2423
import { useFilterStore } from "@/shared/store";
2524
import { TransitionLayout } from "@/shared/ui";
2625

@@ -39,10 +38,11 @@ export default function MapPage() {
3938
3,
4039
);
4140

42-
const { storeList, isFetching, isSearchMode } = useStoreListData({ bounds, center });
41+
const { storeList, isFetching } = useStoreListData({ bounds, center });
4342
const { isFilterOpen, filters, openFilter, closeFilter, setFilters } = useFilterStore();
4443

45-
const { showToast } = useToast();
44+
// TODO: 임시 구현, 나중에 Observer로 자동 감지 개선 예정
45+
const [bottomSheetHeight, setBottomSheetHeight] = useState(0);
4646

4747
const levelDisplayValue =
4848
filters.honbobLevel.length > 1 ? "커스텀" : `레벨${filters.honbobLevel[0] || 1}`;
@@ -52,12 +52,6 @@ export default function MapPage() {
5252
resetDragging();
5353
};
5454

55-
useEffect(() => {
56-
if (!isSearchMode && !isFetching && storeList.length === 0) {
57-
showToast({ message: "현재는 강남·역삼 지역만 이용할 수 있어요." });
58-
}
59-
}, [isSearchMode, isFetching, storeList.length, showToast]);
60-
6155
return (
6256
<TransitionLayout>
6357
<Link href="/search">
@@ -74,11 +68,12 @@ export default function MapPage() {
7468
>
7569
<LevelFilterButton honbobLevel={levelDisplayValue} onClick={openFilter} />
7670
</div>
77-
<CurrentLocationButton onClick={requestLocation} />
71+
<CurrentLocationButton bottomOffset={bottomSheetHeight} onClick={requestLocation} />
7872
<StoreBottomSheet
7973
storeList={storeList || []}
8074
isCollapsed={isDragging}
8175
onStationChange={() => updateCoordinate(map)}
76+
onHeightChange={setBottomSheetHeight}
8277
/>
8378
<FilterBottomSheet
8479
isOpen={isFilterOpen}

web/src/app/(stack)/search/page.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,28 @@ import {
66
SEARCH_BAR_HEIGHT,
77
SearchBar,
88
useSearchHistoryAdapter,
9+
useSearchNavigation,
910
} from "@/features/search";
1011
import { TransitionLayout } from "@/shared/ui";
1112

1213
export default function SearchPage() {
1314
const { searchHistory, isLoading, removeHistory, clearHistory } = useSearchHistoryAdapter();
15+
const { executeSearch } = useSearchNavigation();
16+
17+
const hasSearchHistory = searchHistory.length > 0;
1418

1519
return (
1620
<TransitionLayout>
17-
<SearchBar />
21+
<SearchBar onSubmit={executeSearch} />
1822
<div
1923
className="flex flex-col gap-3"
2024
style={{ paddingTop: `calc(${SEARCH_BAR_HEIGHT}px + 17px)` }}
2125
>
22-
<RecentSearchHeader onDelete={clearHistory} />
26+
<RecentSearchHeader hasSearchHistory={hasSearchHistory} onDelete={clearHistory} />
2327
<RecentSearchList
2428
searchHistory={searchHistory}
2529
isLoading={isLoading}
30+
onSearch={executeSearch}
2631
onRemove={removeHistory}
2732
/>
2833
</div>

web/src/features/map/lib/hooks/useStoreListData.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation";
44
import { useEffect } from "react";
55
import { type Bounds, type Center, useStoreListQuery } from "@/features/map/lib";
66
import { useStoreSearch } from "@/features/search";
7+
import { useToast } from "@/shared/lib/hooks";
78
import { useFilterStore } from "@/shared/store";
89

910
interface UseStoreListDataParams {
@@ -13,37 +14,47 @@ interface UseStoreListDataParams {
1314

1415
/**
1516
* 지도에 표시할 가게 리스트 데이터 관리
16-
* 검색 모드와 일반 모드 통합 관리
17+
*
18+
* 두 가지 모드 지원:
19+
* 1. 기본 모드: 지도 영역 기반 가게 목록 조회
20+
* 2. 검색 모드: 검색어 기반 가게 목록 조회
1721
*/
1822
export function useStoreListData({ bounds, center }: UseStoreListDataParams) {
1923
const searchParams = useSearchParams();
20-
const query = searchParams.get("query");
24+
const searchQuery = searchParams.get("query");
25+
const isSearchMode = !!searchQuery;
2126

2227
const { filters } = useFilterStore();
2328

24-
// 일반 모드: 지도 기반 스토어 목록
25-
const { storeList: baseStoreList, isFetching: isBaseFetching } = useStoreListQuery({
29+
const { showToast } = useToast();
30+
31+
// 기본 모드: 지도 기반 스토어 목록
32+
const { storeList: mapStoreList, isFetching: isMapFetching } = useStoreListQuery({
2633
filters,
2734
center,
2835
bounds,
2936
limit: 30,
30-
enabled: !query,
37+
enabled: !isSearchMode,
3138
});
3239

3340
// 검색 모드: 검색어 기반 스토어 목록
3441
const { searchStoreList, isPending, searchStores } = useStoreSearch();
3542

3643
useEffect(() => {
37-
if (query) {
38-
searchStores({ query });
44+
if (isSearchMode) {
45+
searchStores({ query: searchQuery });
3946
}
40-
}, [query, searchStores]);
47+
}, [isSearchMode, searchQuery, searchStores]);
4148

42-
const isSearchMode = !!query;
49+
useEffect(() => {
50+
if (!isSearchMode && !isMapFetching && mapStoreList.length === 0) {
51+
showToast({ message: "아직 이 지역은 준비 중이에요." });
52+
}
53+
}, [isSearchMode, isMapFetching, mapStoreList.length, showToast]);
4354

4455
return {
45-
storeList: isSearchMode ? searchStoreList : baseStoreList,
46-
isFetching: isSearchMode ? isPending : isBaseFetching,
56+
storeList: isSearchMode ? searchStoreList : mapStoreList,
57+
isFetching: isSearchMode ? isPending : isMapFetching,
4758
isSearchMode,
4859
};
4960
}

web/src/features/map/ui/CurrentLocationButton.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
import { BOTTOM_NAV_HEIGHT } from "@/shared/config";
12
import { Icon } from "@/shared/ui";
23

3-
export function CurrentLocationButton({ onClick }: { onClick: () => void }) {
4+
interface CurrentLocationButtonProps {
5+
bottomOffset: number;
6+
onClick: () => void;
7+
}
8+
9+
export function CurrentLocationButton({ bottomOffset, onClick }: CurrentLocationButtonProps) {
10+
// TODO: 임시 구현, 나중에 Observer로 개선 예정
11+
const totalBottomOffset = (bottomOffset > 0 ? bottomOffset : BOTTOM_NAV_HEIGHT) + 20;
12+
413
return (
514
<button
615
type="button"
716
onClick={onClick}
8-
className="fixed right-5 z-40 flex items-center justify-center h-9 w-9 cursor-pointer rounded-full bg-gray0 shadow-fab"
17+
className="fixed right-5 z-40 flex items-center justify-center h-9 w-9 cursor-pointer rounded-full bg-gray0 shadow-fab transition-all duration-300"
918
style={{
10-
bottom: "calc(330px + var(--safe-area-inset-bottom))",
19+
bottom: `calc(${totalBottomOffset}px + env(safe-area-inset-bottom))`,
1120
}}
1221
>
1322
<Icon name="location" size={24} color="gray800" />

web/src/features/map/ui/StoreBottomSheet.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ interface StoreBottomSheetProps {
1717
storeList: StoreListResponseData[];
1818
isCollapsed: boolean;
1919
onStationChange: () => void;
20+
onHeightChange: (height: number) => void;
2021
}
2122

2223
export function StoreBottomSheet({
2324
storeList,
2425
isCollapsed,
2526
onStationChange,
27+
onHeightChange,
2628
}: StoreBottomSheetProps) {
2729
const { selectedStoreId } = useMapStore();
2830
const [safeBottom, setSafeBottom] = useState(0);
2931

32+
// 바텀시트 높이: 마커 선택(245px) | 드래그 중(95px) | 기본(310px) + Safe Area
33+
const height = (selectedStoreId ? 245 : isCollapsed ? 95 : 310) + safeBottom;
34+
3035
const selectedStoreInfo = selectedStoreId
3136
? storeList.find((store) => store.id === selectedStoreId)
3237
: null;
@@ -38,8 +43,15 @@ export function StoreBottomSheet({
3843
setSafeBottom(parseFloat(value));
3944
}, []);
4045

41-
// 바텀시트 높이: 마커 선택(245px) | 드래그 중(95px) | 기본(310px) + Safe Area
42-
const height = (selectedStoreId ? 245 : isCollapsed ? 95 : 310) + safeBottom;
46+
// TODO: 임시 구현, 나중에 Observer로 개선 예정
47+
useEffect(() => {
48+
if (storeList.length === 0) {
49+
onHeightChange(0);
50+
return;
51+
}
52+
53+
onHeightChange(height);
54+
}, [height, onHeightChange, storeList.length]);
4355

4456
const renderStoreContent = () => {
4557
if (selectedStoreInfo) {

web/src/features/search/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./config/searchBarConfig";
77
// Hooks
88
export * from "./lib/hooks/useLocalSearchHistory";
99
export * from "./lib/hooks/useSearchHistoryAdapter";
10+
export * from "./lib/hooks/useSearchNavigation";
1011

1112
// Queries
1213
export * from "./lib/queries/useServerSearchHistory";
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useSearchHistoryAdapter } from "@/features/search";
5+
6+
export function useSearchNavigation() {
7+
const router = useRouter();
8+
const { addHistory } = useSearchHistoryAdapter();
9+
10+
const executeSearch = (query: string) => {
11+
if (!query) return;
12+
13+
// 검색 기록에 추가
14+
addHistory?.(query);
15+
16+
// 지도 페이지로 이동
17+
router.push(`/map?query=${encodeURIComponent(query)}`);
18+
};
19+
20+
return { executeSearch };
21+
}
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
export function RecentSearchHeader({ onDelete }: { onDelete: () => void }) {
1+
interface RecentSearchHeaderProps {
2+
hasSearchHistory: boolean;
3+
onDelete: () => void;
4+
}
5+
6+
export function RecentSearchHeader({ hasSearchHistory, onDelete }: RecentSearchHeaderProps) {
27
return (
38
<div className="flex items-center justify-between px-5">
49
<span className="text-body3-semibold text-gray900">최근 검색</span>
5-
<button type="button" onClick={onDelete} className="text-body3-regular text-gray600">
6-
전체 삭제
7-
</button>
10+
{hasSearchHistory && (
11+
<button type="button" onClick={onDelete} className="text-body3-regular text-gray600">
12+
전체 삭제
13+
</button>
14+
)}
815
</div>
916
);
1017
}

web/src/features/search/ui/RecentSearchItem.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,39 @@ import { Icon } from "@/shared/ui";
55

66
interface RecentSearchItemProps {
77
search: SearchHistoryResponse;
8+
onSearch: (query: string) => void;
89
onRemove: (id: number) => void;
910
}
1011

11-
export function RecentSearchItem({ search, onRemove }: RecentSearchItemProps) {
12+
export function RecentSearchItem({ search, onRemove, onSearch }: RecentSearchItemProps) {
1213
const formattedDate = formatToMonthDay(search.updateAt);
1314

15+
const handleSearch = () => {
16+
onSearch(search.query);
17+
};
18+
1419
const handleRemove = (e: React.MouseEvent) => {
1520
e.stopPropagation();
1621

1722
onRemove(search.id);
1823
};
1924

2025
return (
21-
<div className="flex items-center w-full py-3 px-5">
22-
<div className="flex items-center gap-2 flex-1 min-w-0">
26+
<div className="flex items-center w-full py-3 px-5 hover:bg-gray50 active:bg-gray100 transition-colors">
27+
<button
28+
type="button"
29+
onClick={handleSearch}
30+
className="flex items-center gap-2 flex-1 min-w-0"
31+
>
2332
<Icon name="clock" size={24} color="gray500" className="shrink-0" />
24-
<span className="text-body1-regular text-gray800 truncate">{search.query}</span>
25-
</div>
33+
<span className="text-body1-regular text-gray800 truncate text-left">{search.query}</span>
34+
</button>
2635
<div className="flex items-center gap-3 shrink-0">
2736
<span className="text-body2-regular text-gray600">{formattedDate}</span>
2837
<button
2938
type="button"
3039
onClick={handleRemove}
31-
className="flex items-center justify-center"
40+
className="flex items-center justify-center p-1 hover:bg-gray200 rounded"
3241
aria-label="검색어 삭제"
3342
>
3443
<Icon name="close" size={20} color="gray600" />

web/src/features/search/ui/RecentSearchList.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,31 @@ import { RecentSearchItem } from "./RecentSearchItem";
66
interface RecentSearchListProps {
77
searchHistory: SearchHistoryResponse[];
88
isLoading?: boolean;
9+
onSearch: (query: string) => void;
910
onRemove: (id: number) => void;
1011
}
1112

12-
export function RecentSearchList({ searchHistory, isLoading, onRemove }: RecentSearchListProps) {
13+
export function RecentSearchList({
14+
searchHistory,
15+
isLoading,
16+
onSearch,
17+
onRemove,
18+
}: RecentSearchListProps) {
1319
if (isLoading) {
1420
return <div className="px-5 py-4 text-center text-body2-regular text-gray600">로딩 중...</div>;
1521
}
1622

1723
if (searchHistory.length === 0) {
1824
return (
19-
<div className="px-5 py-4 text-center text-body2-regular text-gray600">
20-
최근 검색 내역이 없습니다
21-
</div>
25+
<div className="px-5 py-3 text-body1-regular text-gray800">최근 검색 내역이 없습니다</div>
2226
);
2327
}
2428

2529
return (
2630
<ul className="flex flex-col">
2731
{searchHistory.map((search) => (
2832
<li key={search.id}>
29-
<RecentSearchItem search={search} onRemove={onRemove} />
33+
<RecentSearchItem search={search} onSearch={onSearch} onRemove={onRemove} />
3034
</li>
3135
))}
3236
</ul>

0 commit comments

Comments
 (0)