generated from ttipo/project-base
feat(flutter): 장소 검색·상세 화면과 홈 검색창·카테고리·반려동물 장소 칸
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 21m21s
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m16s
CI / Frontend (lint · type-check · test) (pull_request) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Has been cancelled
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 21m21s
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m16s
CI / Frontend (lint · type-check · test) (pull_request) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Has been cancelled
디자인 캔버스 4(검색)·6·7(장소 상세) 중 지금 장소 API 로 되는 부분을 붙였다. - 검색은 반경 20km(백엔드 최대) 거리순. 대구 도심에서 대구 전역이 들어온다. - 영업시간을 모르는 곳은 "영업시간 미확인 · 방문 전 확인"으로 보인다. - 한눈에 보기는 공공데이터 원문으로 확실한 것만 올린다(주차 '인근 공영주차장' 같은 애매한 값은 빼기). - 함께 가기 좋은 곳에는 확인 안 된 상가정보 매장을 넣지 않는다(2026-11-02 합의). - 1330 관광통역안내는 영어 화면에만(2026-10-29 합의). 검색의 '축제' 탭과 실내·무료·주차·이번 주말 조건은 데이터가 없어 넣지 않았다 (행사 API 검색어 조건 · 운영자 확인 장소 · 날씨). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wandergo/features/place/data/models/place_models.dart';
|
||||
import 'package:wandergo/features/place/domain/place_facts.dart';
|
||||
import 'package:wandergo/features/place/domain/place_search.dart';
|
||||
import 'package:wandergo/shared/models/source_models.dart';
|
||||
|
||||
/// 개발 백엔드 실제 응답(2026-11-04)에서 가져온 모양
|
||||
Map<String, dynamic> _listItem({
|
||||
String category = 'cafe',
|
||||
String source = 'store',
|
||||
String petPolicy = 'unknown',
|
||||
}) => {
|
||||
'id': '5cb981e1-dfab-497d-b00d-ddac3c962f1e',
|
||||
'name': '마실커피',
|
||||
'branchName': '동성로점',
|
||||
'category': category,
|
||||
'source': source,
|
||||
'address': '대구광역시 중구 중앙대로 380',
|
||||
'lat': 35.8669069452959,
|
||||
'lng': 128.593680108517,
|
||||
'distanceM': 53,
|
||||
'imageThumbUrl': null,
|
||||
'imageLicense': null,
|
||||
'hoursOrigin': 'unknown',
|
||||
'openNow': null,
|
||||
'petPolicy': petPolicy,
|
||||
'petOrigin': 'unknown',
|
||||
'setting': null,
|
||||
'verifiedAt': null,
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('PlaceItem.fromJson', () {
|
||||
test('목록 한 건을 읽고 지점명을 이름에 붙인다', () {
|
||||
final item = PlaceItem.fromJson(_listItem());
|
||||
expect(item.displayName, '마실커피 동성로점');
|
||||
expect(item.category, PlaceCategory.cafe);
|
||||
expect(item.source, PlaceSource.store);
|
||||
expect(item.distanceM, 53);
|
||||
expect(item.openNow, isNull);
|
||||
expect(item.hoursUnknown, isTrue);
|
||||
expect(item.petPolicy, PetPolicy.unknown);
|
||||
});
|
||||
|
||||
test('모르는 코드값은 null·unknown 으로 읽는다(앱이 죽지 않는다)', () {
|
||||
final item = PlaceItem.fromJson(
|
||||
_listItem(category: 'spaceport', source: 'x', petPolicy: 'maybe'),
|
||||
);
|
||||
expect(item.category, isNull);
|
||||
expect(item.source, isNull);
|
||||
expect(item.petPolicy, PetPolicy.unknown);
|
||||
});
|
||||
|
||||
test('not_allowed 는 동반 불가, partial 은 동반 가능으로 본다', () {
|
||||
expect(PetPolicy.parse('not_allowed'), PetPolicy.notAllowed);
|
||||
expect(PetPolicy.notAllowed.isPetFriendly, isFalse);
|
||||
expect(PetPolicy.parse('partial').isPetFriendly, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaceDetail.fromJson', () {
|
||||
test('상세 — 시간·반려동물·이용 안내·출처를 읽고 빈 값은 없음으로 본다', () {
|
||||
final detail = PlaceDetail.fromJson({
|
||||
..._listItem(category: 'attraction', source: 'tour'),
|
||||
'branchName': null,
|
||||
'name': '김광석다시그리기길',
|
||||
'imageLicense': 'Type1',
|
||||
'hoursOrigin': 'public',
|
||||
'openNow': true,
|
||||
'petPolicy': 'allowed',
|
||||
'petOrigin': 'public',
|
||||
'addressJibun': null,
|
||||
'phone': null,
|
||||
'homepage': 'https://tour.daegu.go.kr/index.do',
|
||||
'overview': '',
|
||||
'imageUrl': 'https://example.com/a.jpg',
|
||||
'hours': {'text': '상시 개방', 'restText': '연중무휴', 'status': 'always_open'},
|
||||
'pet': {'sizes': null, 'conditions': '전 견종 동반 가능 · 목줄 착용'},
|
||||
'extra': {'parking': '불가능', 'infocenter': '053-627-1337', 'fee': ' '},
|
||||
'curation': null,
|
||||
'attribution': {'provider': '한국관광공사', 'asOf': '2026-06-26'},
|
||||
});
|
||||
expect(detail.item.displayName, '김광석다시그리기길');
|
||||
expect(detail.item.imageLicense, ImageLicense.type1);
|
||||
expect(detail.overview, isNull);
|
||||
expect(detail.hours.status, HoursStatus.alwaysOpen);
|
||||
expect(detail.hours.restText, '연중무휴');
|
||||
expect(detail.pet.sizes, isNull);
|
||||
expect(splitConditions(detail.pet.conditions), ['전 견종 동반 가능', '목줄 착용']);
|
||||
expect(detail.extra, {'parking': '불가능', 'infocenter': '053-627-1337'});
|
||||
expect(detail.curation, isNull);
|
||||
expect(detail.attribution.provider, '한국관광공사');
|
||||
});
|
||||
|
||||
test('운영자 확인 크기·추천 이유를 읽는다', () {
|
||||
final detail = PlaceDetail.fromJson({
|
||||
..._listItem(),
|
||||
'hours': {'text': null, 'restText': null, 'status': 'unknown'},
|
||||
'pet': {
|
||||
'sizes': ['small', 'medium', 'huge'],
|
||||
'conditions': null,
|
||||
},
|
||||
'extra': null,
|
||||
'curation': {'reason': '골목 산책하기 좋아요.', 'officialUrl': null},
|
||||
'attribution': {'provider': '소상공인시장진흥공단', 'asOf': '202609'},
|
||||
});
|
||||
expect(detail.pet.sizes, [PetSize.small, PetSize.medium]);
|
||||
expect(detail.extra, isEmpty);
|
||||
expect(detail.curation?.reason, '골목 산책하기 좋아요.');
|
||||
});
|
||||
});
|
||||
|
||||
group('PlacePage.fromJson', () {
|
||||
test('items·pageInfo(total 포함)를 읽는다', () {
|
||||
final page = PlacePage.fromJson({
|
||||
'items': [_listItem()],
|
||||
'pageInfo': {'hasNext': true, 'nextCursor': 'abc', 'total': 4500},
|
||||
});
|
||||
expect(page.items, hasLength(1));
|
||||
expect(page.hasNext, isTrue);
|
||||
expect(page.nextCursor, 'abc');
|
||||
expect(page.total, 4500);
|
||||
});
|
||||
});
|
||||
|
||||
group('place_facts', () {
|
||||
test('주차 — 확실할 때만 가능·불가, 애매하면 모름', () {
|
||||
expect(parkingAvailable('가능'), isTrue);
|
||||
expect(parkingAvailable('주차가능(승용차 20대)'), isTrue);
|
||||
expect(parkingAvailable('불가능'), isFalse);
|
||||
expect(parkingAvailable('없음'), isFalse);
|
||||
expect(parkingAvailable('인근 공영주차장 이용'), isNull);
|
||||
expect(parkingAvailable(null), isNull);
|
||||
});
|
||||
|
||||
test('입장 무료 — 유료가 섞이면 무료로 보지 않는다', () {
|
||||
expect(isFreeEntry('무료'), isTrue);
|
||||
expect(isFreeEntry('무료(일부 체험 유료)'), isFalse);
|
||||
expect(isFreeEntry('성인 3,000원'), isFalse);
|
||||
expect(isFreeEntry(null), isFalse);
|
||||
});
|
||||
|
||||
test('조건 — " · "·줄바꿈으로 나누고, 단어 안의 가운뎃점은 두다', () {
|
||||
expect(splitConditions('목줄 착용 · 실내 전시·온실 시설은 동반 불가\n배변봉투 지참'), [
|
||||
'목줄 착용',
|
||||
'실내 전시·온실 시설은 동반 불가',
|
||||
'배변봉투 지참',
|
||||
]);
|
||||
expect(splitConditions(null), isEmpty);
|
||||
});
|
||||
|
||||
test('금지 조건 — 불가·금지·제한, "제한 없음"은 아니다', () {
|
||||
expect(isRestriction('실내 동반 불가'), isTrue);
|
||||
expect(isRestriction('맹견 출입 금지'), isTrue);
|
||||
expect(isRestriction('15kg 이상 제한'), isTrue);
|
||||
expect(isRestriction('크기 제한 없음'), isFalse);
|
||||
expect(isRestriction('수성못 산책로 이용'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaceQuery', () {
|
||||
test('검색어 앞뒤 공백은 같은 조건으로 본다', () {
|
||||
expect(const PlaceQuery(text: ' 김광석 '), const PlaceQuery(text: '김광석'));
|
||||
expect(const PlaceQuery(text: ' ').queryOrNull, isNull);
|
||||
});
|
||||
|
||||
test('탭은 백엔드 분류 여러 개로 바뀐다', () {
|
||||
expect(PlaceTab.food.categories, [
|
||||
PlaceCategory.food,
|
||||
PlaceCategory.cafe,
|
||||
PlaceCategory.bar,
|
||||
]);
|
||||
expect(PlaceTab.all.categories, isEmpty);
|
||||
expect(PlaceTab.parse('nope'), PlaceTab.all);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user