UI/UX 개선: 모바일 네비게이션, 로그인 모달, 지도 기능, 캐치테이블 연동
- 모바일 하단 네비게이션(홈/식당목록/내주변/찜/내정보) 추가 - 로그인 버튼을 모달 방식으로 변경 (소셜 로그인 확장 가능) - 내위치 기반 정렬 및 영역 필터, 지도 내위치 버튼 추가 - 채널 필터 시 해당 채널만 마커/범례 표시 - 캐치테이블 검색/연동 (단건/벌크), NONE 저장 패턴 - 벌크 트랜스크립트 SSE (Playwright 브라우저 재사용) - 테이블링/캐치테이블 버튼 UI 차별화 - Google Maps 링크 모바일 호환, 초기화 버튼, 검색 라벨 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,33 +57,14 @@ interface MapViewProps {
|
||||
onSelectRestaurant?: (r: Restaurant) => void;
|
||||
onBoundsChanged?: (bounds: MapBounds) => void;
|
||||
flyTo?: FlyTo | null;
|
||||
onMyLocation?: () => void;
|
||||
activeChannel?: string;
|
||||
}
|
||||
|
||||
function MapContent({ restaurants, selected, onSelectRestaurant, onBoundsChanged, flyTo }: MapViewProps) {
|
||||
function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeChannel }: Omit<MapViewProps, "onMyLocation" | "onBoundsChanged">) {
|
||||
const map = useMap();
|
||||
const [infoTarget, setInfoTarget] = useState<Restaurant | null>(null);
|
||||
const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]);
|
||||
const boundsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Report bounds on idle (debounced)
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
const listener = map.addListener("idle", () => {
|
||||
if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current);
|
||||
boundsTimerRef.current = setTimeout(() => {
|
||||
const b = map.getBounds();
|
||||
if (b && onBoundsChanged) {
|
||||
const ne = b.getNorthEast();
|
||||
const sw = b.getSouthWest();
|
||||
onBoundsChanged({ north: ne.lat(), south: sw.lat(), east: ne.lng(), west: sw.lng() });
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
return () => {
|
||||
google.maps.event.removeListener(listener);
|
||||
if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current);
|
||||
};
|
||||
}, [map, onBoundsChanged]);
|
||||
|
||||
const handleMarkerClick = useCallback(
|
||||
(r: Restaurant) => {
|
||||
@@ -113,7 +94,8 @@ function MapContent({ restaurants, selected, onSelectRestaurant, onBoundsChanged
|
||||
{restaurants.map((r) => {
|
||||
const isSelected = selected?.id === r.id;
|
||||
const isClosed = r.business_status === "CLOSED_PERMANENTLY";
|
||||
const chColor = r.channels?.[0] ? channelColors[r.channels[0]] : CHANNEL_COLORS[0];
|
||||
const chKey = activeChannel && r.channels?.includes(activeChannel) ? activeChannel : r.channels?.[0];
|
||||
const chColor = chKey ? channelColors[chKey] : CHANNEL_COLORS[0];
|
||||
const c = chColor || CHANNEL_COLORS[0];
|
||||
return (
|
||||
<AdvancedMarker
|
||||
@@ -208,29 +190,56 @@ function MapContent({ restaurants, selected, onSelectRestaurant, onBoundsChanged
|
||||
);
|
||||
}
|
||||
|
||||
export default function MapView({ restaurants, selected, onSelectRestaurant, onBoundsChanged, flyTo }: MapViewProps) {
|
||||
export default function MapView({ restaurants, selected, onSelectRestaurant, onBoundsChanged, flyTo, onMyLocation, activeChannel }: MapViewProps) {
|
||||
const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]);
|
||||
const channelNames = useMemo(() => Object.keys(channelColors), [channelColors]);
|
||||
const channelNames = useMemo(() => {
|
||||
const names = Object.keys(channelColors);
|
||||
if (activeChannel) return names.filter((n) => n === activeChannel);
|
||||
return names;
|
||||
}, [channelColors, activeChannel]);
|
||||
const boundsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const handleCameraChanged = useCallback((ev: { detail: { bounds: { north: number; south: number; east: number; west: number } } }) => {
|
||||
if (!onBoundsChanged) return;
|
||||
if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current);
|
||||
boundsTimerRef.current = setTimeout(() => {
|
||||
const { north, south, east, west } = ev.detail.bounds;
|
||||
onBoundsChanged({ north, south, east, west });
|
||||
}, 150);
|
||||
}, [onBoundsChanged]);
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={API_KEY}>
|
||||
<Map
|
||||
defaultCenter={SEOUL_CENTER}
|
||||
defaultZoom={12}
|
||||
defaultZoom={13}
|
||||
mapId="tasteby-map"
|
||||
className="h-full w-full"
|
||||
colorScheme="LIGHT"
|
||||
mapTypeControl={false}
|
||||
fullscreenControl={false}
|
||||
onCameraChanged={handleCameraChanged}
|
||||
>
|
||||
<MapContent
|
||||
restaurants={restaurants}
|
||||
selected={selected}
|
||||
onSelectRestaurant={onSelectRestaurant}
|
||||
onBoundsChanged={onBoundsChanged}
|
||||
flyTo={flyTo}
|
||||
activeChannel={activeChannel}
|
||||
/>
|
||||
</Map>
|
||||
{channelNames.length > 1 && (
|
||||
{onMyLocation && (
|
||||
<button
|
||||
onClick={onMyLocation}
|
||||
className="absolute top-2 right-2 w-9 h-9 bg-white dark:bg-gray-900 rounded-lg shadow-md flex items-center justify-center text-gray-600 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors z-10"
|
||||
title="내 위치"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="w-5 h-5 fill-current">
|
||||
<path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3A8.994 8.994 0 0013 3.06V1h-2v2.06A8.994 8.994 0 003.06 11H1v2h2.06A8.994 8.994 0 0011 20.94V23h2v-2.06A8.994 8.994 0 0020.94 13H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{channelNames.length > 0 && (
|
||||
<div className="absolute bottom-2 left-2 bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm rounded-lg shadow px-2.5 py-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[11px] z-10">
|
||||
{channelNames.map((ch) => (
|
||||
<div key={ch} className="flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user