feat(flutter): 다이어리를 서버에 저장 (ApiDiaryRepository)

다이어리 저장소도 "장부 전체 저장" 에서 건별 계약(다이어리·기록·댓글 만들기·고치기·지우기)으로 바꿨다.

- 서버 저장소: 다이어리·기록·댓글 목록을 합쳐 DiaryBook 으로 만들고, 표지·기록 사진은 저장할 때 올린다
- 기록·댓글의 작성자가 로그인한 나이면 앱의 "나"로 담는다 — 하루 한 번 쓰기 판단이 그대로 맞는다.
  로그인할 때 회원 id 를 기기에 둔다
- 책장이 가득 찬 경우(DIARY_001)는 null 로 받아 화면이 직접 안내하고 전역 알림은 끈다
- 아이 삭제 때의 다이어리 정리는 서버가 하므로 dropTarget 은 아무것도 보내지 않는다
- 다이어리 화면은 실패하면 쓴 기록·댓글을 그대로 둔다 (알림은 인터셉터)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:28:20 +09:00
parent 60efc65ee0
commit a09086daf7
10 changed files with 546 additions and 45 deletions
+141
View File
@@ -0,0 +1,141 @@
import 'dart:ui' show Color;
import 'package:flutter_test/flutter_test.dart';
import 'package:tma_app/core/api/file_uploader.dart';
import 'package:tma_app/features/diary/data/api_diary_repository.dart';
import 'package:tma_app/features/diary/domain/diary.dart';
import 'package:tma_app/features/diary/domain/mood.dart';
import 'support/fake_api.dart';
Map<String, Object?> _diaryJson({String id = 'd1', String? coverFileId}) => {
'id': id,
'name': '콩이의 하루',
'targetIds': ['p1'],
'ribbon': '#FF7500',
'tint': '#FF8A5B',
'coverFileId': coverFileId,
'mutedNotices': ['reminder'],
'createdAt': '2026-10-11T00:00:00.000Z',
};
Map<String, Object?> _postJson({String id = 'post-1', String authorId = 'me-uuid', String date = '2026-10-11'}) => {
'id': id,
'diaryId': 'd1',
'authorId': authorId,
'date': date,
'createdAt': '2026-10-11T03:00:00.000Z',
'text': '산책했어요',
'mood': 'happy',
'photoFileId': 'f1',
};
void main() {
late FakeApi api;
late ApiDiaryRepository repo;
String? myId = 'me-uuid';
setUp(() {
api = FakeApi();
myId = 'me-uuid';
repo = ApiDiaryRepository(
dio: api.dio,
session: api.session,
uploader: FileUploader(api.dio, api.reported.add),
myUserId: () => myId,
);
});
test('다이어리·기록·댓글을 불러와 하나의 장부로 합친다 — 내가 쓴 기록은 "나"로 담는다', () async {
api.on('GET', '/diaries', (r) => ok(page([_diaryJson(coverFileId: 'cover-1')])));
api.on('GET', '/diary-posts', (r) => ok(page([_postJson(), _postJson(id: 'post-2', authorId: 'friend-uuid')])));
api.on('GET', '/diary-comments', (r) => ok(page([
{'id': 'c1', 'postId': 'post-1', 'authorId': 'me-uuid', 'text': '귀여워', 'createdAt': '2026-10-11T04:00:00.000Z'},
])));
final book = await repo.fetch();
final diary = book.diaries.single;
expect(diary.coverPath, 'server-file:cover-1');
expect(diary.mutedNotices, {DiaryNotice.reminder});
expect(diary.ribbon, const Color(0xFFFF7500));
final mine = book.posts.firstWhere((p) => p.id == 'post-1');
expect(mine.authorId, kMeAuthorId);
expect(mine.mood, Mood.happy);
expect(mine.photoPath, 'server-file:f1');
expect(book.posts.firstWhere((p) => p.id == 'post-2').authorId, 'friend-uuid');
expect(book.comments.single.authorId, kMeAuthorId);
// 하루 한 번 쓰기 — 내가 오늘 쓴 기록을 알아본다
expect(book.wroteOn('d1', DateTime(2026, 10, 11)), isTrue);
});
test('책장이 가득 차면(DIARY_001) null 로 받고 전역 알림은 띄우지 않는다', () async {
api.on('POST', '/diaries', (r) => failure('DIARY_001', 409));
final created = await repo.createDiary(
name: '새 다이어리',
targetIds: const [],
ribbon: kRibbonColors.first,
tint: kDiaryTints.first,
);
expect(created, isNull);
expect(api.reported, isEmpty);
});
test('기록 남기기는 기기의 오늘 날짜와 기분·사진을 보낸다', () async {
api.on('POST', '/diaries/:id/posts', (r) {
expect(r.params['id'], 'd1');
expect(r.json, {'date': '2026-10-11', 'text': '산책했어요', 'mood': 'happy', 'photoFileId': 'f1'});
return ok(_postJson(), status: 201);
});
final post = await repo.addPost(
diaryId: 'd1',
date: DateTime(2026, 10, 11),
text: '산책했어요',
mood: Mood.happy,
photoPath: 'server-file:f1',
);
expect(post.authorId, kMeAuthorId);
expect(api.sent('POST', '/files'), isEmpty);
});
test('오늘 이미 기록했으면(DIARY_002) 서버 문구 대신 사전 문구로 알린다', () async {
api.on('POST', '/diaries/:id/posts', (r) => failure('DIARY_002', 409));
await expectLater(
repo.addPost(diaryId: 'd1', date: DateTime(2026, 10, 11), text: '두 번째'),
throwsA(anything),
);
expect(api.reported.single.code, 'DIARY_002');
expect(api.reported.single.message, isNot('서버 문구'));
});
test('다이어리 수정은 바탕색을 보내지 않는다 (만들 때 정한 값을 유지한다)', () async {
api.on('PATCH', '/diaries/:id', (r) {
expect(r.json!.containsKey('tint'), isFalse);
expect(r.json!['mutedNotices'], ['newComment']);
return ok(_diaryJson());
});
final diary = Diary(
id: 'd1',
name: '콩이의 하루',
targetIds: const ['p1'],
ribbon: kRibbonColors.first,
tint: kDiaryTints.first,
mutedNotices: const {DiaryNotice.newComment},
);
await repo.updateDiary(diary);
expect(api.sent('PATCH', '/diaries/d1'), hasLength(1));
});
test('아이를 지울 때 다이어리 정리는 서버가 이미 했다', () async {
await repo.dropTarget('p1');
expect(api.requests, isEmpty);
});
}