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

@@ -23,19 +23,20 @@ export default function RestaurantDetail({
const [favLoading, setFavLoading] = useState(false);
useEffect(() => {
let cancelled = false;
setLoading(true);
api
.getRestaurantVideos(restaurant.id)
.then(setVideos)
.catch(() => setVideos([]))
.finally(() => setLoading(false));
.then((v) => { if (!cancelled) setVideos(v); })
.catch(() => { if (!cancelled) setVideos([]); })
.finally(() => { if (!cancelled) setLoading(false); });
// Load favorite status if logged in
if (getToken()) {
api.getFavoriteStatus(restaurant.id)
.then((r) => setFavorited(r.favorited))
.then((r) => { if (!cancelled) setFavorited(r.favorited); })
.catch(() => {});
}
return () => { cancelled = true; };
}, [restaurant.id]);
const handleToggleFavorite = async () => {
@@ -57,12 +58,12 @@ export default function RestaurantDetail({
<button
onClick={handleToggleFavorite}
disabled={favLoading}
className={`text-xl leading-none transition-colors ${
className={`p-1.5 -m-1.5 transition-colors touch-manipulation ${
favorited ? "text-rose-500" : "text-gray-300 dark:text-gray-600 hover:text-rose-400"
}`}
title={favorited ? "찜 해제" : "찜하기"}
>
<Icon name="favorite" size={20} filled={favorited} />
<Icon name="favorite" size={22} filled={favorited} />
</button>
)}
{restaurant.business_status === "CLOSED_PERMANENTLY" && (
@@ -218,7 +219,7 @@ export default function RestaurantDetail({
<Icon name="play_circle" size={16} filled className="flex-shrink-0" />
{v.title}
</a>
{v.foods_mentioned.length > 0 && (
{v.foods_mentioned?.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{v.foods_mentioned.map((f, i) => (
<span
@@ -235,7 +236,7 @@ export default function RestaurantDetail({
{v.evaluation.text}
</p>
)}
{v.guests.length > 0 && (
{v.guests?.length > 0 && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
: {v.guests.join(", ")}
</p>