2 Commits

Author SHA1 Message Date
ttipo 8e53974c3f feat(flutter): 축제·행사 칸을 공연·행사로 바꾸고 카드에 종류와 세로 포스터를 보임
CI / Backend (lint · test · e2e · build) (pull_request) Waiting to run
CI / Frontend (lint · type-check · test) (pull_request) Waiting to run
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Waiting to run
CI (Flutter) / Flutter (analyze · 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>
2026-11-12 15:45:01 +09:00
ttipo a0c507a350 chore(backend): 예시 env 에 AI 코스 설정 항목 추가
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m11s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m4s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m15s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m16s
CI / Frontend (lint · type-check · test) (push) Successful in 10m54s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m18s
AI 설정(AI_PROVIDER · 모델 · effort · 하루 횟수 · 비용 상한 · 제한 시간)이 예시 파일에 없어서,
실제 env 에 무엇을 적을 수 있는지 코드를 읽어야 알 수 있었다. 키와 기본값 설명을 빈 값으로 적어 둔다.
비어 있으면 코드 기본값을 쓰므로 예시를 그대로 옮겨도 동작이 바뀌지 않는다.

설계 §11 은 이 설정들이 루트 .env 에서 compose 로 넘어간다고 적었지만 compose 는 넘기지 않는다.
backend/.env.{env} 에 적는 것으로 고쳤다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 10:45:08 +09:00
15 changed files with 133 additions and 62 deletions
+19
View File
@@ -32,3 +32,22 @@ KAKAO_REST_API_KEY=change-me-kakao-rest-api-key
# Claude API 키 — AI 코스 생성. 사용량만큼 과금된다.
ANTHROPIC_API_KEY=change-me-anthropic-api-key
# ── AI 코스 생성 설정 (설계 docs/design/ai-course.md §11) ──
# 비워 두면 코드 기본값을 쓴다. 바꿀 때만 값을 적는다 — 값을 적으면 코드 기본값이 바뀌어도 이 값이 이긴다.
# AI 를 부르는 곳: anthropic(기본) · fake(키 없이 흐름만 확인하는 가짜 AI)
AI_PROVIDER=
# 코스 초안 모델(기본 claude-sonnet-5-5) · 조건 해석 모델(기본 claude-haiku-5-5)
# claude-haiku-5-5 · claude-sonnet-5-5 만 받는다(가격표 src/shared/utils/ai-pricing.ts). 바꾸면 npm run ai:eval 로 다시 잰다
AI_COURSE_MODEL=
AI_CONDITION_MODEL=
# 추론 깊이 low · medium · high (기본 low · low)
AI_COURSE_EFFORT=
AI_CONDITION_EFFORT=
# 하루 생성 횟수 — 비로그인(기기 기준) · 로그인(사용자 기준) (기본 2 · 5)
AI_DAILY_LIMIT_GUEST=
AI_DAILY_LIMIT_MEMBER=
# 하루 비용 상한(정수 달러, 조건 해석 포함, 기본 3) — 넘으면 그날 새 생성을 막는다
AI_DAILY_BUDGET_USD=
# AI 호출 한 번의 제한 시간(ms, 기본 30000)
AI_TIMEOUT_MS=
+19
View File
@@ -29,3 +29,22 @@ KAKAO_REST_API_KEY=change-me-kakao-rest-api-key
# Claude API 키 — 운영용 키를 따로 발급하고 사용량 한도를 걸어 둔다.
ANTHROPIC_API_KEY=change-me-anthropic-api-key
# ── AI 코스 생성 설정 (설계 docs/design/ai-course.md §11) ──
# 비워 두면 코드 기본값을 쓴다. 바꿀 때만 값을 적는다 — 값을 적으면 코드 기본값이 바뀌어도 이 값이 이긴다.
# 운영에서는 ANTHROPIC_API_KEY 가 필수이고, AI_PROVIDER=fake 면 기동하지 않는다
AI_PROVIDER=
# 코스 초안 모델(기본 claude-sonnet-5-5) · 조건 해석 모델(기본 claude-haiku-5-5)
# claude-haiku-5-5 · claude-sonnet-5-5 만 받는다(가격표 src/shared/utils/ai-pricing.ts). 바꾸면 npm run ai:eval 로 다시 잰다
AI_COURSE_MODEL=
AI_CONDITION_MODEL=
# 추론 깊이 low · medium · high (기본 low · low)
AI_COURSE_EFFORT=
AI_CONDITION_EFFORT=
# 하루 생성 횟수 — 비로그인(기기 기준) · 로그인(사용자 기준) (기본 2 · 5)
AI_DAILY_LIMIT_GUEST=
AI_DAILY_LIMIT_MEMBER=
# 하루 비용 상한(정수 달러, 조건 해석 포함, 기본 3) — 넘으면 그날 새 생성을 막는다. 운영 값은 출시 전에 정한다
AI_DAILY_BUDGET_USD=
# AI 호출 한 번의 제한 시간(ms, 기본 30000)
AI_TIMEOUT_MS=
+8 -7
View File
@@ -575,16 +575,17 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
| `ANTHROPIC_API_KEY` | `backend/.env.{env}` | — | 운영에서는 필수(없으면 기동 실패). 개발에서 없으면 AI API 가 `AI_002` 를 준다 |
| `PUBLIC_DATA_SERVICE_KEY` | `backend/.env.{env}` | — | 이미 있는 공공데이터포털 키(장소 동기화). 날씨도 쓴다. 없으면 날씨만 '예보 없음'이다 |
| `AI_PROVIDER` | `backend/.env.{env}` | `anthropic` | `fake` 면 키 없이 흐름만 돌리는 가짜 AI(개발용). 운영에서는 `fake` 면 기동 실패 |
| `AI_COURSE_MODEL` | 루트 `.env` → compose | `claude-sonnet-5-5`(평가로 정함, §8) | 코스 생성 모델. 가격표(`shared/utils/ai-pricing.ts`)에 있는 모델만 받는다 |
| `AI_CONDITION_MODEL` | 루트 `.env` → compose | `claude-haiku-5-5` | 조건 해석 모델(같은 가격표) |
| `AI_COURSE_MODEL` | `backend/.env.{env}` | `claude-sonnet-5-5`(평가로 정함, §8) | 코스 생성 모델. 가격표(`shared/utils/ai-pricing.ts`)에 있는 모델만 받는다 |
| `AI_CONDITION_MODEL` | `backend/.env.{env}` | `claude-haiku-5-5` | 조건 해석 모델(같은 가격표) |
| `AI_COURSE_EFFORT` · `AI_CONDITION_EFFORT` | `backend/.env.{env}` | `low` · `low` | 추론 깊이 low · medium · high. 평가(6단계)로 둘 다 low 로 정했다(§8) |
| `AI_DAILY_LIMIT_GUEST` · `AI_DAILY_LIMIT_MEMBER` | 루트 `.env` → compose | 2 · 5 | §5 |
| `AI_DAILY_BUDGET_USD` | 루트 `.env` → compose | 개발 3 | 하루 비용 상한(정수 달러) |
| `AI_TIMEOUT_MS` | 루트 `.env` → compose | 30000 | AI 호출 제한 시간 |
| `AI_DAILY_LIMIT_GUEST` · `AI_DAILY_LIMIT_MEMBER` | `backend/.env.{env}` | 2 · 5 | §5 |
| `AI_DAILY_BUDGET_USD` | `backend/.env.{env}` | 개발 3 | 하루 비용 상한(정수 달러) |
| `AI_TIMEOUT_MS` | `backend/.env.{env}` | 30000 | AI 호출 제한 시간 |
- 숫자는 `parsePositiveInt` 로 읽는다(빈 문자열 방어). 새 필수 값은 `env.validation.ts` 와 테스트에 함께 추가한다.
- 4단계(2026-11-08)는 `AI_DAILY_LIMIT_*` · `AI_DAILY_BUDGET_USD` · `AI_TIMEOUT_MS` 를 `ConfigService` 로 읽고 없으면 위 기본값을 쓴다(형식은 `env.validation.ts` 가 본다).
compose 로 넘기는 설정은 값을 바꿀 일이 생길 때 더한다 — 그 전에는 `backend/.env.{env}` 에 적으면 된다.
- AI 설정은 모두 `ConfigService` 로 읽고, 없거나 비어 있으면 위 기본값을 쓴다(형식은 `env.validation.ts` 가 본다).
compose 가 넘기지 않으므로 `backend/.env.{env}` 에 적는다. 예시 파일(`backend/.env.{env}.example`)에 빈 값으로 키를 모두 적어 두었다(2026-11-12).
값을 적으면 코드 기본값이 바뀌어도 그 값이 이기므로, 바꿀 때만 적는다.
- SDK(`@anthropic-ai/sdk`)는 컨테이너 안에서 설치하고, 잠금 파일을 npm 10 으로 확인한다(nestjs 스킬의 의존성 주의 항목).
5단계(2026-11-11)에 1.5.0 을 넣었다. 컨테이너 npm 11 이 지운 선택 peer 항목(chokidar 3개)은 이전 잠금 파일에서 되살렸다.
- 모델 값은 가격표에 있어야 기동한다. 하루 비용 상한이 그 가격으로 계산되기 때문이다. 모델을 더하면 가격표와 테스트를 함께 고친다.
+5 -3
View File
@@ -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 공연 (구현됨)
+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": {
+5 -4
View File
@@ -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'),