generated from ttipo/project-base
feat(flutter): 추억 보관함 — 보관하기·다시 보기·다시 불러오기
반려동물 정보에서 아이를 추억 보관함으로 옮기고(함께한 기간·메모), 마이페이지의 보관함에서 메모리얼 사진·추억 기록과 원래 정보·펫 명함·다이어리(읽기 전용)를 다시 보며, 다시 불러올 수 있게 했다. 보관 기록은 반려동물에 붙여 저장한다(Pet.archive) — 서버가 생기면 보관/복구 요청으로 바꾼다. 보관하면 그 아이의 일정은 지우고(여러 아이 일정은 그 아이만 뗀다), 홈·나의 반려동물·펫 명함·일정 대상에서 빼며, 그 아이만 담은 다이어리는 책장·홈 최근 기록·기록 알림과 다이어리 수 한도에서 뺀다. 화면은 petListProvider 를 직접 보고 목록 확장(active/archived)으로 거른다 — 걸러 낸 목록을 파생 프로바이더로 두면 Riverpod 3.3 의 일시정지 해제 때 markNeedsBuild 오류가 난다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tma_app/core/utils/korean.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary_book.dart';
|
||||
import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
import 'package:tma_app/features/pet/data/repositories/pet_repository.dart';
|
||||
import 'package:tma_app/features/pet/domain/pet.dart';
|
||||
import 'package:tma_app/features/pet/domain/pet_archive.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
final PetArchive _archive = PetArchive(
|
||||
from: DateTime(2021, 9, 14),
|
||||
to: DateTime(2026, 9, 28),
|
||||
archivedAt: DateTime(2026, 9, 28, 21, 5),
|
||||
memo: '고마웠어',
|
||||
);
|
||||
|
||||
Diary _diary(String id, List<String> targets) =>
|
||||
Diary(id: id, name: id, targetIds: targets, ribbon: kRibbonColors.first, tint: kDiaryTints.first);
|
||||
|
||||
DiaryPost _post(String id, String diaryId, DateTime date) =>
|
||||
DiaryPost(id: id, diaryId: diaryId, date: date, createdAt: date, text: id, mood: Mood.happy);
|
||||
|
||||
void main() {
|
||||
group('togetherSpan', () {
|
||||
test('해와 달로 읽는다', () {
|
||||
expect(togetherSpan(DateTime(2018, 6, 1), DateTime(2026, 9, 1)), '8년 3개월');
|
||||
expect(togetherSpan(DateTime(2024, 3, 10), DateTime(2026, 3, 10)), '2년');
|
||||
expect(togetherSpan(DateTime(2026, 4, 1), DateTime(2026, 9, 15)), '5개월');
|
||||
});
|
||||
|
||||
test('마지막 날이 첫날의 날짜보다 이르면 그 달은 세지 않는다', () {
|
||||
expect(togetherSpan(DateTime(2021, 9, 14), DateTime(2026, 9, 13)), '4년 11개월');
|
||||
});
|
||||
|
||||
test('한 달이 안 되면 "한 달"', () {
|
||||
expect(togetherSpan(DateTime(2026, 9, 1), DateTime(2026, 9, 20)), '한 달');
|
||||
expect(togetherSpan(DateTime(2026, 9, 20), DateTime(2026, 9, 1)), '한 달');
|
||||
});
|
||||
});
|
||||
|
||||
test('공동격 조사는 받침에 따라 와/과', () {
|
||||
expect(withAndParticle('콩이'), '콩이와');
|
||||
expect(withAndParticle('사랑'), '사랑과');
|
||||
expect(withAndParticle('Coco'), 'Coco와');
|
||||
});
|
||||
|
||||
group('보관 기록 저장', () {
|
||||
test('보관 기록은 JSON 으로 오가도 같다', () {
|
||||
final back = PetArchive.fromJson(_archive.toJson());
|
||||
expect(back.from, _archive.from);
|
||||
expect(back.to, _archive.to);
|
||||
expect(back.archivedAt, _archive.archivedAt);
|
||||
expect(back.memo, '고마웠어');
|
||||
expect(back.spanLabel, '5년');
|
||||
});
|
||||
|
||||
test('보관한 아이는 보관 기록까지 저장되고, 함께 지내는 아이는 보관 항목이 없다', () {
|
||||
final pet = samplePet();
|
||||
expect(pet.toJson().containsKey('archive'), isFalse);
|
||||
expect(Pet.fromJson(pet.toJson()).isArchived, isFalse);
|
||||
|
||||
final archived = Pet.fromJson(pet.withArchive(_archive).toJson());
|
||||
expect(archived.isArchived, isTrue);
|
||||
expect(archived.archive!.memo, '고마웠어');
|
||||
expect(archived.name, pet.name);
|
||||
expect(archived.tint, pet.tint);
|
||||
});
|
||||
});
|
||||
|
||||
group('LocalPetRepository — 보관·복구', () {
|
||||
Future<LocalPetRepository> repo() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final r = LocalPetRepository(await SharedPreferences.getInstance());
|
||||
await r.create(samplePet());
|
||||
return r;
|
||||
}
|
||||
|
||||
test('보관하면 보관 기록이 붙고, 다시 불러오면 떨어진다', () async {
|
||||
final r = await repo();
|
||||
final id = (await r.fetchPets()).single.id;
|
||||
|
||||
await r.archive(id, _archive);
|
||||
expect((await r.fetchPets()).single.isArchived, isTrue);
|
||||
|
||||
await r.restore(id);
|
||||
expect((await r.fetchPets()).single.isArchived, isFalse);
|
||||
});
|
||||
|
||||
test('정보를 수정해도 보관 기록은 그대로다', () async {
|
||||
final r = await repo();
|
||||
final id = (await r.fetchPets()).single.id;
|
||||
await r.archive(id, _archive);
|
||||
|
||||
final updated = await r.update(id, samplePet(name: '콩순이'));
|
||||
expect(updated.name, '콩순이');
|
||||
expect(updated.isArchived, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('보관된 아이의 다이어리', () {
|
||||
final book = DiaryBook(
|
||||
diaries: [
|
||||
_diary('only', ['pet-1']),
|
||||
_diary('mixed', ['pet-1', 'pet-2']),
|
||||
_diary('other', ['pet-2']),
|
||||
],
|
||||
posts: [
|
||||
_post('a', 'only', DateTime(2026, 9, 20)),
|
||||
_post('b', 'mixed', DateTime(2026, 9, 21)),
|
||||
_post('c', 'other', DateTime(2026, 9, 22)),
|
||||
],
|
||||
);
|
||||
|
||||
test('보관된 아이만 담은 다이어리는 책장에서 빠진다', () {
|
||||
expect(book.shelf({'pet-1'}).map((d) => d.id), ['mixed', 'other']);
|
||||
expect(book.shelf().map((d) => d.id), ['only', 'mixed', 'other']);
|
||||
});
|
||||
|
||||
test('책장이 가득 찼는지는 보관함으로 간 다이어리를 빼고 센다', () {
|
||||
final full = DiaryBook(diaries: [
|
||||
for (var i = 0; i < kMaxDiaries - 1; i++) _diary('d$i', ['pet-2']),
|
||||
_diary('gone', ['pet-1']),
|
||||
]);
|
||||
expect(full.atMax(), isTrue);
|
||||
expect(full.atMax({'pet-1'}), isFalse);
|
||||
});
|
||||
|
||||
test('아이가 담긴 다이어리와 그 기록 수', () {
|
||||
expect(book.diariesOf('pet-1').map((d) => d.id), ['only', 'mixed']);
|
||||
expect(book.postCountOf('pet-1'), 2);
|
||||
});
|
||||
|
||||
test('최근 기록은 고른 다이어리에서만 뽑을 수 있다', () {
|
||||
expect(book.latest(3).map((p) => p.id), ['c', 'b', 'a']);
|
||||
expect(book.latest(3, diaryIds: {'mixed', 'other'}).map((p) => p.id), ['c', 'b']);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tma_app/core/utils/hour_minute.dart';
|
||||
import 'package:tma_app/features/archive/presentation/pages/archive_list_page.dart';
|
||||
import 'package:tma_app/features/archive/presentation/pages/pet_archive_move_page.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary.dart';
|
||||
import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
import 'package:tma_app/features/diary/presentation/pages/diary_page.dart';
|
||||
import 'package:tma_app/features/pet/domain/pet_archive.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_style.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/pet_card.dart';
|
||||
import 'package:tma_app/features/pet_card/presentation/widgets/name_card.dart';
|
||||
import 'package:tma_app/features/pet_card/presentation/widgets/pet_id_card.dart';
|
||||
import 'package:tma_app/features/schedule/domain/event_category.dart';
|
||||
import 'package:tma_app/features/schedule/domain/schedule_event.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
final DateTime _today = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
|
||||
|
||||
final Diary _diary = Diary(
|
||||
id: 'd1',
|
||||
name: '콩이 성장일기',
|
||||
targetIds: const ['pet-1'],
|
||||
ribbon: kRibbonColors.first,
|
||||
tint: kDiaryTints.first,
|
||||
);
|
||||
|
||||
final DiaryPost _post = DiaryPost(
|
||||
id: 'p1',
|
||||
diaryId: 'd1',
|
||||
date: DateTime(2026, 9, 1),
|
||||
createdAt: DateTime(2026, 9, 1, 9),
|
||||
text: '공원 산책',
|
||||
mood: Mood.happy,
|
||||
);
|
||||
|
||||
final ScheduleEvent _walk = ScheduleEvent(
|
||||
id: 'e1',
|
||||
date: _today,
|
||||
details: const EventDetails(
|
||||
title: '저녁 산책',
|
||||
kind: EventKind.general,
|
||||
category: EventCategory.walk,
|
||||
targetIds: ['pet-1'],
|
||||
start: HourMinute(19, 0),
|
||||
),
|
||||
);
|
||||
|
||||
final PetArchive _archive = PetArchive(
|
||||
from: DateTime(2021, 9, 14),
|
||||
to: DateTime(2026, 9, 14),
|
||||
archivedAt: DateTime(2026, 9, 14, 21),
|
||||
memo: '고마웠어 콩이야',
|
||||
);
|
||||
|
||||
/// 콩이 + 다이어리·기록·명함·일정. [archived] 면 콩이는 이미 추억 보관함에 있다
|
||||
Map<String, Object> _prefs({bool archived = false}) {
|
||||
final pet = archived ? samplePet().withArchive(_archive) : samplePet();
|
||||
return {
|
||||
'pets.v1': jsonEncode([pet.toJson()]),
|
||||
'diary.diaries.v1': jsonEncode([_diary.toJson()]),
|
||||
'diary.posts.v1': jsonEncode([_post.toJson()]),
|
||||
'petCards.v1': jsonEncode([PetCard(id: 'c1', petId: 'pet-1', style: CardStyle.initial()).toJson()]),
|
||||
'schedule.events.v1': jsonEncode([_walk.toJson()]),
|
||||
};
|
||||
}
|
||||
|
||||
/// 목록 아래쪽 줄은 스크롤해야 만들어진다 — 보일 때까지 내려서 누른다
|
||||
Future<void> _tapVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.scrollUntilVisible(finder, 200, scrollable: find.byType(Scrollable).first);
|
||||
await tester.pump();
|
||||
await tester.tap(finder);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> _openMyPage(WidgetTester tester) async {
|
||||
await tester.tap(find.bySemanticsLabel('마이페이지'));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> _openArchive(WidgetTester tester) async {
|
||||
await _openMyPage(tester);
|
||||
await _tapVisible(tester, find.text('보관된 아이'));
|
||||
expect(find.byType(ArchiveListPage), findsOneWidget);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('반려동물 정보에서 추억 보관함으로 옮기면 일정은 지워지고 홈·책장에서 빠진다', (tester) async {
|
||||
final prefs = await launchSignedIn(tester, extra: _prefs());
|
||||
await _openMyPage(tester);
|
||||
await tester.tap(find.bySemanticsLabel('반려동물 1마리'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('콩이'));
|
||||
await settle(tester);
|
||||
|
||||
await _tapVisible(tester, find.text('추억 보관함으로'));
|
||||
expect(find.text('콩이를 추억 보관함으로'), findsOneWidget);
|
||||
await tester.tap(find.text('보관하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byType(PetArchiveMovePage), findsOneWidget);
|
||||
expect(find.text('콩이와 함께한 기간을 알려주세요'), findsOneWidget);
|
||||
expect(find.text('2021.09.14'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), '고마웠어');
|
||||
await tester.tap(find.text('추억 보관함으로 옮기기'));
|
||||
await settle(tester);
|
||||
|
||||
// 마이페이지로 돌아와 보관함에 한 마리
|
||||
expect(find.text('추억보관함에 보관했어요.'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('반려동물 0마리'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(find.text('보관된 아이'), 200, scrollable: find.byType(Scrollable).first);
|
||||
expect(find.text('1마리'), findsOneWidget);
|
||||
|
||||
final saved = prefs.getString('pets.v1')!;
|
||||
expect(saved, contains('"archive"'));
|
||||
expect(saved, contains('고마웠어'));
|
||||
// 콩이만의 일정은 지운다
|
||||
expect(prefs.getString('schedule.events.v1'), isNot(contains('저녁 산책')));
|
||||
|
||||
// 홈에는 아이 등록 안내, 다이어리 책장에서도 빠진다
|
||||
await tester.tap(find.bySemanticsLabel('홈'));
|
||||
await settle(tester);
|
||||
expect(find.text('우리 아이를 등록해 주세요'), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('일정·기록'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('다이어리'));
|
||||
await settle(tester);
|
||||
expect(find.text('콩이 성장일기'), findsNothing);
|
||||
expect(find.text('0/6'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('보관된 아이를 누르면 함께한 기간과 메모, 원래 정보·명함·다이어리를 다시 본다', (tester) async {
|
||||
await launchSignedIn(tester, extra: _prefs(archived: true));
|
||||
await _openArchive(tester);
|
||||
expect(find.text('1마리'), findsOneWidget);
|
||||
expect(find.text('강아지 · 말티즈 · 기록 1개'), findsOneWidget);
|
||||
expect(find.text('2021.09.14 ~ 2026.09.14'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('콩이'));
|
||||
await settle(tester);
|
||||
expect(find.text('5년 함께했어요'), findsOneWidget);
|
||||
expect(find.text('고마웠어 콩이야'), findsOneWidget);
|
||||
expect(find.text('함께한 펫 명함 1개'), findsOneWidget);
|
||||
|
||||
await _tapVisible(tester, find.text('기본·건강 정보를 다시 봐요.'));
|
||||
expect(find.text('콩이의 정보'), findsOneWidget);
|
||||
expect(find.text('기본 정보'), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('뒤로'));
|
||||
await settle(tester);
|
||||
|
||||
await _tapVisible(tester, find.text('함께한 펫 명함 1개'));
|
||||
expect(find.byType(NameCard), findsOneWidget);
|
||||
await tester.tap(find.byType(NameCard));
|
||||
await settle(tester);
|
||||
expect(find.byType(PetIdCard), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('뒤로'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.bySemanticsLabel('뒤로'));
|
||||
await settle(tester);
|
||||
|
||||
await _tapVisible(tester, find.text('함께한 날들을 다시 봐요.'));
|
||||
expect(find.text('다이어리 · 1권'), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('콩이 성장일기, 기록 1개'));
|
||||
await settle(tester);
|
||||
|
||||
// 읽기만 — 쓰기·설정 버튼이 없다
|
||||
expect(find.byType(DiaryPage), findsOneWidget);
|
||||
expect(find.text('공원 산책'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('다이어리 쓰기'), findsNothing);
|
||||
expect(find.bySemanticsLabel('다이어리 설정'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('다시 불러오면 보관함에서 빠지고 나의 반려동물로 돌아온다', (tester) async {
|
||||
final prefs = await launchSignedIn(tester, extra: _prefs(archived: true));
|
||||
await _openArchive(tester);
|
||||
await tester.tap(find.text('콩이'));
|
||||
await settle(tester);
|
||||
|
||||
await _tapVisible(tester, find.text('콩이 다시 불러오기'));
|
||||
expect(find.text('다시 불러왔어요'), findsOneWidget);
|
||||
expect(find.byType(ArchiveListPage), findsOneWidget);
|
||||
expect(find.text('보관된 아이가 없어요'), findsOneWidget);
|
||||
expect(prefs.getString('pets.v1'), isNot(contains('"archive"')));
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('뒤로'));
|
||||
await settle(tester);
|
||||
expect(find.bySemanticsLabel('반려동물 1마리'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -94,7 +94,7 @@ void main() {
|
||||
|
||||
test('다이어리는 6권까지', () {
|
||||
final book = DiaryBook(diaries: [for (var i = 0; i < kMaxDiaries; i++) _diary('d$i')]);
|
||||
expect(book.atMax, isTrue);
|
||||
expect(book.atMax(), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user