feat(flutter): 축제·행사 칸을 공연·행사로 바꾸고 카드에 종류와 세로 포스터를 보임
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Waiting to run
CI (Flutter) / Flutter (analyze · test) (pull_request) Failing after 22m20s
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m37s
CI / Frontend (lint · type-check · test) (pull_request) Has started running

KOPIS 공연이 들어온 뒤 홈 '이번 주말' 칸의 대부분이 공연이 되었다.
'축제·행사'라는 이름이면 축제 목록으로 오해하기 쉬워 '공연·행사'로 바꿨다
(이번 주말 · 다가오는 · 장소 상세 근처 · 빈 문구 · 홈 카테고리 칸, 영어 Shows & events).

- 카드 아래 줄 앞에 종류(공연 · 축제 · 행사 · 전시)를 굵게 보인다. 배지는 쓰지 않는다
- 포스터가 대부분 세로(A판)라 썸네일을 5:7 로 보인다(홈 120×168, 세로 목록 60×84).
  공공누리 3유형 사진은 그대로 자르지 않는다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 15:45:01 +09:00
parent a0c507a350
commit 8e53974c3f
12 changed files with 87 additions and 55 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ abstract final class AppColors {
/// 사진이 없을 때의 자리 표시
static const Color placeholder = Color(0xFFF5F6FA);
/// 축제·행사 날짜 글자
/// 공연·행사 날짜 글자
static const Color eventDate = Color(0xFF8A5A00);
/// 반려동물 동반 글자(발바닥 아이콘과 함께)
@@ -30,11 +30,11 @@ class HomeEvents {
/// 조회한 기간 — '전체 보기' 가 같은 기간을 연다
final DateRange range;
/// 주말이 비어 '다가오는 축제·행사' 로 바꿔 보이는지
/// 주말이 비어 '다가오는 공연·행사' 로 바꿔 보이는지
final bool isUpcoming;
}
/// 홈 '이번 주말 축제·행사' — 기준 위치 반경 50km 거리순, 비면 같은 반경 30일
/// 홈 '이번 주말 공연·행사' — 기준 위치 반경 50km 거리순, 비면 같은 반경 30일
final homeEventsProvider = FutureProvider<HomeEvents>((ref) async {
final base = await ref.watch(basePointProvider.future);
final repo = ref.watch(eventRepositoryProvider);
@@ -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),
],
),
),
@@ -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({
@@ -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(),
@@ -7,7 +7,7 @@ const int kPlaceSearchRadiusM = 20000;
/// 검색 화면의 분류 탭 — 백엔드 분류 여러 개를 사람이 고르는 단위로 묶는다.
///
/// 디자인의 '축제' 탭은 행사 API 에 검색어 조건이 없어 아직 넣지 않았다
/// (홈 카테고리의 '축제'는 행사 목록으로 간다).
/// (홈 카테고리의 '공연·행사'는 행사 목록으로 간다).
enum PlaceTab {
all([]),
sights([PlaceCategory.attraction, PlaceCategory.leisure]),
@@ -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<List<PlaceItem>>((ref) async {
return page.items;
});
/// 장소 상세 '근처 축제·행사' — 그 장소 반경 5km, 오늘부터 30일
/// 장소 상세 '근처 공연·행사' — 그 장소 반경 5km, 오늘부터 30일
final nearbyEventsProvider = FutureProvider.family<List<EventItem>, GeoPoint>((
ref,
point,
@@ -631,7 +631,7 @@ class _PlaceAboutSectionState extends State<PlaceAboutSection> {
}
}
/// 근처 축제·행사 — 반경 5km·30일 안에 없으면(또는 못 불러오면) 칸을 숨긴다
/// 근처 공연·행사 — 반경 5km·30일 안에 없으면(또는 못 불러오면) 칸을 숨긴다
class NearbyEventsSection extends ConsumerWidget {
const NearbyEventsSection({super.key, required this.point});
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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": {