fix(a11y): #301+#302 모달 접근성 + race condition + 필터 상태 동기화

CRITICAL — 모달 접근성:
- frontend/src/lib/hooks/useModalA11y.ts 신규 (useEscapeKey, useFocusTrap, useBodyScrollLock)
- BottomSheet: role='dialog' / aria-modal / aria-label / ESC 닫기 / focus trap
- FilterSheet: role='dialog' / aria-modal / aria-labelledby / ESC 닫기 / focus trap, 닫기 버튼 aria-label

MAJOR — race condition (#301):
- RestaurantDetail useEffect에 cancelled 플래그 추가 → restaurant.id 변경 시 이전 fetch 결과 폐기

MAJOR — 필터 상태 동기화 (#302):
- page.tsx에 exitSearchMode 헬퍼 추가
- 검색 모드(isSearchResult=true)에서 cuisine/price/country/city/district 변경 시 자동으로 검색 모드 해제 + 원본 restaurants 재로드

후속 분리: #319(BottomSheet 매직넘버/UX), #320(필터 정밀도/접근성/테스트)

Refs: #301 #302
This commit is contained in:
joungmin
2026-06-15 12:23:15 +09:00
parent 2d41f22b83
commit 43fd931824
5 changed files with 157 additions and 43 deletions

View File

@@ -1,23 +1,28 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEscapeKey, useFocusTrap } from "@/lib/hooks/useModalA11y";
interface BottomSheetProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
ariaLabel?: string;
}
const SNAP_POINTS = { PEEK: 0.4, HALF: 0.55, FULL: 0.92 };
const VELOCITY_THRESHOLD = 0.5;
export default function BottomSheet({ open, onClose, children }: BottomSheetProps) {
export default function BottomSheet({ open, onClose, children, ariaLabel = "상세 정보" }: BottomSheetProps) {
const sheetRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(SNAP_POINTS.PEEK);
const [dragging, setDragging] = useState(false);
const dragState = useRef({ startY: 0, startH: 0, lastY: 0, lastTime: 0 });
useEscapeKey(open, onClose);
useFocusTrap(open, sheetRef);
// Reset to peek when opened
useEffect(() => {
if (open) setHeight(SNAP_POINTS.PEEK);
@@ -89,6 +94,10 @@ export default function BottomSheet({ open, onClose, children }: BottomSheetProp
{/* Sheet */}
<div
ref={sheetRef}
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
tabIndex={-1}
className="fixed bottom-0 left-0 right-0 z-50 md:hidden flex flex-col bg-surface/85 backdrop-blur-xl rounded-t-2xl shadow-2xl"
style={{
height: `${height * 100}vh`,