generated from ttipo/project-base
feat(flutter): 다이어리 책장·기록·댓글·달력 보기와 홈 최근 기록 구현
일정 관리 다음 흐름 — 일정·기록 탭의 다이어리. - 책장: 표지 그리드(최대 6권), 반투명 책갈피 리본, 새 다이어리 만들기(표지·이름·대상·리본 색) - 다이어리 안: 기록 피드(모두/사진/글) · 검색 · 달력 보기(날마다 기분 표정) · 하루 한 번 쓰기 버튼 - 기록하기: 사진 · 오늘 기분 6종 · 글 / 기록 수정·삭제 / 댓글 작성·수정·삭제 - 다이어리 설정: 정보 수정, 알림 스위치(값만 저장), 다이어리 제거(기록·댓글 함께) - 홈 다이어리: 최근 기록 3개를 사진/글 타일로, 누르면 그 다이어리로 - 아이를 지우면 다이어리 대상에서만 떼고, 회원탈퇴는 다이어리도 지운다 - 기록은 기기에 저장한다 (공유 다이어리 서버가 생기면 DiaryRepository 구현을 바꾼다) 시안과 다른 점: 공유 다이어리(초대 코드·QR·멤버·참여)는 서버가 필요해 안내만 둔다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tma_app/core/utils/date_format.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';
|
||||
|
||||
Diary _diary(String id, {List<String> targets = const ['pet-1']}) =>
|
||||
Diary(id: id, name: id, targetIds: targets, ribbon: kRibbonColors.first, tint: kDiaryTints.first);
|
||||
|
||||
DiaryPost _post(String id, String diaryId, DateTime date, {int minute = 0}) => DiaryPost(
|
||||
id: id,
|
||||
diaryId: diaryId,
|
||||
date: date,
|
||||
createdAt: date.add(Duration(minutes: minute)),
|
||||
text: id,
|
||||
mood: Mood.happy,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('relativeDayLabel', () {
|
||||
final today = DateTime(2026, 9, 28);
|
||||
|
||||
test('오늘 · 어제 · 일주일 안은 N일 전', () {
|
||||
expect(relativeDayLabel(today, today), '오늘');
|
||||
expect(relativeDayLabel(DateTime(2026, 9, 27), today), '어제');
|
||||
expect(relativeDayLabel(DateTime(2026, 9, 22), today), '6일 전');
|
||||
});
|
||||
|
||||
test('일주일이 넘으면 날짜로', () {
|
||||
expect(relativeDayLabel(DateTime(2026, 9, 21), today), '2026.09.21');
|
||||
});
|
||||
});
|
||||
|
||||
group('DiaryBook', () {
|
||||
final day1 = DateTime(2026, 9, 27);
|
||||
final day2 = DateTime(2026, 9, 28);
|
||||
|
||||
test('기록은 최근 날짜부터, 같은 날이면 나중에 쓴 것부터', () {
|
||||
final book = DiaryBook(diaries: [_diary('d1')], posts: [
|
||||
_post('old', 'd1', day1),
|
||||
_post('early', 'd1', day2),
|
||||
_post('late', 'd1', day2, minute: 5),
|
||||
]);
|
||||
expect(book.postsOf('d1').map((p) => p.id), ['late', 'early', 'old']);
|
||||
});
|
||||
|
||||
test('하루 한 번 — 그날 쓴 기록이 있는지', () {
|
||||
final book = DiaryBook(posts: [_post('p', 'd1', day2)]);
|
||||
expect(book.wroteOn('d1', day2), isTrue);
|
||||
expect(book.wroteOn('d1', day1), isFalse);
|
||||
expect(book.wroteOn('d2', day2), isFalse);
|
||||
});
|
||||
|
||||
test('다이어리를 지우면 그 기록과 댓글도 지운다', () {
|
||||
final book = DiaryBook(
|
||||
diaries: [_diary('d1'), _diary('d2')],
|
||||
posts: [_post('p1', 'd1', day1), _post('p2', 'd2', day1)],
|
||||
comments: [
|
||||
DiaryComment(id: 'c1', postId: 'p1', text: '!', createdAt: day1),
|
||||
DiaryComment(id: 'c2', postId: 'p2', text: '!', createdAt: day1),
|
||||
],
|
||||
).deleteDiary('d1');
|
||||
expect(book.diaries.map((d) => d.id), ['d2']);
|
||||
expect(book.posts.map((p) => p.id), ['p2']);
|
||||
expect(book.comments.map((c) => c.id), ['c2']);
|
||||
});
|
||||
|
||||
test('기록을 지우면 그 댓글도 지운다', () {
|
||||
final book = DiaryBook(
|
||||
posts: [_post('p1', 'd1', day1)],
|
||||
comments: [DiaryComment(id: 'c1', postId: 'p1', text: '!', createdAt: day1)],
|
||||
).deletePost('p1');
|
||||
expect(book.comments, isEmpty);
|
||||
});
|
||||
|
||||
test('아이를 지우면 다이어리 대상에서만 떼고 기록은 남긴다', () {
|
||||
final book = DiaryBook(
|
||||
diaries: [_diary('d1', targets: ['pet-1', 'self'])],
|
||||
posts: [_post('p1', 'd1', day1)],
|
||||
).dropTarget('pet-1');
|
||||
expect(book.diaries.single.targetIds, ['self']);
|
||||
expect(book.posts, hasLength(1));
|
||||
});
|
||||
|
||||
test('홈 — 모든 다이어리의 최근 기록', () {
|
||||
final book = DiaryBook(posts: [
|
||||
_post('a', 'd1', day1),
|
||||
_post('b', 'd2', day2),
|
||||
_post('c', 'd1', day2, minute: 3),
|
||||
_post('d', 'd2', DateTime(2026, 9, 20)),
|
||||
]);
|
||||
expect(book.latest(3).map((p) => p.id), ['c', 'b', 'a']);
|
||||
});
|
||||
|
||||
test('다이어리는 6권까지', () {
|
||||
final book = DiaryBook(diaries: [for (var i = 0; i < kMaxDiaries; i++) _diary('d$i')]);
|
||||
expect(book.atMax, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
test('다이어리는 끈 알림까지 저장 형식으로 되살린다', () {
|
||||
final d = _diary('d1').copyWith(mutedNotices: {DiaryNotice.reminder}, coverPath: '/c.jpg');
|
||||
final back = Diary.fromJson(d.toJson());
|
||||
expect(back.notifies(DiaryNotice.reminder), isFalse);
|
||||
expect(back.notifies(DiaryNotice.newPost), isTrue);
|
||||
expect(back.coverPath, '/c.jpg');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tma_app/core/utils/date_format.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_compose_page.dart';
|
||||
import 'package:tma_app/features/diary/presentation/pages/diary_page.dart';
|
||||
import 'package:tma_app/features/diary/presentation/pages/diary_settings_page.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
final Diary _diary = Diary(
|
||||
id: 'd1',
|
||||
name: '콩이 성장일기',
|
||||
targetIds: const ['pet-1'],
|
||||
ribbon: kRibbonColors.first,
|
||||
tint: kDiaryTints.first,
|
||||
);
|
||||
|
||||
DiaryPost _post(String text, DateTime date) =>
|
||||
DiaryPost(id: 'p-$text', diaryId: 'd1', date: date, createdAt: date, text: text, mood: Mood.calm);
|
||||
|
||||
Map<String, Object> _prefs({List<DiaryPost> posts = const [], bool withDiary = true}) => {
|
||||
'pets.v1': jsonEncode([samplePet().toJson()]),
|
||||
if (withDiary) 'diary.diaries.v1': jsonEncode([_diary.toJson()]),
|
||||
if (posts.isNotEmpty) 'diary.posts.v1': jsonEncode([for (final p in posts) p.toJson()]),
|
||||
};
|
||||
|
||||
Future<void> _openShelf(WidgetTester tester) async {
|
||||
await tester.tap(find.bySemanticsLabel('일정·기록'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('다이어리'));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> _openDiary(WidgetTester tester) async {
|
||||
await _openShelf(tester);
|
||||
await tester.tap(find.text('콩이 성장일기'));
|
||||
await settle(tester);
|
||||
expect(find.byType(DiaryPage), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> _tapVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.ensureVisible(finder);
|
||||
await tester.pump();
|
||||
await tester.tap(finder);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('다이어리 탭에서 새 다이어리를 만들면 책장에 나타난다', (tester) async {
|
||||
final prefs = await launchSignedIn(tester, extra: _prefs(withDiary: false));
|
||||
await _openShelf(tester);
|
||||
expect(find.text('나의 다이어리'), findsOneWidget);
|
||||
expect(find.text('0/6'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('새 다이어리'));
|
||||
await settle(tester);
|
||||
expect(find.text('다이어리 추가'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('새 다이어리 만들기'));
|
||||
await settle(tester);
|
||||
await tester.enterText(find.byType(TextField).first, '보리 일기');
|
||||
await _tapVisible(tester, find.text('다이어리 만들기'));
|
||||
|
||||
expect(find.text('보리 일기'), findsOneWidget);
|
||||
expect(find.text('기록 0개'), findsOneWidget);
|
||||
expect(find.text('1/6'), findsOneWidget);
|
||||
expect(prefs.getString('diary.diaries.v1'), contains('보리 일기'));
|
||||
});
|
||||
|
||||
testWidgets('오늘 기록을 남기면 피드와 홈에 나오고, 쓰기 버튼은 완료로 바뀐다', (tester) async {
|
||||
await launchSignedIn(tester, extra: _prefs());
|
||||
await _openDiary(tester);
|
||||
expect(find.text('아직 기록이 없어요.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('다이어리 쓰기'));
|
||||
await settle(tester);
|
||||
expect(find.byType(DiaryComposePage), findsOneWidget);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('신남'));
|
||||
await tester.enterText(find.byType(TextField), '산책하고 낮잠 잤어요');
|
||||
await _tapVisible(tester, find.text('기록 남기기'));
|
||||
|
||||
expect(find.byType(DiaryComposePage), findsNothing);
|
||||
expect(find.text('산책하고 낮잠 잤어요'), findsOneWidget);
|
||||
expect(find.text('기록 1개'), findsOneWidget);
|
||||
|
||||
// 하루 한 번 — 다시 누르면 안내만
|
||||
await tester.tap(find.bySemanticsLabel('오늘 작성 완료'));
|
||||
await settle(tester);
|
||||
expect(find.text('오늘 기록은 이미 남겼어요. 내일 다시 작성할 수 있어요'), findsOneWidget);
|
||||
expect(find.byType(DiaryComposePage), findsNothing);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('뒤로'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.bySemanticsLabel('홈'));
|
||||
await settle(tester);
|
||||
await tester.scrollUntilVisible(find.text('산책하고 낮잠 잤어요'), 300, scrollable: find.byType(Scrollable).first);
|
||||
expect(find.text('콩이 · 오늘'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('기록에 댓글을 달고, 기록을 지울 수 있다', (tester) async {
|
||||
await launchSignedIn(tester, extra: _prefs(posts: [_post('어제 기록', DateTime.now().subtract(const Duration(days: 1)))]));
|
||||
await _openDiary(tester);
|
||||
expect(find.text('어제'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('댓글'));
|
||||
await settle(tester);
|
||||
expect(find.text('첫 댓글을 남겨보세요'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField).last, '귀여워요');
|
||||
await tester.tap(find.bySemanticsLabel('등록'));
|
||||
await settle(tester);
|
||||
expect(find.text('귀여워요'), findsOneWidget);
|
||||
expect(find.text('댓글 1'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('기록 관리'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('삭제하기'));
|
||||
await settle(tester);
|
||||
expect(find.text('이 기록을 삭제할까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('삭제하기').last);
|
||||
await settle(tester);
|
||||
expect(find.text('어제 기록'), findsNothing);
|
||||
expect(find.text('아직 기록이 없어요.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('달력 보기에서 날짜를 고르면 그날 기록을 보여 준다', (tester) async {
|
||||
final today = dateOnly(DateTime.now());
|
||||
await launchSignedIn(tester, extra: _prefs(posts: [_post('오늘 기록', today)]));
|
||||
await _openDiary(tester);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('보기 전환'));
|
||||
await settle(tester);
|
||||
expect(find.text('${today.year}년 ${today.month}월'), findsOneWidget);
|
||||
expect(find.text('${formatMonthDay(today)} · 기록 1개'), findsOneWidget);
|
||||
expect(find.text('오늘 기록'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('다이어리 설정에서 다이어리를 제거하면 책장으로 돌아간다', (tester) async {
|
||||
final prefs = await launchSignedIn(tester, extra: _prefs(posts: [_post('기록', DateTime.now())]));
|
||||
await _openDiary(tester);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('다이어리 설정'));
|
||||
await settle(tester);
|
||||
expect(find.byType(DiarySettingsPage), findsOneWidget);
|
||||
expect(find.text('알림 설정'), findsOneWidget);
|
||||
|
||||
await _tapVisible(tester, find.text('다이어리 제거'));
|
||||
expect(find.text('다이어리를 제거할까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('제거하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byType(DiarySettingsPage), findsNothing);
|
||||
expect(find.byType(DiaryPage), findsNothing);
|
||||
expect(find.text('콩이 성장일기'), findsNothing);
|
||||
expect(find.text('0/6'), findsOneWidget);
|
||||
expect(prefs.getString('diary.posts.v1'), '[]');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user