fix(flutter): 홈·일정 시안 점검 반영과 우리 아이 순서 바꾸기

홈
- 우리 아이를 꾹 눌러(320ms) 끌면 순서가 바뀌고 저장된다 (PetRepository.reorder).
  보관함으로 간 아이는 목록에서 빠지므로 원래 순서대로 뒤에 둔다.
- 오늘 할 일 진행 막대 끝을 둥글게, 아이 사진 겹침의 글자 아래 여백(inline-flex)을 반영하고
  긴 일정 제목은 줄을 바꾼다.
- 최근 다이어리 칸을 누르면 시안처럼 일정·기록 탭의 다이어리 목록으로 간다.

일정
- 하루 종일이 켜진 채 기간을 하루 2회로 정하면 회차 시각(08:00·20:00)이 저장되지 않던 문제를 고쳤다.
- 예약 분류를 고른 뒤 같은 일반 일정 칩을 다시 누르면 분류 선택이 풀린다.
- 주기 직접 설정은 2주마다로 시작하고, 매일·평일을 골랐다 돌아와도 단위·횟수가 남는다.
- 12개월 보기에서는 일정 목록을 숨기고, 달력 점 묶음을 칸 가운데에 둔다.
- 저장 버튼 흐림 .45, 일정 제거는 전용 확인창으로.

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 d9a4eba9a3
commit 81af93a813
16 changed files with 454 additions and 143 deletions
+32 -1
View File
@@ -66,5 +66,36 @@ void main() {
expect(find.text('명함 만들기'), findsNothing);
expect(find.bySemanticsLabel('콩이 명함 공유'), findsOneWidget);
});
}
testWidgets('우리 아이를 꾹 눌러 끌면 순서가 바뀌고 저장된다', (tester) async {
final pets = [samplePet(id: 'pet-1', name: '콩이'), samplePet(id: 'pet-2', name: '보리')];
final prefs = await launchSignedIn(tester, extra: {'pets.v1': jsonEncode([for (final p in pets) p.toJson()])});
final from = tester.getCenter(find.text('콩이'));
final to = tester.getCenter(find.text('보리'));
// 320ms 이상 누르면 들어 올리고, 끌어서 보리 자리에 놓는다
final gesture = await tester.startGesture(from);
await tester.pump(const Duration(milliseconds: 400));
await gesture.moveTo(Offset.lerp(from, to, .5)!);
await tester.pump();
await gesture.moveTo(to);
await tester.pump();
await gesture.up();
await settle(tester);
expect(tester.getCenter(find.text('보리')).dx, lessThan(tester.getCenter(find.text('콩이')).dx));
final saved = (jsonDecode(prefs.getString('pets.v1')!) as List<Object?>).cast<Map<String, Object?>>();
expect(saved.map((p) => p['name']), ['보리', '콩이']);
});
testWidgets('짧게 누르면 순서는 그대로 두고 그 아이를 고른다', (tester) async {
final pets = [samplePet(id: 'pet-1', name: '콩이'), samplePet(id: 'pet-2', name: '보리')];
final prefs = await launchSignedIn(tester, extra: {'pets.v1': jsonEncode([for (final p in pets) p.toJson()])});
await tester.tap(find.text('보리'));
await settle(tester);
expect(find.text('보리의 펫 명함이 아직 없어요', findRichText: true), findsOneWidget);
final saved = (jsonDecode(prefs.getString('pets.v1')!) as List<Object?>).cast<Map<String, Object?>>();
expect(saved.map((p) => p['name']), ['콩이', '보리']);
});
}
+18
View File
@@ -7,6 +7,8 @@ import 'package:tma_app/features/pet/domain/pet.dart';
import 'package:tma_app/features/pet/domain/pet_age.dart';
import 'package:tma_app/features/pet/domain/pet_species.dart';
import 'support/app_harness.dart';
PetInput _input({String name = '콩이', double weight = 3.2}) => PetInput(
name: name,
species: '강아지',
@@ -138,4 +140,20 @@ void main() {
expect(await repo.fetchPets(), isEmpty);
});
});
group('reorderPets — 우리 아이 순서 바꾸기', () {
final pets = [samplePet(id: 'a', name: '콩이'), samplePet(id: 'b', name: '보리'), samplePet(id: 'c', name: '초코')];
test('준 순서대로 줄 세운다', () {
expect(reorderPets(pets, ['c', 'a', 'b']).map((p) => p.id), ['c', 'a', 'b']);
});
test('목록에 없는 아이(추억 보관함 등)는 원래 순서대로 뒤에 둔다', () {
expect(reorderPets(pets, ['b', 'a']).map((p) => p.id), ['b', 'a', 'c']);
});
test('없는 id 는 건너뛴다', () {
expect(reorderPets(pets, ['x', 'b']).map((p) => p.id), ['b', 'a', 'c']);
});
});
}
+73
View File
@@ -138,4 +138,77 @@ void main() {
expect(find.text('예방접종'), findsOneWidget);
expect(find.text('예약된 일정이 없어요'), findsNothing);
});
testWidgets('하루 종일이 켜진 채 기간을 하루 2회로 정하면 회차 시각(08:00·20:00)으로 저장된다', (tester) async {
final prefs = await launchSignedIn(tester, extra: _withPet());
await _openRecords(tester);
await tester.tap(find.text('일정 추가'));
await settle(tester);
await tester.enterText(find.byType(TextField).first, '영양제');
await _tapVisible(tester, find.bySemanticsLabel('기간 설정'));
await _tapVisible(tester, find.text('2회'));
await _tapVisible(tester, find.text('일정 저장'));
final saved = jsonDecode(prefs.getString('schedule.events.v1')!) as List<Object?>;
final starts = {for (final e in saved.cast<Map<String, Object?>>()) e['start']};
expect(starts, {'08:00', '20:00'});
});
testWidgets('예약 분류를 골랐다가 같은 일반 일정 칩을 다시 누르면 분류 선택이 풀린다', (tester) async {
await launchSignedIn(tester, extra: _withPet());
await _openRecords(tester);
await tester.tap(find.text('일정 추가'));
await settle(tester);
await _tapVisible(tester, find.text('목욕'));
await _tapVisible(tester, find.bySemanticsLabel('병원·검진'));
expect(tester.getSemantics(find.bySemanticsLabel('병원·검진')), isSemantics(isSelected: true));
// 이름 칸에도 '목욕' 이 들어 있으므로 위쪽의 칩을 누른다
await _tapVisible(tester, find.text('목욕').first);
expect(tester.getSemantics(find.bySemanticsLabel('병원·검진')), isSemantics(isSelected: false));
});
testWidgets('주기 직접 설정은 2주마다로 시작하고, 매일·평일을 골랐다 돌아와도 단위·횟수가 남는다', (tester) async {
await launchSignedIn(tester, extra: _withPet());
await _openRecords(tester);
await tester.tap(find.text('일정 추가'));
await settle(tester);
await _tapVisible(tester, find.bySemanticsLabel('반복 일정'));
await _tapVisible(tester, find.text('주기 직접 설정'));
expect(find.text('2주 마다 반복'), findsWidgets);
await _tapVisible(tester, find.text('평일만'));
expect(find.text('평일마다 반복'), findsOneWidget);
await _tapVisible(tester, find.text('주기 직접 설정'));
expect(find.text('2주 마다 반복'), findsWidgets);
});
testWidgets('시간 휠의 가운데 숫자를 누르면 직접 입력할 수 있다', (tester) async {
await launchSignedIn(tester, extra: _withPet());
await _openRecords(tester);
await tester.tap(find.text('일정 추가'));
await settle(tester);
await _tapVisible(tester, find.text('시작'));
expect(find.text('오후 2:00 선택'), findsOneWidget);
// 가운데(선택된) 시 — 02
await tester.tap(find.text('02').first);
await tester.pump();
await tester.enterText(find.byType(TextField).last, '7');
await tester.testTextInput.receiveAction(TextInputAction.done);
await settle(tester);
expect(find.text('오후 7:00 선택'), findsOneWidget);
// 범위를 넘으면 가장 가까운 값으로 (분 99 → 59)
await tester.tap(find.text('00').first);
await tester.pump();
await tester.enterText(find.byType(TextField).last, '99');
await tester.testTextInput.receiveAction(TextInputAction.done);
await settle(tester);
expect(find.text('오후 7:59 선택'), findsOneWidget);
});
}