fix(flutter): 함께 가기 좋은 곳 · 화면 읽기 버튼 · 사진 메모리 수정 #5

Merged
ttipo merged 3 commits from fix/flutter-nearby-a11y-images into main 2026-11-05 01:33:16 +09:00
4 changed files with 68 additions and 12 deletions
Showing only changes of commit 5caed15482 - Show all commits
+2 -1
View File
@@ -60,5 +60,6 @@ lib/
- 한국어 문단은 `keepAll()` 을 거쳐 단어 단위로 줄바꿈한다(Flutter 기본은 글자 단위).
- 카드·배지 대신 흰 바탕 위 평면 목록과 구분선, 색 글자로 메타 정보를 보인다.
- 공공누리 Type3 사진은 자르지 않는다(`PublicImage` 가 contain 으로 보인다 — 행사·장소 사진 모두 이 위젯을 거친다).
- 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다.
- 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다
(서버가 `trusted=true` 로 거른다 — 앱에서 거르면 상가가 빽빽한 도심에서 칸이 비었다).
- 실패 문구는 에러 사전(`error_lexicon.g.dart`)에서만 가져온다 — `errorMessageOf()`.
@@ -15,10 +15,12 @@ class PlaceRepository {
/// 장소 검색. [near] 를 주면 [radiusM] 안을 거리순으로, 없으면 이름순으로 준다.
/// [categories] 가 비면 전체 분류, [petOnly] 면 반려동물 동반(전 구역·일부 구역) 장소만.
/// [trustedOnly] 면 관광공사 장소와 운영자가 확인한 장소만(확인 안 된 상가정보 매장·추천 안 함 제외).
Future<PlacePage> search({
String? query,
List<PlaceCategory> categories = const [],
bool petOnly = false,
bool trustedOnly = false,
GeoPoint? near,
int? radiusM,
int limit = 20,
@@ -31,6 +33,7 @@ class PlaceRepository {
if (categories.isNotEmpty)
'category': categories.map((c) => c.name).join(','),
if (petOnly) 'pet': 'true',
if (trustedOnly) 'trusted': 'true',
'lat': ?near?.lat,
'lng': ?near?.lng,
'radius': ?radiusM,
@@ -18,9 +18,8 @@ const int kHomePetPlaceLimit = 3;
const int kNearbyEventRadiusM = 5000;
const int kNearbyEventLimit = 3;
/// 장소 상세 '함께 가기 좋은 곳' — 반경 안에서 받아 고른 뒤 이만큼 보인다
/// 장소 상세 '함께 가기 좋은 곳' — 반경 안에서 이만큼 보인다
const int kNearbyPlaceRadiusM = 1500;
const int kNearbyPlaceFetchLimit = 30;
const int kNearbyPlaceLimit = 6;
/// 장소 상세
@@ -66,21 +65,20 @@ typedef NearbyPlaceKey = ({String id, double lat, double lng});
///
/// 확인 안 된 상가정보 매장은 고르지 않는다(관광공사 장소 또는 운영자가 확인한 장소만).
/// 폐업·분위기를 검증할 수 없는 곳을 추천처럼 보이지 않으려는 것이다(2026-11-02 합의).
/// 서버가 거른다(`trusted`) — 앱이 가까운 몇 곳을 받아 거르면 도심처럼 상가가 빽빽한 곳에서는
/// 받은 곳이 모두 상가라 칸이 비었다(2026-11-04 회귀 점검). 자기 자신이 섞여 오므로 하나 더 받는다.
final nearbyPlacesProvider =
FutureProvider.family<List<PlaceItem>, NearbyPlaceKey>((ref, key) async {
final page = await ref
.watch(placeRepositoryProvider)
.search(
trustedOnly: true,
near: (lat: key.lat, lng: key.lng),
radiusM: kNearbyPlaceRadiusM,
limit: kNearbyPlaceFetchLimit,
limit: kNearbyPlaceLimit + 1,
);
return page.items
.where(
(p) =>
p.id != key.id &&
(p.source == PlaceSource.tour || p.verifiedAt != null),
)
.where((p) => p.id != key.id)
.take(kNearbyPlaceLimit)
.toList();
});
+57 -3
View File
@@ -12,6 +12,7 @@ import 'package:wandergo/features/location/data/location_service.dart';
import 'package:wandergo/features/location/domain/base_location.dart';
import 'package:wandergo/features/place/data/models/place_models.dart';
import 'package:wandergo/features/place/data/repositories/place_repository.dart';
import 'package:wandergo/features/place/presentation/providers/place_providers.dart';
/// 2026-11-04(수) 한국 정오 — 이번 주말은 11-07~11-08
final DateTime _now = DateTime.utc(2026, 11, 4, 3);
@@ -113,7 +114,15 @@ class _FakePlaceRepository implements PlaceRepository {
final List<PlaceItem> places;
final List<PlaceItem> petPlaces;
final List<({String? query, List<PlaceCategory> categories, bool petOnly})>
final List<
({
String? query,
List<PlaceCategory> categories,
bool petOnly,
bool trustedOnly,
int limit,
})
>
calls = [];
@override
@@ -121,13 +130,29 @@ class _FakePlaceRepository implements PlaceRepository {
String? query,
List<PlaceCategory> categories = const [],
bool petOnly = false,
bool trustedOnly = false,
GeoPoint? near,
int? radiusM,
int limit = 20,
String? cursor,
}) async {
calls.add((query: query, categories: categories, petOnly: petOnly));
final items = petOnly ? petPlaces : places;
calls.add((
query: query,
categories: categories,
petOnly: petOnly,
trustedOnly: trustedOnly,
limit: limit,
));
// 서버처럼 trusted 면 관광공사·운영자 확인 장소만 주고, limit 만큼 자른다
final items = (petOnly ? petPlaces : places)
.where(
(p) =>
!trustedOnly ||
p.source == PlaceSource.tour ||
p.verifiedAt != null,
)
.take(limit)
.toList();
return PlacePage(items: items, hasNext: false, total: items.length);
}
@@ -389,4 +414,33 @@ void main() {
expect(find.text('Need help? Call 1330'), findsOneWidget);
expect(_text('Hours not confirmed · check before you go'), findsOneWidget);
});
// 2026-11-04 회귀 점검: 앱이 가까운 30곳을 받아 거르면 도심에서는 모두 상가라 칸이 비었다
testWidgets('함께 가기 좋은 곳은 관광공사·확인 장소만 서버에 요청해, 상가가 더 가까워도 채워진다', (
tester,
) async {
await _useTallScreen(tester);
final alley = _place(
'p-alley',
'대구근대골목',
category: PlaceCategory.attraction,
distanceM: 320,
);
final places = _FakePlaceRepository(
places: [_storeCafe, _petPark, alley],
petPlaces: [_petPark],
);
await _pumpApp(tester, prefs: {'onboarding_done': true}, places: places);
await tester.tap(find.text('경상감영공원'));
await tester.pumpAndSettle();
final nearby = places.calls.where((c) => c.trustedOnly).toList();
expect(nearby, hasLength(1));
// 자기 자신이 섞여 오므로 보일 수보다 하나 더 받는다
expect(nearby.single.limit, kNearbyPlaceLimit + 1);
expect(find.text('함께 가기 좋은 곳'), findsOneWidget);
expect(find.text('대구근대골목'), findsOneWidget);
expect(find.text('마실커피 동성로점'), findsNothing);
});
}