Skip to content

Commit 0c941f2

Browse files
authored
Merge pull request #322 from DevKor-github/321-준비과정-진행사항-로컬-저장
321 준비과정 진행사항 로컬 저장
2 parents 7779870 + a7f0298 commit 0c941f2

22 files changed

Lines changed: 627 additions & 376 deletions

docs/Architecture.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ OnTime follows **Clean Architecture** principles with a clear separation of conc
3636
│ DATA LAYER │
3737
├─────────────────────────────────────────────────────────────┤
3838
│ • Repository Implementations │
39-
│ • Data Sources (Remote API, Local Database)
39+
│ • Data Sources (Remote API, Local Database, Local Storage)
4040
│ • Data Models (JSON Serialization) │
4141
│ • Database Tables & DAOs │
4242
└─────────────────────────────────────────────────────────────┘
@@ -289,6 +289,12 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity
289289
- **Synchronization strategy** for online/offline data consistency
290290
- **Caching mechanisms** for improved performance
291291

292+
### 5. **Local Storage for Timed Preparation**
293+
294+
- `PreparationWithTimeLocalDataSource` persists `PreparationWithTimeEntity` per schedule using SharedPreferences.
295+
- Intended for lightweight, per-schedule timer state (elapsed time, completion) that should survive app restarts.
296+
- Repository reads canonical preparation from remote/DB; BLoC can merge it with locally persisted timing state when needed.
297+
292298
## 🧪 Testing Strategy
293299

