From c6017fd0ed11569664c2f0b650886cb32a0074aa Mon Sep 17 00:00:00 2001 From: ttipo Date: Thu, 5 Nov 2026 00:00:14 +0900 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=ED=99=94=EB=A9=B4=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=88=84=EB=A5=BC=20=EC=88=98=20=EC=97=86=EB=8D=98?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=206=EC=A2=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semantics(excludeSemantics: true) 로 감싼 버튼은 안쪽 InkWell 의 누르기 동작까지 지워져 TalkBack·VoiceOver 로 누를 수 없었다(하단 탭 · 언어 · 홈 검색창 · 카테고리 · 기준 위치 · 반려견 칩). Semantics 에도 같은 onTap 을 준다. 기기 접근성 트리에서 누를 수 있음으로 바뀐 것을 확인했고, 누르기 동작이 빠지면 위젯 테스트가 실패한다. Co-Authored-By: Claude Opus 5.5 --- flutter/README.md | 2 ++ flutter/lib/core/router/app_shell.dart | 2 ++ .../home/presentation/pages/home_page.dart | 16 ++++++---- .../presentation/widgets/category_grid.dart | 2 ++ .../presentation/widgets/home_search_bar.dart | 5 ++- .../presentation/pages/place_search_page.dart | 5 ++- .../lib/shared/widgets/language_button.dart | 5 ++- flutter/test/widget_test.dart | 32 +++++++++++++++++++ 8 files changed, 60 insertions(+), 9 deletions(-) diff --git a/flutter/README.md b/flutter/README.md index dc30539..2732584 100644 --- a/flutter/README.md +++ b/flutter/README.md @@ -62,4 +62,6 @@ lib/ - 공공누리 Type3 사진은 자르지 않는다(`PublicImage` 가 contain 으로 보인다 — 행사·장소 사진 모두 이 위젯을 거친다). - 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다 (서버가 `trusted=true` 로 거른다 — 앱에서 거르면 상가가 빽빽한 도심에서 칸이 비었다). +- `Semantics(excludeSemantics: true)` 로 버튼을 감싸면 안쪽 InkWell 의 누르기 동작까지 지워진다. Semantics 에도 `onTap` 을 같이 준다 + (빠뜨리면 TalkBack·VoiceOver 로 누를 수 없다 — `widget_test.dart` 의 화면 읽기 테스트가 잡는다). - 실패 문구는 에러 사전(`error_lexicon.g.dart`)에서만 가져온다 — `errorMessageOf()`. diff --git a/flutter/lib/core/router/app_shell.dart b/flutter/lib/core/router/app_shell.dart index ced0544..8bdda1f 100644 --- a/flutter/lib/core/router/app_shell.dart +++ b/flutter/lib/core/router/app_shell.dart @@ -70,11 +70,13 @@ class _TabItem extends StatelessWidget { @override Widget build(BuildContext context) { final color = selected ? AppColors.brand : AppColors.textTertiary; + // excludeSemantics 는 아래 InkResponse 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( button: true, selected: selected, label: label, excludeSemantics: true, + onTap: onTap, child: InkResponse( onTap: onTap, radius: 36, diff --git a/flutter/lib/features/home/presentation/pages/home_page.dart b/flutter/lib/features/home/presentation/pages/home_page.dart index 500e02c..f7c9250 100644 --- a/flutter/lib/features/home/presentation/pages/home_page.dart +++ b/flutter/lib/features/home/presentation/pages/home_page.dart @@ -132,17 +132,21 @@ class _BaseLocationButton extends ConsumerWidget { final label = fellBack ? kFallbackNeighborhood.label(l10n) : choice.label(l10n); + Future pickBase() async { + final picked = await showRegionSheet(context, selected: choice); + if (picked != null && context.mounted) { + await applyBaseLocation(context, ref, picked); + } + } + + // excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( button: true, label: l10n.baseLocationButtonLabel(label), excludeSemantics: true, + onTap: pickBase, child: InkWell( - onTap: () async { - final picked = await showRegionSheet(context, selected: choice); - if (picked != null && context.mounted) { - await applyBaseLocation(context, ref, picked); - } - }, + onTap: pickBase, child: SizedBox( height: 32, child: Row( diff --git a/flutter/lib/features/home/presentation/widgets/category_grid.dart b/flutter/lib/features/home/presentation/widgets/category_grid.dart index 02ba698..9e18c46 100644 --- a/flutter/lib/features/home/presentation/widgets/category_grid.dart +++ b/flutter/lib/features/home/presentation/widgets/category_grid.dart @@ -115,10 +115,12 @@ class _CategoryCell extends StatelessWidget { @override Widget build(BuildContext context) { final label = shortcut.label(context.l10n); + // excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( button: true, label: label, excludeSemantics: true, + onTap: onTap, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), diff --git a/flutter/lib/features/home/presentation/widgets/home_search_bar.dart b/flutter/lib/features/home/presentation/widgets/home_search_bar.dart index 6dbfaea..47bd545 100644 --- a/flutter/lib/features/home/presentation/widgets/home_search_bar.dart +++ b/flutter/lib/features/home/presentation/widgets/home_search_bar.dart @@ -14,16 +14,19 @@ class HomeSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { final hint = context.l10n.searchHint; + void openSearch() => context.push(AppRoutes.placeSearch(focus: true)); + // excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( button: true, label: hint, excludeSemantics: true, + onTap: openSearch, child: Material( color: AppColors.inputFill, borderRadius: BorderRadius.circular(kRadius), child: InkWell( borderRadius: BorderRadius.circular(kRadius), - onTap: () => context.push(AppRoutes.placeSearch(focus: true)), + onTap: openSearch, child: SizedBox( height: 48, child: Row( diff --git a/flutter/lib/features/place/presentation/pages/place_search_page.dart b/flutter/lib/features/place/presentation/pages/place_search_page.dart index 5186738..14dedda 100644 --- a/flutter/lib/features/place/presentation/pages/place_search_page.dart +++ b/flutter/lib/features/place/presentation/pages/place_search_page.dart @@ -427,17 +427,20 @@ class FilterToggleChip extends StatelessWidget { color: selected ? AppColors.chipBorderSelected : AppColors.chipBorder, ), ); + void toggle() => onChanged(!selected); + // excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( toggled: selected, button: true, excludeSemantics: true, label: label, + onTap: toggle, child: Material( color: selected ? AppColors.brandSoft : AppColors.background, shape: shape, child: InkWell( customBorder: shape, - onTap: () => onChanged(!selected), + onTap: toggle, child: Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), diff --git a/flutter/lib/shared/widgets/language_button.dart b/flutter/lib/shared/widgets/language_button.dart index e52ccfe..18f0a69 100644 --- a/flutter/lib/shared/widgets/language_button.dart +++ b/flutter/lib/shared/widgets/language_button.dart @@ -16,12 +16,15 @@ class LanguageButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = context.l10n; + void openSheet() => _openSheet(context, ref); + // excludeSemantics 는 버튼의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다 return Semantics( button: true, label: l10n.languageButtonLabel(l10n.languageName), excludeSemantics: true, + onTap: openSheet, child: OutlinedButton( - onPressed: () => _openSheet(context, ref), + onPressed: openSheet, style: OutlinedButton.styleFrom( minimumSize: Size(44, compact ? 44 : 40), padding: EdgeInsets.only(left: 12, right: compact ? 12 : 10), diff --git a/flutter/test/widget_test.dart b/flutter/test/widget_test.dart index 73d0f84..d79e57c 100644 --- a/flutter/test/widget_test.dart +++ b/flutter/test/widget_test.dart @@ -443,4 +443,36 @@ void main() { expect(find.text('대구근대골목'), findsOneWidget); expect(find.text('마실커피 동성로점'), findsNothing); }); + + // 2026-11-04 회귀 점검: excludeSemantics 가 누르기 동작까지 지워 TalkBack 으로 누를 수 없었다 + testWidgets('홈·검색의 버튼은 화면 읽기 프로그램으로도 누를 수 있다', (tester) async { + final semantics = tester.ensureSemantics(); + await _pumpApp( + tester, + prefs: {'onboarding_done': true}, + places: _FakePlaceRepository(places: [_storeCafe]), + ); + + for (final label in [ + '홈', + '관광지', + '장소 이름으로 검색', + RegExp('^언어 변경'), + RegExp('^기준 위치 변경'), + ]) { + expect( + tester.getSemantics(find.bySemanticsLabel(label).first), + isSemantics(isButton: true, hasTapAction: true), + reason: '$label', + ); + } + + await tester.tap(find.text('관광지')); + await tester.pumpAndSettle(); + expect( + tester.getSemantics(find.bySemanticsLabel('반려견 동반')), + isSemantics(isButton: true, hasTapAction: true), + ); + semantics.dispose(); + }); }