feat(flutter): 홈 화면 구현과 반려동물 등록 연결

시안 screen-home 의 흐름을 옮겼다.
- 인사말(소셜 프로필 이름, 모르면 "집사") · 알림 아이콘
- 우리 아이: 없으면 등록 안내 카드 → 등록 화면, 있으면 아이 고르기 + 펫 명함 빈 슬롯
- 일정(오늘 일정·오늘 할 일·예약 일정)·다이어리는 해당 기능 전까지 시안의 빈 상태로 표시
- 등록·수정 화면을 탭바 위에 덮는 라우트로 연결, 토스트를 모든 화면 위에 표시

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 19:35:39 +09:00
parent 50d1aeea4b
commit 2fab3af26d
10 changed files with 673 additions and 3 deletions
+92
View File
@@ -0,0 +1,92 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tma_app/app.dart';
import 'package:tma_app/core/storage/preferences_provider.dart';
import 'package:tma_app/features/pet/domain/pet.dart';
import 'package:tma_app/features/pet/presentation/pages/pet_register_page.dart';
/// 로그인·온보딩까지 마친 사용자
const Map<String, Object> _signedIn = {
'entry.introSeen': true,
'entry.onboardingSeen': true,
'auth.demoSession': 'kakao',
};
Future<void> _launchHome(WidgetTester tester, {Map<String, Object> extra = const {}}) async {
tester.view.physicalSize = const Size(402, 874);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
SharedPreferences.setMockInitialValues({..._signedIn, ...extra});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
child: const RupApp(),
),
);
// 스플래시 이미지 디코딩 → 2.6초 뒤 홈으로
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 300)));
await tester.pump(const Duration(seconds: 4));
await _settle(tester);
}
Future<void> _settle(WidgetTester tester) async {
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
await tester.pump();
}
void main() {
testWidgets('아이가 없으면 홈에 등록 안내가 보이고, 누르면 등록 화면으로 간다', (tester) async {
await _launchHome(tester);
expect(find.text('안녕하세요, 집사님'), findsOneWidget);
expect(find.text('우리 아이를 등록해 주세요'), findsOneWidget);
expect(find.text('오늘은 등록된 일정이 없어요'), findsOneWidget);
await tester.tap(find.text('우리 아이를 등록해 주세요'));
await _settle(tester);
expect(find.byType(PetRegisterPage), findsOneWidget);
});
testWidgets('기본 정보 없이 등록을 누르면 빠진 칸마다 오류를 보여 준다', (tester) async {
await _launchHome(tester);
await tester.tap(find.text('우리 아이를 등록해 주세요'));
await _settle(tester);
await tester.ensureVisible(find.text('반려동물 등록').last);
await tester.tap(find.text('반려동물 등록').last);
await _settle(tester);
expect(find.text('이름을 입력해주세요.'), findsOneWidget);
expect(find.text('종을 선택해주세요.'), findsOneWidget);
expect(find.text('생년월일을 선택해주세요.'), findsOneWidget);
expect(find.text('몸무게를 입력해주세요.'), findsOneWidget);
});
testWidgets('등록된 아이가 있으면 아이 목록과 명함 만들기 안내가 보인다', (tester) async {
final pet = Pet.fromInput(
'pet-1',
PetInput(
name: '콩이',
species: '강아지',
breed: '말티즈',
gender: PetGender.male,
neutered: true,
birthDate: DateTime(2021, 9, 14),
weight: 3.2,
weightUnit: WeightUnit.kg,
),
);
await _launchHome(tester, extra: {'pets.v1': jsonEncode([pet.toJson()])});
expect(find.text('우리 아이를 등록해 주세요'), findsNothing);
expect(find.text('콩이'), findsWidgets);
expect(find.text('명함 만들기'), findsOneWidget);
});
}