diff --git a/docs/design/place-db.md b/docs/design/place-db.md index eaad7a0..352c84e 100644 --- a/docs/design/place-db.md +++ b/docs/design/place-db.md @@ -417,11 +417,13 @@ node dist/commands/place-sync.js tour-kor | `GET /api/events/{id}` | 소개·공연 시간·요금·장소·홈페이지·전화·출처. 숨김이면 RES_001 | - **화면 규칙**(2026-11-03 합의): - - 홈 '이번 주말 축제·행사' = `from`=토 · `to`=일 · 동네 중심 좌표 · `radius`=50000 → 대구 전역(군위 포함)과 경산·칠곡 등 바로 옆 도시까지 거리순. + - 홈 '이번 주말 공연·행사' = `from`=토 · `to`=일 · 동네 중심 좌표 · `radius`=50000 → 대구 전역(군위 포함)과 경산·칠곡 등 바로 옆 도시까지 거리순. 처음(2026-11-03)에는 관광공사 행사뿐이라 120km(경북)였다. KOPIS·문화포털이 들어온 뒤 대구 도심 기준 이번 주말 50km 32건 · 120km 42건이 되었고, 120km 면 영덕·영주(차로 2시간)까지 나와 2026-11-04 50km 로 줄였다. - - **비면** 같은 반경으로 `from`=오늘 · `to`=오늘+30일을 불러 제목을 '다가오는 축제·행사'로 바꿔 보여 준다(빈 칸을 두지 않는다). - - 장소 상세 '근처 축제·행사' = 장소 좌표 + 기본 반경(5km), 없으면 칸을 숨긴다. + - **비면** 같은 반경으로 `from`=오늘 · `to`=오늘+30일을 불러 제목을 '다가오는 공연·행사'로 바꿔 보여 준다(빈 칸을 두지 않는다). + - 장소 상세 '근처 공연·행사' = 장소 좌표 + 기본 반경(5km), 없으면 칸을 숨긴다. + - 이름은 처음에 '축제·행사'였다. KOPIS 공연이 들어오며 칸의 대부분이 공연이 되어, 축제 목록으로 오해하지 않게 '공연·행사'로 바꿨다(2026-11-12 사용자 결정, 홈 카테고리 칸 포함). + 카드 아래 줄 앞에 종류(공연 · 축제 · 행사 · 전시)를 굵게 보이고, 포스터가 대부분 세로라 썸네일을 5:7 로 보인다. - 계약 테스트: `backend/test/festival.e2e-spec.ts` — 2030년 날짜·동해 좌표 픽스처라 실행 날짜와 무관하다. ### 12.1 KOPIS 공연 (구현됨) diff --git a/flutter/lib/core/theme/app_colors.dart b/flutter/lib/core/theme/app_colors.dart index 79b0fc6..84a213e 100644 --- a/flutter/lib/core/theme/app_colors.dart +++ b/flutter/lib/core/theme/app_colors.dart @@ -41,7 +41,7 @@ abstract final class AppColors { /// 사진이 없을 때의 자리 표시 static const Color placeholder = Color(0xFFF5F6FA); - /// 축제·행사 날짜 글자 + /// 공연·행사 날짜 글자 static const Color eventDate = Color(0xFF8A5A00); /// 반려동물 동반 글자(발바닥 아이콘과 함께) diff --git a/flutter/lib/features/event/presentation/providers/event_providers.dart b/flutter/lib/features/event/presentation/providers/event_providers.dart index deacc6e..3f3eba9 100644 --- a/flutter/lib/features/event/presentation/providers/event_providers.dart +++ b/flutter/lib/features/event/presentation/providers/event_providers.dart @@ -30,11 +30,11 @@ class HomeEvents { /// 조회한 기간 — '전체 보기' 가 같은 기간을 연다 final DateRange range; - /// 주말이 비어 '다가오는 축제·행사' 로 바꿔 보이는지 + /// 주말이 비어 '다가오는 공연·행사' 로 바꿔 보이는지 final bool isUpcoming; } -/// 홈 '이번 주말 축제·행사' — 기준 위치 반경 50km 거리순, 비면 같은 반경 30일 +/// 홈 '이번 주말 공연·행사' — 기준 위치 반경 50km 거리순, 비면 같은 반경 30일 final homeEventsProvider = FutureProvider((ref) async { final base = await ref.watch(basePointProvider.future); final repo = ref.watch(eventRepositoryProvider); diff --git a/flutter/lib/features/event/presentation/widgets/event_card.dart b/flutter/lib/features/event/presentation/widgets/event_card.dart index fa44a9b..75f5936 100644 --- a/flutter/lib/features/event/presentation/widgets/event_card.dart +++ b/flutter/lib/features/event/presentation/widgets/event_card.dart @@ -6,11 +6,21 @@ import 'package:wandergo/core/utils/format.dart'; import 'package:wandergo/features/event/data/models/event_models.dart'; import 'package:wandergo/features/event/presentation/widgets/event_visuals.dart'; -/// 홈 가로 목록 카드 너비·사진 높이 (디자인 캔버스 148×110) -const double kEventCardWidth = 148; -const double kEventCardImageHeight = 110; +/// 홈 가로 목록 카드 너비·사진 높이 — 세로 포스터 비율(5:7) +const double kEventCardWidth = 120; +const double kEventCardImageHeight = kEventCardWidth / kEventPosterRatio; -/// 홈 가로 목록의 한 칸 — 사진 · 기간(갈색) · 이름 · 지역·거리 (테두리 없음) +/// 사진 아래 여백 · 기간 · 이름 · 분류·지역 세 줄의 높이 +const double _kEventCardTextHeight = 72; + +/// 홈 가로 목록 높이 — 사진 + 글자 세 줄 +const double kEventCardHeight = kEventCardImageHeight + _kEventCardTextHeight; + +/// 세로 목록의 작은 사진 — 같은 포스터 비율 +const double _kEventRowImageWidth = 60; +const double _kEventRowImageHeight = _kEventRowImageWidth / kEventPosterRatio; + +/// 홈 가로 목록의 한 칸 — 포스터 · 기간(갈색) · 이름 · 분류·지역·거리 (테두리 없음) class EventCard extends StatelessWidget { const EventCard({super.key, required this.item, required this.onTap}); @@ -55,12 +65,7 @@ class EventCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), - Text( - eventMeta(item), - style: textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + EventMetaText(item: item), ], ), ), @@ -68,7 +73,7 @@ class EventCard extends StatelessWidget { } } -/// 세로 목록의 한 줄 — 작은 사진 + 기간 · 이름 · 분류·지역·거리, 아래 구분선 +/// 세로 목록의 한 줄 — 작은 포스터 + 기간 · 이름 · 분류·지역·거리, 아래 구분선 class EventRow extends StatelessWidget { const EventRow({super.key, required this.item, required this.onTap}); @@ -79,11 +84,6 @@ class EventRow extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final textTheme = Theme.of(context).textTheme; - final kind = item.kind?.label(l10n); - final meta = [ - ?kind, - eventMeta(item), - ].where((s) => s.isNotEmpty).join(' · '); return InkWell( onTap: onTap, child: Padding( @@ -94,8 +94,8 @@ class EventRow extends StatelessWidget { url: item.imageThumbUrl, kind: item.kind, license: item.imageLicense, - width: 64, - height: 64, + width: _kEventRowImageWidth, + height: _kEventRowImageHeight, ), const SizedBox(width: 14), Expanded( @@ -123,12 +123,7 @@ class EventRow extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), - Text( - meta, - style: textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + EventMetaText(item: item), ], ), ), diff --git a/flutter/lib/features/event/presentation/widgets/event_visuals.dart b/flutter/lib/features/event/presentation/widgets/event_visuals.dart index fa81c3d..bb94380 100644 --- a/flutter/lib/features/event/presentation/widgets/event_visuals.dart +++ b/flutter/lib/features/event/presentation/widgets/event_visuals.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:wandergo/core/l10n/l10n.dart'; +import 'package:wandergo/core/theme/app_colors.dart'; import 'package:wandergo/core/theme/app_theme.dart'; import 'package:wandergo/core/utils/format.dart'; import 'package:wandergo/features/event/data/models/event_models.dart'; @@ -48,6 +49,42 @@ String eventMeta(EventItem item) { return parts.join(' · '); } +/// 포스터 비율 — 축제·공연·행사 포스터가 대부분 세로(A판 1:1.41)라 5:7 로 보인다(2026-11-12) +const double kEventPosterRatio = 5 / 7; + +/// 목록 메타 줄 — "공연 · 수성구 · 1.2km". 홈 행사 칸이 대부분 공연이라 분류를 굵게 앞에 둔다(2026-11-12) +class EventMetaText extends StatelessWidget { + const EventMetaText({super.key, required this.item}); + + final EventItem item; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context).textTheme.bodySmall; + final kind = item.kind?.label(context.l10n); + final rest = eventMeta(item); + return Text.rich( + TextSpan( + children: [ + if (kind != null) + TextSpan( + text: kind, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + if (kind != null && rest.isNotEmpty) const TextSpan(text: ' · '), + if (rest.isNotEmpty) TextSpan(text: rest), + ], + ), + style: style, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } +} + /// 행사 사진 — 사진이 없으면 행사 종류별 자리 표시를 보인다([PublicImage]). class EventImage extends StatelessWidget { const EventImage({ diff --git a/flutter/lib/features/home/presentation/pages/home_page.dart b/flutter/lib/features/home/presentation/pages/home_page.dart index c56552d..b4959ad 100644 --- a/flutter/lib/features/home/presentation/pages/home_page.dart +++ b/flutter/lib/features/home/presentation/pages/home_page.dart @@ -21,10 +21,7 @@ import 'package:wandergo/shared/widgets/language_button.dart'; import 'package:wandergo/shared/widgets/load_error.dart'; import 'package:wandergo/shared/widgets/section_header.dart'; -/// 홈 가로 목록 높이 — 사진 110 + 여백·글자 세 줄 -const double _kEventStripHeight = 186; - -/// 3 홈 — 기준 위치 · 제목 · 검색창 · 카테고리 · 이번 주말 축제·행사 · 가까운 반려동물 동반 장소 · 출처. +/// 3 홈 — 기준 위치 · 제목 · 검색창 · 카테고리 · 이번 주말 공연·행사 · 가까운 반려동물 동반 장소 · 출처. /// 배너·AI·추천 코스·지역 바로가기·로그인 버튼은 해당 기능을 만들 때 붙인다. class HomePage extends ConsumerWidget { const HomePage({super.key}); @@ -195,7 +192,7 @@ class _BaseLocationButton extends ConsumerWidget { } } -/// 이번 주말 축제·행사 — 비면 다가오는 축제·행사 +/// 이번 주말 공연·행사 — 비면 다가오는 공연·행사 class _EventsSection extends ConsumerWidget { const _EventsSection(); @@ -239,7 +236,7 @@ class _EventsSection extends ConsumerWidget { ), ), AsyncData(:final value) => SizedBox( - height: _kEventStripHeight, + height: kEventCardHeight, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: kPagePadding), @@ -275,7 +272,7 @@ class _EventStripSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: _kEventStripHeight, + height: kEventCardHeight, child: ListView.separated( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), diff --git a/flutter/lib/features/place/domain/place_search.dart b/flutter/lib/features/place/domain/place_search.dart index 85c32e5..41839c9 100644 --- a/flutter/lib/features/place/domain/place_search.dart +++ b/flutter/lib/features/place/domain/place_search.dart @@ -7,7 +7,7 @@ const int kPlaceSearchRadiusM = 20000; /// 검색 화면의 분류 탭 — 백엔드 분류 여러 개를 사람이 고르는 단위로 묶는다. /// /// 디자인의 '축제' 탭은 행사 API 에 검색어 조건이 없어 아직 넣지 않았다 -/// (홈 카테고리의 '축제'는 행사 목록으로 간다). +/// (홈 카테고리의 '공연·행사'는 행사 목록으로 간다). enum PlaceTab { all([]), sights([PlaceCategory.attraction, PlaceCategory.leisure]), diff --git a/flutter/lib/features/place/presentation/providers/place_providers.dart b/flutter/lib/features/place/presentation/providers/place_providers.dart index 6c710cd..2ed7619 100644 --- a/flutter/lib/features/place/presentation/providers/place_providers.dart +++ b/flutter/lib/features/place/presentation/providers/place_providers.dart @@ -14,7 +14,7 @@ import 'package:wandergo/features/place/domain/place_search.dart'; /// 홈 '가까운 반려동물 동반 장소' 에 보이는 수 const int kHomePetPlaceLimit = 3; -/// 장소 상세 '근처 축제·행사' 반경(m) — 없으면 칸을 숨긴다(2026-11-03 합의) +/// 장소 상세 '근처 공연·행사' 반경(m) — 없으면 칸을 숨긴다(2026-11-03 합의) const int kNearbyEventRadiusM = 5000; const int kNearbyEventLimit = 3; @@ -41,7 +41,7 @@ final homePetPlacesProvider = FutureProvider>((ref) async { return page.items; }); -/// 장소 상세 '근처 축제·행사' — 그 장소 반경 5km, 오늘부터 30일 +/// 장소 상세 '근처 공연·행사' — 그 장소 반경 5km, 오늘부터 30일 final nearbyEventsProvider = FutureProvider.family, GeoPoint>(( ref, point, diff --git a/flutter/lib/features/place/presentation/widgets/place_detail_sections.dart b/flutter/lib/features/place/presentation/widgets/place_detail_sections.dart index f75721c..651beec 100644 --- a/flutter/lib/features/place/presentation/widgets/place_detail_sections.dart +++ b/flutter/lib/features/place/presentation/widgets/place_detail_sections.dart @@ -631,7 +631,7 @@ class _PlaceAboutSectionState extends State { } } -/// 근처 축제·행사 — 반경 5km·30일 안에 없으면(또는 못 불러오면) 칸을 숨긴다 +/// 근처 공연·행사 — 반경 5km·30일 안에 없으면(또는 못 불러오면) 칸을 숨긴다 class NearbyEventsSection extends ConsumerWidget { const NearbyEventsSection({super.key, required this.point}); diff --git a/flutter/lib/l10n/app_en.arb b/flutter/lib/l10n/app_en.arb index 6624cdf..d3e9d4c 100644 --- a/flutter/lib/l10n/app_en.arb +++ b/flutter/lib/l10n/app_en.arb @@ -27,9 +27,9 @@ "locationOutsideServiceArea": "WanderGo only covers Daegu for now, so we're showing downtown Daegu.", "baseLocationButtonLabel": "Change base location, current {place}", "homeTitle": "Where to this weekend?", - "homeWeekendEvents": "Festivals & events this weekend", - "homeUpcomingEvents": "Upcoming festivals & events", - "homeEventsEmpty": "No festivals or events nearby yet.", + "homeWeekendEvents": "Shows & events this weekend", + "homeUpcomingEvents": "Upcoming shows & events", + "homeEventsEmpty": "No shows or events nearby yet.", "seeAll": "See all", "retry": "Try again", "loadFailed": "Couldn't load this.", @@ -57,7 +57,7 @@ "searchClear": "Clear search", "categoryNavLabel": "Categories", "categorySights": "Sights", - "categoryFestival": "Festivals", + "categoryFestival": "Shows & events", "categoryPet": "With pets", "categoryFood": "Food & cafés", "categoryStay": "Stays", @@ -136,7 +136,7 @@ "hotlineBody": "24/7 travel help in English, 日本語, 中文", "showMore": "Show more", "showLess": "Show less", - "nearbyEvents": "Festivals & events nearby", + "nearbyEvents": "Shows & events nearby", "nearbyPlaces": "Good to go with", "photoCredit": "Photo: {provider} · {license}", "koglType1": "KOGL Type 1", diff --git a/flutter/lib/l10n/app_ko.arb b/flutter/lib/l10n/app_ko.arb index 412d98b..6473427 100644 --- a/flutter/lib/l10n/app_ko.arb +++ b/flutter/lib/l10n/app_ko.arb @@ -45,9 +45,9 @@ } }, "homeTitle": "이번 주말, 어디 갈까요?", - "homeWeekendEvents": "이번 주말 축제·행사", - "homeUpcomingEvents": "다가오는 축제·행사", - "homeEventsEmpty": "근처에 예정된 축제·행사가 아직 없어요.", + "homeWeekendEvents": "이번 주말 공연·행사", + "homeUpcomingEvents": "다가오는 공연·행사", + "homeEventsEmpty": "근처에 예정된 공연·행사가 아직 없어요.", "seeAll": "전체 보기", "retry": "다시 시도", "loadFailed": "불러오지 못했어요.", @@ -92,7 +92,7 @@ "searchClear": "검색어 지우기", "categoryNavLabel": "카테고리", "categorySights": "관광지", - "categoryFestival": "축제", + "categoryFestival": "공연·행사", "categoryPet": "반려동물", "categoryFood": "맛집·카페", "categoryStay": "숙소", @@ -226,7 +226,7 @@ "hotlineBody": "영어·일본어·중국어 24시간 안내", "showMore": "더 보기", "showLess": "접기", - "nearbyEvents": "근처 축제·행사", + "nearbyEvents": "근처 공연·행사", "nearbyPlaces": "함께 가기 좋은 곳", "photoCredit": "사진 {provider} · {license}", "@photoCredit": { diff --git a/flutter/test/widget_test.dart b/flutter/test/widget_test.dart index e535b90..0ee0ce0 100644 --- a/flutter/test/widget_test.dart +++ b/flutter/test/widget_test.dart @@ -255,9 +255,10 @@ void main() { expect(_text('이번 주말, 어디 갈까요?'), findsOneWidget); expect(find.text('수성못'), findsOneWidget); - expect(find.text('이번 주말 축제·행사'), findsOneWidget); + expect(find.text('이번 주말 공연·행사'), findsOneWidget); expect(find.text('수성빛예술제'), findsOneWidget); - expect(find.text('수성구 · 1.2km'), findsOneWidget); + // 홈 행사 칸이 대부분 공연이라 분류를 지역 앞에 보인다 + expect(find.text('축제 · 수성구 · 1.2km'), findsOneWidget); expect(repo.calls.single.near, Neighborhood.suseongmot.point); }); @@ -271,14 +272,14 @@ void main() { expect(_text('현재 위치를 가져오지 못해 대구 도심을 기준으로 보여 드려요.'), findsOneWidget); }); - testWidgets('주말 행사가 없으면 다가오는 축제·행사(30일)로 바꿔 보인다', (tester) async { + testWidgets('주말 행사가 없으면 다가오는 공연·행사(30일)로 바꿔 보인다', (tester) async { final repo = await _pumpApp( tester, prefs: {'onboarding_done': true}, repo: _FakeEventRepository(upcoming: [_event('e2', '대구 재즈 페스티벌')]), ); - expect(find.text('다가오는 축제·행사'), findsOneWidget); + expect(find.text('다가오는 공연·행사'), findsOneWidget); expect(find.text('대구 재즈 페스티벌'), findsOneWidget); expect(repo.calls.map((c) => c.range).toList(), [ (from: '2026-11-07', to: '2026-11-08'),