generated from ttipo/project-base
feat(flutter): 공유 다이어리 — 초대·참여·멤버와 함께 쓰기
다이어리를 가족·지인과 함께 쓰는 화면을 연다. - 다이어리 설정: 개인↔공유 전환(멤버가 있으면 확인), 멤버 목록·내보내기, 초대 QR·코드 복사, 다이어리 나가기 - 참여: 코드 입력 시트 · 초대 QR 스캐너 · 참여 확인 (명함 스캐너를 공용 QR 스캐너로 바꿔 함께 쓴다) - 만들 때 공유 다이어리를 고를 수 있다 - 다른 멤버의 기록·댓글은 그 사람 이름·사진으로, 수정·삭제는 내 것만 - 참여한 다이어리 표지는 서버가 준 주인의 아이들로 그린다 - 다른 멤버의 새 글·내 기록의 댓글을 알림으로 보여 주고, 공유 다이어리에 들어오면 다시 읽는다 - 알림 설정은 멤버마다 따로 저장한다 - 기기 저장 모드에서는 공유 다이어리를 쓸 수 없다고 안내한다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import 'package:tma_app/features/auth/data/repositories/device_key_store.dart';
|
||||
import 'package:tma_app/features/auth/domain/social_provider.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/diary_invitation.dart';
|
||||
import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
import 'package:tma_app/features/mypage/data/profile_repository.dart';
|
||||
import 'package:tma_app/features/pet/data/repositories/api_pet_repository.dart';
|
||||
@@ -192,7 +193,7 @@ void main() {
|
||||
final comment = await diaries.addComment(post.id, '귀여워');
|
||||
await diaries.updatePostText(post, '산책 다녀왔어요');
|
||||
await diaries.updateComment(comment, '정말 귀여워');
|
||||
await diaries.updateDiary(diary.copyWith(mutedNotices: {DiaryNotice.reminder}));
|
||||
await diaries.setMutedNotices(diary.id, {DiaryNotice.reminder});
|
||||
|
||||
final diaryBook = await diaries.fetch();
|
||||
expect(diaryBook.wroteOn(diary.id, day), isTrue);
|
||||
@@ -233,6 +234,104 @@ void main() {
|
||||
expect(reported, isEmpty, reason: '정상 흐름에서는 실패 알림이 없어야 한다');
|
||||
});
|
||||
|
||||
test('공유 다이어리 — 초대 코드로 함께 쓰고, 멤버의 글·사진이 서로 보이며, 내보내면 코드가 바뀐다', () async {
|
||||
// 두 번째 회원 — 회원 id 는 prefs 한 자리에 남으므로 각자 붙잡아 둔다
|
||||
final ownerId = prefs.getString(kAuthUserIdKey)!;
|
||||
final memberSession = SessionStore(MemoryTokenVault());
|
||||
final memberDio = DioClient(baseUrl: baseUrl, session: memberSession, onError: reported.add).dio;
|
||||
final memberUploader = FileUploader(memberDio, reported.add);
|
||||
final memberAuth = ApiAuthRepository(
|
||||
dio: memberDio,
|
||||
session: memberSession,
|
||||
prefs: prefs,
|
||||
deviceKeys: FixedDeviceKeyStore('live-member-${DateTime.now().microsecondsSinceEpoch}'),
|
||||
);
|
||||
await memberAuth.signIn(SocialProvider.kakao);
|
||||
final memberId = prefs.getString(kAuthUserIdKey)!;
|
||||
await prefs.setString(kAuthUserIdKey, ownerId);
|
||||
addTearDown(() async {
|
||||
if (memberSession.hasSession) await memberAuth.deleteAccount();
|
||||
await prefs.setString(kAuthUserIdKey, ownerId);
|
||||
});
|
||||
|
||||
final owner = ApiDiaryRepository(dio: dio, session: session, uploader: uploader, myUserId: () => ownerId);
|
||||
final member = ApiDiaryRepository(
|
||||
dio: memberDio,
|
||||
session: memberSession,
|
||||
uploader: memberUploader,
|
||||
myUserId: () => memberId,
|
||||
);
|
||||
final today = DateTime.now();
|
||||
final day = DateTime(today.year, today.month, today.day);
|
||||
|
||||
// 주인 — 공유 다이어리를 만들면 초대 코드가 생긴다
|
||||
final shared = await owner.createDiary(
|
||||
name: '함께 쓰는 하루',
|
||||
targetIds: const ['self'],
|
||||
ribbon: kRibbonColors[3],
|
||||
tint: kDiaryTints[1],
|
||||
coverPath: photoPath,
|
||||
shared: true,
|
||||
);
|
||||
expect(shared!.shared, isTrue);
|
||||
expect(shared.isOwner, isTrue);
|
||||
final code = shared.inviteCode!;
|
||||
expect(parseInviteCode(code), code);
|
||||
|
||||
// 멤버 — 미리 보고 참여한다
|
||||
final invitation = await member.previewInvitation(code);
|
||||
expect(invitation.name, '함께 쓰는 하루');
|
||||
expect(invitation.isMember, isFalse);
|
||||
var memberBook = await member.join(code);
|
||||
final joined = memberBook.diaries.single;
|
||||
expect(joined.isOwner, isFalse);
|
||||
expect(joined.inviteCode, isNull);
|
||||
expect(joined.owner!.name, '라이브 집사');
|
||||
expect(joined.members.where((m) => m.isMe), hasLength(1));
|
||||
expect(joined.targets.single.isSelf, isTrue);
|
||||
|
||||
// 멤버가 사진과 함께 기록하고, 주인이 댓글을 단다
|
||||
final memberPost = await member.addPost(diaryId: joined.id, date: day, text: '같이 산책', photoPath: photoPath);
|
||||
await owner.addComment(memberPost.id, '고마워요');
|
||||
|
||||
final ownerBook = await owner.fetch();
|
||||
final seen = ownerBook.posts.firstWhere((p) => p.id == memberPost.id);
|
||||
expect(seen.isMine, isFalse);
|
||||
expect(seen.author!.id, memberId);
|
||||
expect(ownerBook.diaries.firstWhere((d) => d.id == shared.id).memberCount, 2);
|
||||
|
||||
// 서로의 사진을 받을 수 있다 — 주인은 멤버의 기록 사진, 멤버는 주인의 표지
|
||||
final postPhoto = await dio.get<List<int>>(
|
||||
'$kFilesPath/${serverFileIdOf(seen.photoPath)}',
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
expect(postPhoto.data, _png);
|
||||
final cover = await memberDio.get<List<int>>(
|
||||
'$kFilesPath/${serverFileIdOf(joined.coverPath)}',
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
expect(cover.data, _png);
|
||||
|
||||
// 멤버에게는 주인의 댓글이 남의 댓글로 온다
|
||||
memberBook = await member.fetch();
|
||||
expect(memberBook.comments.single.isMine, isFalse);
|
||||
expect(memberBook.comments.single.author!.name, '라이브 집사');
|
||||
|
||||
// 내보내면 멤버의 책장에서 사라지고, 예전 코드로는 다시 들어올 수 없다
|
||||
await owner.removeMember(shared.id, memberId);
|
||||
expect((await member.fetch()).diaries, isEmpty);
|
||||
await expectLater(
|
||||
member.previewInvitation(code),
|
||||
throwsA(isA<DioException>().having((e) => (e.error! as ApiException).code, 'code', 'DIARY_003')),
|
||||
);
|
||||
final rotated = (await owner.fetch()).diaries.firstWhere((d) => d.id == shared.id);
|
||||
expect(rotated.inviteCode, isNot(code));
|
||||
expect(rotated.memberCount, 1);
|
||||
|
||||
reported.removeWhere((e) => e.code == 'DIARY_003');
|
||||
expect(reported, isEmpty, reason: '정상 흐름에서는 실패 알림이 없어야 한다');
|
||||
});
|
||||
|
||||
test('규칙 위반은 사전 문구의 에러 코드로 돌아온다', () async {
|
||||
final diaries = ApiDiaryRepository(dio: dio, session: session, uploader: uploader, myUserId: () => null);
|
||||
final diary = (await diaries.fetch()).diaries.first;
|
||||
|
||||
Reference in New Issue
Block a user