294300
### Structure
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'dart:convert';
2+
3+
import 'package:injectable/injectable.dart';
4+
import 'package:shared_preferences/shared_preferences.dart';
5+
import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart';
6+
import 'package:on_time_front/domain/entities/preparation_step_with_time_entity.dart';
7+
8+
abstract interface class PreparationWithTimeLocalDataSource {
9+
Future<void> savePreparation(
10+
String scheduleId, PreparationWithTimeEntity preparation);
11+
Future<PreparationWithTimeEntity?> loadPreparation(String scheduleId);
12+
Future<void> clearPreparation(String scheduleId);
13+
}
14+
15+
@Injectable(as: PreparationWithTimeLocalDataSource)
16+
class PreparationWithTimeLocalDataSourceImpl
17+
implements PreparationWithTimeLocalDataSource {
18+
static const String _prefsKeyPrefix = 'preparation_with_time_';
19+
20+
@override
21+
Future<void> savePreparation(
22+
String scheduleId, PreparationWithTimeEntity preparation) async {
23+
final prefs = await SharedPreferences.getInstance();
24+
final key = '$_prefsKeyPrefix$scheduleId';
25+
26+
final jsonMap = {
27+
'steps': preparation.preparationStepList
28+
.map((s) => {
29+
'id': s.id,
30+
'name': s.preparationName,
31+
'time': s.preparationTime.inMilliseconds,
32+
'nextId': s.nextPreparationId,
33+
'elapsed': s.elapsedTime.inMilliseconds,
34+
'isDone': s.isDone,
35+
})
36+
.toList()
37+
};
38+
39+
await prefs.setString(key, jsonEncode(jsonMap));
40+
}
41+
42+
@override
43+
Future<PreparationWithTimeEntity?> loadPreparation(String scheduleId) async {
44+
final prefs = await SharedPreferences.getInstance();
45+
final key = '$_prefsKeyPrefix$scheduleId';
46+
final jsonString = prefs.getString(key);
47+
if (jsonString == null) return null;
48+
49+
final Map<String, dynamic> map = jsonDecode(jsonString);
50+
final List<dynamic> steps = map['steps'] as List<dynamic>;
51+
52+
final stepEntities = steps.map((raw) {
53+
final m = raw as Map<String, dynamic>;
54+
return PreparationStepWithTimeEntity(
55+
id: m['id'] as String,
56+
preparationName: m['name'] as String,
57+
preparationTime: Duration(milliseconds: (m['time'] as num).toInt()),
58+
nextPreparationId: m['nextId'] as String?,
59+
elapsedTime: Duration(milliseconds: (m['elapsed'] as num).toInt()),
60+
isDone: m['isDone'] as bool? ?? false,
61+
);
62+
}).toList();
63+
64+
return PreparationWithTimeEntity(preparationStepList: stepEntities);
65+
}
66+
67+
@override
68+
Future<void> clearPreparation(String scheduleId) async {
69+
final prefs = await SharedPreferences.getInstance();
70+
final key = '$_prefsKeyPrefix$scheduleId';
71+
await prefs.remove(key);
72+
}
73+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:on_time_front/data/data_sources/preparation_with_time_local_data_source.dart';
3+
import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart';
4+
import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart';
5+
6+
@Singleton(as: TimedPreparationRepository)
7+
class TimedPreparationRepositoryImpl implements TimedPreparationRepository {
8+
final PreparationWithTimeLocalDataSource localDataSource;
9+
10+
TimedPreparationRepositoryImpl({required this.localDataSource});
11+
12+
@override
13+
Future<void> clearTimedPreparation(String scheduleId) {
14+
return localDataSource.clearPreparation(scheduleId);
15+
}
16+
17+
@override
18+
Future<PreparationWithTimeEntity?> getTimedPreparation(String scheduleId) {
19+
return localDataSource.loadPreparation(scheduleId);
20+
}
21+
22+
@override
23+
Future<void> saveTimedPreparation(
24+
String scheduleId, PreparationWithTimeEntity preparation) {
25+
return localDataSource.savePreparation(scheduleId, preparation);
26+
}
27+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:on_time_front/domain/entities/preparation_step_entity.dart';
2+
3+
class PreparationStepWithTimeEntity extends PreparationStepEntity {
4+
final Duration elapsedTime;
5+
final bool isDone;
6+
7+
const PreparationStepWithTimeEntity({
8+
required super.id,
9+
required super.preparationName,
10+
required super.preparationTime,
11+
required super.nextPreparationId,
12+
this.elapsedTime = Duration.zero,
13+
this.isDone = false,
14+
});
15+
16+
@override
17+
PreparationStepWithTimeEntity copyWith({
18+
String? id,
19+
String? preparationName,
20+
Duration? preparationTime,
21+
String? nextPreparationId,
22+
Duration? elapsedTime,
23+
bool? isDone,
24+
}) {
25+
return PreparationStepWithTimeEntity(
26+
id: id ?? this.id,
27+
preparationName: preparationName ?? this.preparationName,
28+
preparationTime: preparationTime ?? this.preparationTime,
29+
nextPreparationId: nextPreparationId ?? this.nextPreparationId,
30+
elapsedTime: elapsedTime ?? this.elapsedTime,
31+
isDone: isDone ?? this.isDone,
32+
);
33+
}
34+
35+
PreparationStepWithTimeEntity timeElapsed(Duration elapsed) {
36+
final updatedElapsed = elapsedTime + elapsed;
37+
final updatedIsDone = updatedElapsed >= preparationTime;
38+
return copyWith(
39+
elapsedTime: updatedElapsed,
40+
isDone: updatedIsDone,
41+
);
42+
}
43+
44+
@override
45+
String toString() {
46+
return 'PreparationStepWithTimeEntity(id: $id, preparationName: $preparationName, preparationTime: $preparationTime, nextPreparationId: $nextPreparationId, elapsedTime: $elapsedTime, isDone: $isDone)';
47+
}
48+
49+
@override
50+
List<Object?> get props => [
51+
id,
52+
preparationName,
53+
preparationTime,
54+
nextPreparationId,
55+
elapsedTime,
56+
isDone
57+
];
58+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
3+
import 'package:on_time_front/domain/entities/preparation_step_with_time_entity.dart';
4+
import 'package:on_time_front/presentation/shared/constants/constants.dart';
5+
6+
class PreparationWithTimeEntity extends PreparationEntity implements Equatable {
7+
const PreparationWithTimeEntity({
8+
required List<PreparationStepWithTimeEntity> preparationStepList,
9+
}) : super(preparationStepList: preparationStepList);
10+
11+
factory PreparationWithTimeEntity.fromPreparation(
12+
PreparationEntity preparation) {
13+
return PreparationWithTimeEntity(
14+
preparationStepList: preparation.preparationStepList
15+
.map(
16+
(step) => PreparationStepWithTimeEntity(
17+
id: step.id,
18+
preparationName: step.preparationName,
19+
preparationTime: step.preparationTime,
20+
nextPreparationId: step.nextPreparationId,
21+
),
22+
)
23+
.toList(),
24+
);
25+
}
26+
27+
PreparationWithTimeEntity copyWith({
28+
List<PreparationStepWithTimeEntity>? preparationStepList,
29+
}) {
30+
return PreparationWithTimeEntity(
31+
preparationStepList: preparationStepList ?? this.preparationStepList,
32+
);
33+
}
34+
35+
@override
36+
List<PreparationStepWithTimeEntity> get preparationStepList =>
37+
super.preparationStepList.cast<PreparationStepWithTimeEntity>();
38+
39+
PreparationStepWithTimeEntity? get currentStep {
40+
for (final step in preparationStepList) {
41+
if (!step.isDone) {
42+
return step;
43+
}
44+
}
45+
return null; // All steps are done
46+
}
47+
48+
/// Returns true if all preparation steps are completed
49+
bool get isAllStepsDone {
50+
return currentStep == null;
51+
}
52+
53+
Duration get elapsedTime => preparationStepList.fold<Duration>(
54+
Duration.zero, (sum, s) => sum + s.elapsedTime);
55+
56+
/// Returns the progress as a value between 0.0 and 1.0
57+
double get progress {
58+
final totalSeconds = totalDuration.inSeconds;
59+
final elapsed = elapsedTime.inSeconds;
60+
return totalSeconds == 0 ? 0.0 : (elapsed / totalSeconds).clamp(0.0, 1.0);
61+
}
62+
63+
/// Returns the current step index, or -1 if all steps are done
64+
int get currentStepIndex {
65+
final current = currentStep;
66+
if (current == null) return -1;
67+
return preparationStepList.indexWhere((step) => step.id == current.id);
68+
}
69+
70+
/// Returns the resolved current step index for display purposes
71+
int get resolvedCurrentStepIndex {
72+
final index = currentStepIndex;
73+
return index == -1 ? preparationStepList.length - 1 : index;
74+
}
75+
76+
/// Returns the current step's remaining time
77+
Duration get currentStepRemainingTime {
78+
final current = currentStep;
79+
if (current == null) return Duration.zero;
80+
final remaining = current.preparationTime - current.elapsedTime;
81+
return remaining.isNegative ? Duration.zero : remaining;
82+
}
83+
84+
/// Returns the current step's name for display
85+
String get currentStepName {
86+
final current = currentStep;
87+
if (current != null) return current.preparationName;
88+
return preparationStepList.isNotEmpty
89+
? preparationStepList.last.preparationName
90+
: '';
91+
}
92+
93+
/// Returns elapsed times for each step in seconds
94+
List<int> get stepElapsedTimesInSeconds {
95+
return preparationStepList
96+
.map<int>((step) => step.elapsedTime.inSeconds)
97+
.toList();
98+
}
99+
100+
/// Returns the preparation state for each step
101+
List<PreparationStateEnum> get preparationStepStates {
102+
final resolvedIndex = resolvedCurrentStepIndex;
103+
104+
return List<PreparationStateEnum>.generate(
105+
preparationStepList.length,
106+
(index) {
107+
if (isAllStepsDone) {
108+
// All steps are done
109+
return PreparationStateEnum.done;
110+
}
111+
if (index < resolvedIndex) {
112+
return PreparationStateEnum.done;
113+
}
114+
if (index == resolvedIndex && !isAllStepsDone) {
115+
return PreparationStateEnum.now;
116+
}
117+
return PreparationStateEnum.yet;
118+
},
119+
);
120+
}
121+
122+
PreparationWithTimeEntity timeElapsed(Duration elapsed) {
123+
final current = currentStep;
124+
if (current == null) {
125+
return this; // All steps are done, no changes needed
126+
}
127+
128+
Duration remainingElapsed = elapsed;
129+
List<PreparationStepWithTimeEntity> updatedSteps =
130+
List.from(preparationStepList);
131+
132+
// Find the current step index
133+
int currentIndex = updatedSteps.indexWhere((step) => step.id == current.id);
134+
135+
// Apply elapsed time to current and subsequent steps if needed
136+
while (remainingElapsed > Duration.zero &&
137+
currentIndex < updatedSteps.length) {
138+
final step = updatedSteps[currentIndex];
139+
if (step.isDone) {
140+
currentIndex++;
141+
continue;
142+
}
143+
144+
final stepRemainingTime = step.preparationTime - step.elapsedTime;
145+
146+
if (remainingElapsed >= stepRemainingTime) {
147+
// Complete this step and move to next
148+
updatedSteps[currentIndex] = step.copyWith(
149+
elapsedTime: step.preparationTime,
150+
isDone: true,
151+
);
152+
remainingElapsed -= stepRemainingTime;
153+
currentIndex++;
154+
} else {
155+
// Partially complete this step
156+
updatedSteps[currentIndex] = step.copyWith(
157+
elapsedTime: step.elapsedTime + remainingElapsed,
158+
isDone: step.elapsedTime + remainingElapsed >= step.preparationTime,
159+
);
160+
remainingElapsed = Duration.zero;
161+
}
162+
}
163+
164+
return copyWith(preparationStepList: updatedSteps);
165+
}
166+
167+
PreparationWithTimeEntity skipCurrentStep() {
168+
final current = currentStep;
169+
if (current == null) {
170+
return this; // All steps are done, no changes needed
171+
}
172+
173+
final updatedCurrentStep = current.copyWith(
174+
elapsedTime: current.preparationTime,
175+
isDone: true,
176+
);
177+
return copyWith(
178+
preparationStepList: preparationStepList
179+
.map((step) =>
180+
step.id == updatedCurrentStep.id ? updatedCurrentStep : step)
181+
.toList(),
182+
);
183+
}
184+
185+
@override
186+
String toString() {
187+
return 'PreparationWithTimeEntity(preparationStepList: ${preparationStepList.toString()})';
188+
}
189+
190+
@override
191+
List<Object?> get props => [preparationStepList];
192+
}

0 commit comments

Comments
 (0)