From 1e4c412ee35470bffaca8cb210a823c8f3c452c7 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 28 Jun 2026 15:07:19 +0900 Subject: [PATCH] fix: copy default preparation steps for schedule edits --- .../bloc/schedule_form_bloc.dart | 27 +++- .../bloc/schedule_form_state.dart | 40 +++--- .../bloc/schedule_form_bloc_test.dart | 116 ++++++++++++++++++ 3 files changed, 165 insertions(+), 18 deletions(-) diff --git a/lib/presentation/schedule_create/bloc/schedule_form_bloc.dart b/lib/presentation/schedule_create/bloc/schedule_form_bloc.dart index a0932ed8..29b93a81 100644 --- a/lib/presentation/schedule_create/bloc/schedule_form_bloc.dart +++ b/lib/presentation/schedule_create/bloc/schedule_form_bloc.dart @@ -4,7 +4,9 @@ import 'package:injectable/injectable.dart'; import 'package:on_time_front/core/dio/api_error_message.dart'; import 'package:on_time_front/domain/entities/place_entity.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart'; import 'package:on_time_front/domain/use-cases/create_custom_preparation_use_case.dart'; import 'package:on_time_front/domain/use-cases/create_schedule_with_place_use_case.dart'; import 'package:on_time_front/domain/use-cases/get_default_preparation_use_case.dart'; @@ -97,6 +99,7 @@ class ScheduleFormBloc extends Bloc { scheduleSpareTime: scheduleEntity.scheduleSpareTime, scheduleNote: scheduleEntity.scheduleNote, preparation: preparationEntity, + originalPreparationMode: scheduleEntity.preparationMode, ), ); } @@ -224,7 +227,7 @@ class ScheduleFormBloc extends Bloc { await _updateScheduleUseCase(scheduleEntity); if (state.isChanged != IsPreparationChanged.unchanged) { await _updatePreparationByScheduleIdUseCase( - state.preparation!, + _preparationForScheduleUpdate(), scheduleEntity.id, ); } @@ -311,6 +314,28 @@ class ScheduleFormBloc extends Bloc { ); } + PreparationEntity _preparationForScheduleUpdate() { + final preparation = state.preparation!; + if (state.originalPreparationMode != + SchedulePreparationMode.defaultPreparation) { + return preparation; + } + + final orderedSteps = preparation.ordered.preparationStepList; + final newIds = List.generate(orderedSteps.length, (_) => Uuid().v7()); + final copiedSteps = [ + for (var i = 0; i < orderedSteps.length; i++) + PreparationStepEntity( + id: newIds[i], + preparationName: orderedSteps[i].preparationName, + preparationTime: orderedSteps[i].preparationTime, + nextPreparationId: i + 1 < newIds.length ? newIds[i + 1] : null, + ), + ]; + + return PreparationEntity(preparationStepList: copiedSteps); + } + Future _trackScheduleCreated(ScheduleEntity scheduleEntity) async { await _productUsageEventTracker.track( ProductUsageEvent( diff --git a/lib/presentation/schedule_create/bloc/schedule_form_state.dart b/lib/presentation/schedule_create/bloc/schedule_form_state.dart index 16a6ab13..31d65e12 100644 --- a/lib/presentation/schedule_create/bloc/schedule_form_state.dart +++ b/lib/presentation/schedule_create/bloc/schedule_form_state.dart @@ -22,6 +22,7 @@ final class ScheduleFormState extends Equatable { final Duration? scheduleSpareTime; final String? scheduleNote; final PreparationEntity? preparation; + final SchedulePreparationMode? originalPreparationMode; final bool isValid; final Duration? maxAvailableTime; final String? previousScheduleName; @@ -40,6 +41,7 @@ final class ScheduleFormState extends Equatable { this.scheduleSpareTime, this.scheduleNote, this.preparation, + this.originalPreparationMode, this.isValid = false, this.maxAvailableTime, this.previousScheduleName, @@ -59,6 +61,7 @@ final class ScheduleFormState extends Equatable { Duration? scheduleSpareTime, String? scheduleNote, PreparationEntity? preparation, + SchedulePreparationMode? originalPreparationMode, bool? isValid, Duration? maxAvailableTime, String? previousScheduleName, @@ -79,6 +82,8 @@ final class ScheduleFormState extends Equatable { scheduleSpareTime: scheduleSpareTime ?? this.scheduleSpareTime, scheduleNote: scheduleNote ?? this.scheduleNote, preparation: preparation ?? this.preparation, + originalPreparationMode: + originalPreparationMode ?? this.originalPreparationMode, isValid: isValid ?? this.isValid, maxAvailableTime: maxAvailableTime ?? this.maxAvailableTime, previousScheduleName: previousScheduleName ?? this.previousScheduleName, @@ -111,21 +116,22 @@ final class ScheduleFormState extends Equatable { @override List get props => [ - status, - submissionStatus, - submissionError, - id, - placeId, - placeName, - scheduleName, - scheduleTime, - moveTime, - isChanged, - scheduleSpareTime, - scheduleNote, - preparation, - isValid, - maxAvailableTime, - previousScheduleName, - ]; + status, + submissionStatus, + submissionError, + id, + placeId, + placeName, + scheduleName, + scheduleTime, + moveTime, + isChanged, + scheduleSpareTime, + scheduleNote, + preparation, + originalPreparationMode, + isValid, + maxAvailableTime, + previousScheduleName, + ]; } diff --git a/test/presentation/schedule_create/bloc/schedule_form_bloc_test.dart b/test/presentation/schedule_create/bloc/schedule_form_bloc_test.dart index 0771631a..1db44a11 100644 --- a/test/presentation/schedule_create/bloc/schedule_form_bloc_test.dart +++ b/test/presentation/schedule_create/bloc/schedule_form_bloc_test.dart @@ -5,6 +5,7 @@ import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/product_usage_event.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/use-cases/track_product_usage_event_use_case.dart'; import 'package:on_time_front/domain/use-cases/create_custom_preparation_use_case.dart'; @@ -477,6 +478,121 @@ void main() { expect(preparationUpdateCount, 0); }); + test( + 'ScheduleFormUpdated copies default preparation with fresh step IDs', + () async { + getScheduleByIdUseCase = StubGetScheduleByIdUseCase( + (_) async => schedule.copyWith( + preparationMode: SchedulePreparationMode.defaultPreparation, + ), + ); + + PreparationEntity? updatedPreparation; + updatePreparationByScheduleIdUseCase = + StubUpdatePreparationByScheduleIdUseCase((preparation, _) async { + updatedPreparation = preparation; + }); + + final bloc = buildBloc(); + addTearDown(bloc.close); + + final editReady = bloc.stream.firstWhere( + (state) => state.status == ScheduleFormStatus.success, + ); + bloc.add(const ScheduleFormEditRequested(scheduleId: 'schedule-1')); + await editReady; + + const editedPreparation = PreparationEntity( + preparationStepList: [ + PreparationStepEntity( + id: 'default-step-1', + preparationName: 'Makeup', + preparationTime: Duration(minutes: 20), + nextPreparationId: 'default-step-2', + ), + PreparationStepEntity( + id: 'default-step-2', + preparationName: 'Bathroom', + preparationTime: Duration(minutes: 5), + ), + ], + ); + bloc.add( + const ScheduleFormPreparationChanged(preparation: editedPreparation), + ); + + final submitDone = bloc.stream.firstWhere( + (state) => + state.submissionStatus == ScheduleFormSubmissionStatus.success, + ); + bloc.add(const ScheduleFormUpdated()); + await submitDone; + + final steps = updatedPreparation!.preparationStepList; + expect(steps, hasLength(2)); + expect(steps[0].preparationName, 'Makeup'); + expect(steps[0].preparationTime, const Duration(minutes: 20)); + expect(steps[1].preparationName, 'Bathroom'); + expect(steps[1].preparationTime, const Duration(minutes: 5)); + expect(steps[0].id, isNot('default-step-1')); + expect(steps[1].id, isNot('default-step-2')); + expect(steps[0].nextPreparationId, steps[1].id); + expect(steps[1].nextPreparationId, isNull); + }, + ); + + test('ScheduleFormUpdated preserves custom preparation step IDs', () async { + getScheduleByIdUseCase = StubGetScheduleByIdUseCase( + (_) async => + schedule.copyWith(preparationMode: SchedulePreparationMode.custom), + ); + + PreparationEntity? updatedPreparation; + updatePreparationByScheduleIdUseCase = + StubUpdatePreparationByScheduleIdUseCase((preparation, _) async { + updatedPreparation = preparation; + }); + + final bloc = buildBloc(); + addTearDown(bloc.close); + + final editReady = bloc.stream.firstWhere( + (state) => state.status == ScheduleFormStatus.success, + ); + bloc.add(const ScheduleFormEditRequested(scheduleId: 'schedule-1')); + await editReady; + + const editedPreparation = PreparationEntity( + preparationStepList: [ + PreparationStepEntity( + id: 'custom-step-1', + preparationName: 'Makeup', + preparationTime: Duration(minutes: 20), + nextPreparationId: 'custom-step-2', + ), + PreparationStepEntity( + id: 'custom-step-2', + preparationName: 'Bathroom', + preparationTime: Duration(minutes: 5), + ), + ], + ); + bloc.add( + const ScheduleFormPreparationChanged(preparation: editedPreparation), + ); + + final submitDone = bloc.stream.firstWhere( + (state) => state.submissionStatus == ScheduleFormSubmissionStatus.success, + ); + bloc.add(const ScheduleFormUpdated()); + await submitDone; + + final steps = updatedPreparation!.preparationStepList; + expect(steps.map((step) => step.id), ['custom-step-1', 'custom-step-2']); + expect(steps[0].nextPreparationId, 'custom-step-2'); + expect(steps[1].nextPreparationId, isNull); + }); + test( 'ScheduleFormCreated persists custom preparation when changed', () async {