generated from ttipo/project-base
feat(flutter): 펫 명함을 서버에 저장 (ApiPetCardRepository)
- 명함 목록·만들기·꾸미기·삭제·대표 명함을 /pet-cards, /pets/:id/representative-card 로 옮겼다 - 명함과 대표 지정을 한 번에 불러오도록 저장소가 PetCardBook 을 돌려준다 - 배경은 앱 그림(assets/…)이면 경로만, 기기 사진이면 올린 뒤 파일 id 로 보낸다 - 공유 화면은 서버가 발급한 명함 코드를 보여 준다 (기기 저장 모드는 지금처럼 명함 id 로 만든다) - 명함을 지울 때 대표 지정 정리, 아이를 지울 때 명함 정리는 저장소가 맡는다 — 서버 모드는 서버가 이미 했다 - 명함 화면은 실패하면 꾸민 모양을 그대로 둔다 (알림은 인터셉터) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import 'dart:io';
|
||||
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/pet_card/data/api_pet_card_repository.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_style.dart';
|
||||
|
||||
import 'api_account_test.dart' show kPngBytes;
|
||||
import 'support/fake_api.dart';
|
||||
|
||||
Map<String, Object?> _cardJson({
|
||||
String id = 'c1',
|
||||
String petId = 'p1',
|
||||
bool isRepresentative = false,
|
||||
String? nameBgAsset,
|
||||
String? nameBgFileId,
|
||||
}) =>
|
||||
{
|
||||
'id': id,
|
||||
'petId': petId,
|
||||
'code': '0428-117',
|
||||
'isRepresentative': isRepresentative,
|
||||
'style': {
|
||||
'tint': '#5AA0E0',
|
||||
'tint2': '#9CCBF0',
|
||||
'nameBg': null,
|
||||
'nameBgAsset': nameBgAsset,
|
||||
'nameBgFileId': nameBgFileId,
|
||||
'textLight': true,
|
||||
'scrim': true,
|
||||
'scrimTone': 'dark',
|
||||
},
|
||||
};
|
||||
|
||||
void main() {
|
||||
late FakeApi api;
|
||||
late ApiPetCardRepository repo;
|
||||
|
||||
setUp(() {
|
||||
api = FakeApi();
|
||||
repo = ApiPetCardRepository(dio: api.dio, session: api.session, uploader: FileUploader(api.dio, api.reported.add));
|
||||
});
|
||||
|
||||
test('명함 목록에서 대표 명함을 아이별로 모으고, 서버가 발급한 코드를 담는다', () async {
|
||||
api.on('GET', '/pet-cards', (r) => ok(page([
|
||||
_cardJson(nameBgAsset: 'assets/svg/card_bg/bg_1.svg'),
|
||||
_cardJson(id: 'c2', isRepresentative: true, nameBgFileId: 'f1'),
|
||||
_cardJson(id: 'c3', petId: 'p2'),
|
||||
])));
|
||||
|
||||
final book = await repo.fetch();
|
||||
|
||||
expect(book.cards.map((c) => c.id), ['c1', 'c2', 'c3']);
|
||||
expect(book.representatives, {'p1': 'c2'});
|
||||
expect(book.cards.first.code, '0428-117');
|
||||
expect(book.cards.first.style.nameBgImage, 'assets/svg/card_bg/bg_1.svg');
|
||||
expect(book.cards[1].style.nameBgImage, 'server-file:f1');
|
||||
expect(book.cards.first.style.scrimTone, ScrimTone.dark);
|
||||
});
|
||||
|
||||
test('앱 그림 배경은 경로만 보내고 올리지 않는다', () async {
|
||||
api.on('PUT', '/pet-cards/:id', (r) {
|
||||
final style = r.json!['style'] as Map<String, dynamic>;
|
||||
expect(style['nameBgAsset'], 'assets/svg/card_bg/bg_2.svg');
|
||||
expect(style['nameBgFileId'], isNull);
|
||||
return ok(_cardJson(nameBgAsset: 'assets/svg/card_bg/bg_2.svg'));
|
||||
});
|
||||
|
||||
await repo.update('c1', CardStyle.initial().withBackgroundImage('assets/svg/card_bg/bg_2.svg'));
|
||||
|
||||
expect(api.sent('POST', '/files'), isEmpty);
|
||||
});
|
||||
|
||||
test('기기 사진 배경은 올린 뒤 파일 id 로 보낸다', () async {
|
||||
final dir = Directory.systemTemp.createTempSync('rup-card');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final photo = File('${dir.path}/bg.png')..writeAsBytesSync(kPngBytes);
|
||||
api.on('POST', '/files', (r) => ok({'id': 'f7'}, status: 201));
|
||||
api.on('POST', '/pet-cards', (r) {
|
||||
expect(r.json!['petId'], 'p1');
|
||||
final style = r.json!['style'] as Map<String, dynamic>;
|
||||
expect(style['nameBgFileId'], 'f7');
|
||||
expect(style['nameBgAsset'], isNull);
|
||||
expect(style['tint'], '#FF9B6A');
|
||||
return ok(_cardJson(nameBgFileId: 'f7'), status: 201);
|
||||
});
|
||||
|
||||
final card = await repo.create('p1', CardStyle.forPet(const Color(0xFFFF9B6A)).withBackgroundImage(photo.path));
|
||||
|
||||
expect(card.style.nameBgImage, 'server-file:f7');
|
||||
});
|
||||
|
||||
test('대표 명함은 아이의 하위 경로로 정하고 푼다', () async {
|
||||
api.on('PUT', '/pets/:petId/representative-card', (r) {
|
||||
expect(r.json, {'cardId': 'c1'});
|
||||
return noContent;
|
||||
});
|
||||
api.on('DELETE', '/pets/:petId/representative-card', (r) => noContent);
|
||||
|
||||
await repo.setRepresentative('p1', 'c1');
|
||||
await repo.clearRepresentative('p1');
|
||||
|
||||
expect(api.sent('PUT', '/pets/p1/representative-card'), hasLength(1));
|
||||
expect(api.sent('DELETE', '/pets/p1/representative-card'), hasLength(1));
|
||||
});
|
||||
|
||||
test('아이를 지울 때 명함 정리는 서버가 이미 했다', () async {
|
||||
await repo.dropPet('p1');
|
||||
|
||||
expect(api.requests, isEmpty);
|
||||
});
|
||||
}
|
||||
@@ -113,21 +113,50 @@ void main() {
|
||||
await repo.create('pet-1', CardStyle.initial());
|
||||
await repo.update(a.id, CardStyle.initial().withTextLight(true));
|
||||
|
||||
var cards = await repo.fetchCards();
|
||||
var cards = (await repo.fetch()).cards;
|
||||
expect(cards, hasLength(2));
|
||||
expect(cards.first.style.textLight, isTrue);
|
||||
|
||||
await repo.delete(a.id);
|
||||
cards = await repo.fetchCards();
|
||||
cards = (await repo.fetch()).cards;
|
||||
expect(cards.map((c) => c.id), isNot(contains(a.id)));
|
||||
});
|
||||
|
||||
test('대표 명함을 아이별로 기억하고 지울 수 있다', () async {
|
||||
final a = await repo.create('pet-1', CardStyle.initial());
|
||||
await repo.setRepresentative('pet-1', a.id);
|
||||
expect(await repo.fetchRepresentatives(), {'pet-1': a.id});
|
||||
expect((await repo.fetch()).representatives, {'pet-1': a.id});
|
||||
await repo.clearRepresentative('pet-1');
|
||||
expect(await repo.fetchRepresentatives(), isEmpty);
|
||||
expect((await repo.fetch()).representatives, isEmpty);
|
||||
});
|
||||
|
||||
test('명함을 지우면 그 명함을 가리키던 대표 지정도 사라진다', () async {
|
||||
final a = await repo.create('pet-1', CardStyle.initial());
|
||||
await repo.setRepresentative('pet-1', a.id);
|
||||
|
||||
await repo.delete(a.id);
|
||||
|
||||
expect((await repo.fetch()).representatives, isEmpty);
|
||||
});
|
||||
|
||||
test('아이를 지우면 그 아이의 명함과 대표 지정을 모두 지운다', () async {
|
||||
final a = await repo.create('pet-1', CardStyle.initial());
|
||||
await repo.create('pet-2', CardStyle.initial());
|
||||
await repo.setRepresentative('pet-1', a.id);
|
||||
|
||||
await repo.dropPet('pet-1');
|
||||
|
||||
final book = await repo.fetch();
|
||||
expect(book.cards.map((c) => c.petId), ['pet-2']);
|
||||
expect(book.representatives, isEmpty);
|
||||
});
|
||||
|
||||
test('명함 코드는 서버가 발급한 값, 없으면 명함 id 로 만든다', () {
|
||||
final local = PetCard(id: 'c1', petId: 'p1', style: CardStyle.initial());
|
||||
const fromServer = PetCard(id: 'c1', petId: 'p1', style: CardStyle(tint: Color(0xFFFF9B6A), tint2: Color(0xFFFF9B6A)), code: '0428-117');
|
||||
|
||||
expect(displayCardCode(local), petCardCode('c1'));
|
||||
expect(displayCardCode(fromServer), '0428-117');
|
||||
});
|
||||
|
||||
test('명함 JSON 을 그대로 되살릴 수 있다', () {
|
||||
|
||||
Reference in New Issue
Block a user