feat(flutter): 마이페이지·나의 반려동물·반려동물 정보·설정 구현

시안은 아이를 등록하면 "나의 반려동물" 목록으로 보내므로 명함 다음에 만든다.
- 마이페이지: 프로필(닉네임·사진 편집), 반려동물·명함 바로가기, 권한·알림 스위치
  (스위치는 앱 설정값만 저장한다 — OS 권한·푸시는 해당 기능에서 요청)
- 나의 반려동물 / 반려동물 정보(읽기 전용, 연필로 수정) — 아이마다 고유색을 배정한다
- 설정: 약관 문서, 계정 변경·로그아웃, 회원탈퇴(기기 기록 삭제 후 인트로로)
- 등록·삭제 후와 홈 "아이 상세보기"는 나의 반려동물 목록으로 간다

시안 대조 중 발견한 공통 문제도 고쳤다.
- 줄 높이를 주지 않은 글자가 본문 기본값(1.6)을 물려받아 시안보다 줄이 컸다
  → AppTypography.normalHeight 를 두고 섹션 제목·시트 제목에 적용 (홈 간격 보정 포함)
- 버튼이 옆 제목 글자와 한 의미 단위로 합쳐져 스크린리더가 함께 읽었다
  → Pressable·설정 줄을 별도 의미 노드로

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:50:10 +09:00
parent 46bb77df9b
commit 1dba0c80ab
36 changed files with 2049 additions and 21 deletions
+135
View File
@@ -0,0 +1,135 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tma_app/features/auth/presentation/pages/login_page.dart';
import 'package:tma_app/features/intro/presentation/pages/intro_page.dart';
import 'package:tma_app/features/mypage/presentation/pages/my_pets_page.dart';
import 'package:tma_app/features/mypage/presentation/pages/settings_page.dart';
import 'package:tma_app/features/pet/presentation/pages/pet_info_page.dart';
import 'package:tma_app/features/pet/presentation/pages/pet_register_page.dart';
import 'support/app_harness.dart';
Map<String, Object> _withPet() => {'pets.v1': jsonEncode([samplePet().toJson()])};
Future<void> _openMyPage(WidgetTester tester) async {
await tester.tap(find.bySemanticsLabel('마이페이지'));
await settle(tester);
}
Future<void> _openSettings(WidgetTester tester) async {
await _openMyPage(tester);
await tester.tap(find.bySemanticsLabel('설정'));
await settle(tester);
expect(find.byType(SettingsPage), findsOneWidget);
}
void main() {
testWidgets('홈의 "아이 상세보기"는 나의 반려동물 목록으로, 아이를 누르면 반려동물 정보로 간다', (tester) async {
await launchSignedIn(tester, extra: _withPet());
await tester.tap(find.text('아이 상세보기'));
await settle(tester);
expect(find.byType(MyPetsPage), findsOneWidget);
expect(find.text('1마리와 함께해요'), findsOneWidget);
expect(find.text('강아지 · 말티즈'), findsOneWidget);
expect(find.text('3.2kg'), findsOneWidget);
await tester.tap(find.text('콩이'));
await settle(tester);
expect(find.byType(PetInfoPage), findsOneWidget);
expect(find.text('콩이의 상세 정보'), findsOneWidget);
expect(find.text('2021.09.14'), findsOneWidget);
expect(find.text('남아 · 중성화 완료'), findsOneWidget);
// 혈액형·마이크로칩·담당 병원을 입력하지 않았다
expect(find.text('미등록'), findsNWidgets(3));
// 알레르기·보유 질환·염려 질환이 비어 있다
expect(find.text('없음'), findsNWidgets(3));
expect(find.text('명함 만들기'), findsOneWidget);
// 연필 → 수정 화면, 뒤로 → 정보 화면
await tester.tap(find.bySemanticsLabel('콩이 정보 수정'));
await settle(tester);
expect(find.byType(PetRegisterPage), findsOneWidget);
expect(find.text('반려동물 정보 수정'), findsOneWidget);
});
testWidgets('마이페이지는 로그인 계정과 아이 수·명함 수를 보여 준다', (tester) async {
await launchSignedIn(tester, extra: _withPet());
await _openMyPage(tester);
expect(find.text('카카오 계정으로 로그인했어요'), findsOneWidget);
expect(find.text('1마리'), findsOneWidget);
expect(find.text('0장'), findsOneWidget);
expect(find.text('카메라 접근 권한'), findsOneWidget);
});
testWidgets('알림 스위치를 바꾸면 저장되고 안내가 뜬다', (tester) async {
final prefs = await launchSignedIn(tester);
await _openMyPage(tester);
await tester.tap(find.bySemanticsLabel('혜택 및 이벤트 알림'));
await settle(tester);
expect(prefs.getBool('settings.notiEvent'), isTrue);
expect(find.text('혜택·이벤트 알림을 켰어요'), findsOneWidget);
});
testWidgets('프로필 편집에서 닉네임을 바꾸면 홈 인사말도 바뀐다', (tester) async {
final prefs = await launchSignedIn(tester);
await _openMyPage(tester);
await tester.tap(find.text('편집'));
await settle(tester);
expect(find.text('프로필 편집'), findsOneWidget);
await tester.enterText(find.byType(TextField), '나영');
await tester.tap(find.text('수정 완료'));
await settle(tester);
expect(prefs.getString('profile.nickname'), '나영');
expect(find.text('나영'), findsOneWidget);
await tester.tap(find.bySemanticsLabel('홈'));
await settle(tester);
expect(find.text('안녕하세요, 나영님'), findsOneWidget);
});
testWidgets('설정에서 약관 문서를 열 수 있다', (tester) async {
await launchSignedIn(tester);
await _openSettings(tester);
await tester.tap(find.text('이용약관'));
await settle(tester);
expect(find.text('제1조 (목적)'), findsOneWidget);
expect(find.text('2026년 1월 1일 시행'), findsOneWidget);
});
testWidgets('로그아웃을 확인하면 로그인 화면으로 가고 기록은 남는다', (tester) async {
final prefs = await launchSignedIn(tester, extra: _withPet());
await _openSettings(tester);
await tester.tap(find.text('로그아웃'));
await settle(tester);
expect(find.text('로그아웃할까요?'), findsOneWidget);
await tester.tap(find.text('로그아웃').last);
await settle(tester);
expect(find.byType(LoginPage), findsOneWidget);
expect(prefs.getString('pets.v1'), isNotNull);
});
testWidgets('회원탈퇴를 확인하면 기기의 기록을 지우고 인트로로 돌아간다', (tester) async {
final prefs = await launchSignedIn(tester, extra: _withPet());
await _openSettings(tester);
await tester.tap(find.text('회원탈퇴'));
await settle(tester);
expect(find.text('정말 탈퇴할까요?'), findsOneWidget);
await tester.tap(find.text('탈퇴하기'));
await settle(tester);
expect(find.byType(IntroPage), findsOneWidget);
expect(prefs.getString('pets.v1'), isNull);
expect(prefs.getString('auth.demoSession'), isNull);
});
}