feat(flutter): 펫 명함 이미지를 앨범에 저장

공유 시트의 '이미지로 저장'과 QR 창의 'QR 이미지 저장'이 시안처럼 휴대폰 앨범에 저장한다.
지금까지는 공유 창을 띄웠다. 웹은 앨범이 없으므로 공유 창을 그대로 쓴다.

- 저장은 AlbumSaver 뒤에 두고(gal), 테스트에서는 가짜 저장기로 바꿔 끼운다.
- 사진 접근을 거절해 저장하지 못하면 권한을 확인하라고 알린다.
- QR 창은 배경 흐림·테두리를 시안대로, 저장하면 QR 창과 공유 시트를 함께 닫는다.
- 코드 복사 안내 문구를 시안에 맞췄다.

권한: Android 는 10 미만에서만 저장 권한을 요청하고(maxSdkVersion 29), iOS 는 사진 추가 안내 문구를 넣었다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 01:24:24 +09:00
parent 4173c64af8
commit e6f5cd0720
10 changed files with 209 additions and 56 deletions
+76
View File
@@ -0,0 +1,76 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:tma_app/core/storage/album_saver.dart';
import 'package:tma_app/features/pet_card/domain/card_style.dart';
import 'package:tma_app/features/pet_card/domain/pet_card.dart';
import 'support/app_harness.dart';
/// 앨범 대신 받은 이름만 모아 두는 저장기
class _FakeAlbum implements AlbumSaver {
_FakeAlbum({this.ok = true});
final bool ok;
final saved = <String>[];
@override
Future<bool> savePng(Uint8List bytes, String name) async {
saved.add(name);
return ok;
}
}
Map<String, Object> _prefs() => {
'pets.v1': jsonEncode([samplePet().toJson()]),
'petCards.v1': jsonEncode([PetCard(id: 'c1', petId: 'pet-1', style: CardStyle.initial()).toJson()]),
};
Future<_FakeAlbum> _openShare(WidgetTester tester, {bool ok = true}) async {
final album = _FakeAlbum(ok: ok);
await launchSignedIn(tester, extra: _prefs(), overrides: [albumSaverProvider.overrideWithValue(album)]);
await tester.tap(find.bySemanticsLabel('콩이 명함 공유'));
await settle(tester);
expect(find.text('콩이 펫 명함 공유'), findsOneWidget);
return album;
}
/// 명함 이미지를 떠 내는 일은 실제 비동기라 runAsync 안에서 누른다
Future<void> _tapAndWait(WidgetTester tester, Finder finder) async {
await tester.runAsync(() async {
await tester.tap(finder);
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await settle(tester);
}
void main() {
testWidgets('이미지로 저장하면 앨범에 저장하고 시트를 닫은 뒤 알린다', (tester) async {
final album = await _openShare(tester);
await _tapAndWait(tester, find.bySemanticsLabel('이미지로 저장'));
expect(album.saved, ['rup_콩이_명함']);
expect(find.text('콩이 펫 명함 공유'), findsNothing);
expect(find.text('이미지로 저장했어요'), findsOneWidget);
});
testWidgets('QR 이미지 저장은 QR 창과 공유 시트를 함께 닫는다', (tester) async {
final album = await _openShare(tester);
await tester.tap(find.bySemanticsLabel('QR 코드 공유'));
await settle(tester);
expect(find.text('펫 명함 QR'), findsOneWidget);
await _tapAndWait(tester, find.text('QR 이미지 저장'));
expect(album.saved, ['rup_콩이_QR']);
expect(find.text('펫 명함 QR'), findsNothing);
expect(find.text('콩이 펫 명함 공유'), findsNothing);
expect(find.text('QR 코드를 저장했어요'), findsOneWidget);
});
testWidgets('사진 접근을 거절해 저장하지 못하면 알려 준다', (tester) async {
await _openShare(tester, ok: false);
await _tapAndWait(tester, find.bySemanticsLabel('이미지로 저장'));
expect(find.text('저장하지 못했어요. 사진 접근 권한을 확인해 주세요'), findsOneWidget);
});
}