Skip to content

Commit 2bb5599

Browse files
authored
Merge pull request #92 from Nexters/feature/diary-detail-pending-FD-282
[FD-282] 홈/상세 이미지 업로드 후 processing 처리를 위해 api 새로 요청
2 parents 64cf473 + 363197f commit 2bb5599

11 files changed

Lines changed: 87 additions & 50 deletions

File tree

app/src/main/java/com/nexters/fooddiary/navigation/FoodDiaryNavHost.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -387,14 +387,17 @@ fun FoodDiaryNavHost(
387387
if (previousIsHome) {
388388
navController.previousBackStackEntry
389389
?.savedStateHandle
390-
?.set(SyncConstants.DIARY_UPLOAD_PENDING_DATE, uploadedDate.toString())
390+
?.set(SyncConstants.DIARY_REFRESH_DATE, uploadedDate.toString())
391391
}
392-
navController.popBackStack()
393392
if (previousIsDetail) {
394-
navController.navigate(DetailRoute(dateString = uploadedDate.toString())) {
395-
launchSingleTop = true
396-
}
397-
} else if (!previousIsHome) {
393+
navController.previousBackStackEntry
394+
?.savedStateHandle
395+
?.set(SyncConstants.DIARY_REFRESH_DATE, uploadedDate.toString())
396+
}
397+
navController.popBackStack()
398+
if (previousIsHome) {
399+
navController.navigate(DetailRoute(dateString = uploadedDate.toString()))
400+
} else if (!previousIsDetail) {
398401
navController.navigate(HomeRoute) {
399402
launchSingleTop = true
400403
}

core/common/src/main/java/com/nexters/fooddiary/core/common/navigation/SyncConstants.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ package com.nexters.fooddiary.core.common.navigation
22

33
object SyncConstants {
44
const val DIARY_REFRESH_DATE = "diary_refresh_date"
5-
const val DIARY_UPLOAD_PENDING_DATE = "diary_upload_pending_date"
65
}

presentation/home/src/main/java/com/nexters/fooddiary/presentation/home/HomeScreen.kt

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ internal fun HomeScreen(
6161
onNavigateToMyPage: () -> Unit = {},
6262
isMonthlyCalendarView: Boolean = false,
6363
refreshDiaryDateString: String? = null,
64-
diaryUploadPendingDateString: String? = null,
6564
onRefreshDiaryConsumed: () -> Unit = {},
66-
onDiaryUploadPendingConsumed: () -> Unit = {},
6765
onShowSnackBar: (SnackBarData) -> Unit = {},
6866
viewModel: HomeViewModel = mavericksViewModel(),
6967
) {
@@ -123,14 +121,6 @@ internal fun HomeScreen(
123121
onRefreshDiaryConsumed()
124122
}
125123

126-
LaunchedEffect(diaryUploadPendingDateString) {
127-
val dateString = diaryUploadPendingDateString ?: return@LaunchedEffect
128-
runCatching { LocalDate.parse(dateString) }
129-
.getOrNull()
130-
?.let(viewModel::onDiaryUploadPending)
131-
onDiaryUploadPendingConsumed()
132-
}
133-
134124
HomeScreen(
135125
state = state,
136126
isMonthlyCalendarView = isMonthlyCalendarView,
@@ -142,8 +132,8 @@ internal fun HomeScreen(
142132
selectedDateImageUrls = selectedDateImageUrls(
143133
weeklyPhotosByDate = state.weeklyPhotosByDate,
144134
selectedDate = state.selectedDate,
135+
selectedDateImageStatesByUrl = state.selectedDateImageStatesByUrl,
145136
),
146-
isSelectedDatePending = state.pendingDates.contains(state.selectedDate),
147137
onShowSnackBar = onShowSnackBar,
148138
onMonthChanged = viewModel::loadPhotosForMonth,
149139
photoCountByDate = photoCountByDate,
@@ -155,7 +145,9 @@ internal fun HomeScreen(
155145
internal fun selectedDateImageUrls(
156146
weeklyPhotosByDate: Map<LocalDate, List<String>>,
157147
selectedDate: LocalDate,
148+
selectedDateImageStatesByUrl: Map<String, FoodImageState>,
158149
): List<String> = weeklyPhotosByDate[selectedDate].orEmpty()
150+
.ifEmpty { selectedDateImageStatesByUrl.keys.toList() }
159151

160152
@Composable
161153
private fun HomeScreen(
@@ -168,7 +160,6 @@ private fun HomeScreen(
168160
onNavigateToDetail: (LocalDate) -> Unit = {},
169161
onNavigateToMyPage: () -> Unit = {},
170162
selectedDateImageUrls: List<String> = emptyList(),
171-
isSelectedDatePending: Boolean = false,
172163
onShowSnackBar: (SnackBarData) -> Unit = {},
173164
onMonthChanged: (YearMonth) -> Unit = {},
174165
photoCountByDate: Map<LocalDate, Int> = emptyMap(),
@@ -238,15 +229,6 @@ private fun HomeScreen(
238229
.padding(horizontal = 20.dp)
239230
.aspectRatio(1f),
240231
)
241-
} else if (isSelectedDatePending) {
242-
FoodImageCard(
243-
imageUrl = "",
244-
state = FoodImageState.Pending,
245-
modifier = Modifier
246-
.fillMaxWidth()
247-
.padding(horizontal = 20.dp)
248-
.aspectRatio(1f),
249-
)
250232
} else if (canShowAddPhoto == null) {
251233
Box(
252234
modifier = Modifier

presentation/home/src/main/java/com/nexters/fooddiary/presentation/home/HomeScreenState.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ data class HomeScreenState(
1515
locationText = "",
1616
),
1717
val selectedDateImageStatesByUrl: Map<String, FoodImageState> = emptyMap(),
18-
val pendingDates: Set<LocalDate> = emptySet(),
1918
val hasAddableImagesForSelectedDate: Boolean? = null,
2019
val loadedWeekStartDate: LocalDate? = null,
2120
) : MavericksState

presentation/home/src/main/java/com/nexters/fooddiary/presentation/home/HomeViewModel.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory
99
import com.nexters.fooddiary.core.common.permission.PermissionUtil
1010
import com.nexters.fooddiary.core.common.toLocalTimeText
1111
import com.nexters.fooddiary.core.ui.food.FoodImageState
12+
import com.nexters.fooddiary.domain.model.AnalysisStatus
1213
import com.nexters.fooddiary.domain.model.DiaryDetail
1314
import com.nexters.fooddiary.domain.usecase.GetDiaryByDateUseCase
1415
import com.nexters.fooddiary.domain.usecase.GetDiarySummaryUseCase
@@ -146,7 +147,6 @@ class HomeViewModel @AssistedInject constructor(
146147
}
147148

148149
fun onDiaryUpdated(date: LocalDate) {
149-
setState { copy(pendingDates = pendingDates - date) }
150150
withState { state ->
151151
if (YearMonth.from(state.selectedDate) == YearMonth.from(date)) {
152152
loadPhotosForMonth(YearMonth.from(state.selectedDate))
@@ -155,15 +155,12 @@ class HomeViewModel @AssistedInject constructor(
155155
loadSummaryForSelectedWeek(forceRefresh = true)
156156
}
157157
if (state.selectedDate == date) {
158+
loadSelectedDateImageState(state.selectedDate)
158159
loadAddableImageStateForSelectedDate(state.selectedDate)
159160
}
160161
}
161162
}
162163

163-
fun onDiaryUploadPending(date: LocalDate) {
164-
setState { copy(pendingDates = pendingDates + date) }
165-
}
166-
167164
fun onCardStackClicked() {
168165
withState { state ->
169166
_events.tryEmit(HomeEvent.NavigateToDetail(state.selectedDate))
@@ -281,10 +278,14 @@ internal fun shouldLoadWeek(
281278

282279
private fun DiaryDetail.toHomeFoodImageStatesByUrl(): Map<String, FoodImageState> {
283280
return diaries.flatMap { diary ->
284-
val state = FoodImageState.Ready(
285-
timeText = diary.diaryDate.toLocalTimeText(),
286-
locationText = diary.location.orEmpty(),
287-
)
281+
val state = when (diary.analysisStatus) {
282+
AnalysisStatus.PROCESSING -> FoodImageState.Processing
283+
AnalysisStatus.DONE,
284+
AnalysisStatus.FAILED -> FoodImageState.Ready(
285+
timeText = diary.diaryDate.toLocalTimeText(),
286+
locationText = diary.location.orEmpty(),
287+
)
288+
}
288289
diary.photos.map { photo -> photo.imageUrl to state }
289290
}.toMap()
290291
}

presentation/home/src/main/java/com/nexters/fooddiary/presentation/home/navigation/HomeNavigation.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,15 @@ fun NavGraphBuilder.homeScreen(
2424
val refreshDiaryDateString by backStackEntry.savedStateHandle
2525
.getStateFlow<String?>(SyncConstants.DIARY_REFRESH_DATE, null)
2626
.collectAsState()
27-
val diaryUploadPendingDateString by backStackEntry.savedStateHandle
28-
.getStateFlow<String?>(SyncConstants.DIARY_UPLOAD_PENDING_DATE, null)
29-
.collectAsState()
3027
HomeScreen(
3128
onNavigateToImagePicker = onNavigateToImagePicker,
3229
onNavigateToDetail = onNavigateToDetail,
3330
onNavigateToMyPage = onNavigateToMyPage,
3431
isMonthlyCalendarView = isMonthlyCalendarView(),
3532
refreshDiaryDateString = refreshDiaryDateString,
36-
diaryUploadPendingDateString = diaryUploadPendingDateString,
3733
onRefreshDiaryConsumed = {
3834
backStackEntry.savedStateHandle.remove<String>(SyncConstants.DIARY_REFRESH_DATE)
3935
},
40-
onDiaryUploadPendingConsumed = {
41-
backStackEntry.savedStateHandle.remove<String>(SyncConstants.DIARY_UPLOAD_PENDING_DATE)
42-
},
4336
onShowSnackBar = onShowSnackBar,
4437
)
4538
}

presentation/home/src/test/java/com/nexters/fooddiary/presentation/home/HomeSummaryLogicTest.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.nexters.fooddiary.presentation.home
22

3+
import com.nexters.fooddiary.core.ui.food.FoodImageState
34
import org.junit.Assert.assertEquals
45
import org.junit.Assert.assertFalse
56
import org.junit.Assert.assertTrue
@@ -41,8 +42,28 @@ class HomeSummaryLogicTest {
4142
LocalDate.parse("2026-02-23") to listOf("c"),
4243
)
4344

44-
val result = selectedDateImageUrls(map, selectedDate)
45+
val result = selectedDateImageUrls(
46+
weeklyPhotosByDate = map,
47+
selectedDate = selectedDate,
48+
selectedDateImageStatesByUrl = emptyMap(),
49+
)
4550

4651
assertEquals(listOf("a", "b"), result)
4752
}
53+
54+
@Test
55+
fun `summary 이미지가 없어도 detail 이미지로 fallback 한다`() {
56+
val selectedDate = LocalDate.parse("2026-02-22")
57+
58+
val result = selectedDateImageUrls(
59+
weeklyPhotosByDate = emptyMap(),
60+
selectedDate = selectedDate,
61+
selectedDateImageStatesByUrl = linkedMapOf(
62+
"processing-a" to FoodImageState.Processing,
63+
"processing-b" to FoodImageState.Processing,
64+
),
65+
)
66+
67+
assertEquals(listOf("processing-a", "processing-b"), result)
68+
}
4869
}

presentation/image/src/main/java/com/nexters/fooddiary/presentation/image/ImagePickerScreen.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ fun ImagePickerScreen(
135135
foodImageUris = state.foodImageUris,
136136
allImageUris = state.allImageUris,
137137
isLoading = state.isLoading,
138+
isUploading = state.isUploading,
138139
hasPermission = state.hasPermission,
139140
selectedUris = state.selectedUris,
140141
onImageClick = { uri -> viewModel.toggleImageSelection(uri) },
@@ -153,6 +154,7 @@ fun ImagePickerContent(
153154
foodImageUris: List<Uri> = emptyList(),
154155
allImageUris: List<Uri> = emptyList(),
155156
isLoading: Boolean = false,
157+
isUploading: Boolean = false,
156158
hasPermission: Boolean = true,
157159
selectedUris: Set<Uri> = emptySet(),
158160
onImageClick: (Uri) -> Unit = {},
@@ -214,6 +216,7 @@ fun ImagePickerContent(
214216

215217
ImagePickerDoneButton(
216218
selectedCount = selectedUris.size,
219+
enabled = !isUploading,
217220
onClick = onDone,
218221
modifier = Modifier.align(Alignment.BottomCenter)
219222
)
@@ -474,6 +477,7 @@ private fun Modifier.doneButtonSurface(): Modifier = this
474477
@Composable
475478
private fun ImagePickerDoneButton(
476479
selectedCount: Int,
480+
enabled: Boolean,
477481
onClick: () -> Unit,
478482
modifier: Modifier = Modifier
479483
) {
@@ -484,6 +488,7 @@ private fun ImagePickerDoneButton(
484488
) {
485489
Button(
486490
onClick = onClick,
491+
enabled = enabled,
487492
modifier = Modifier.fillMaxWidth(),
488493
shape = RoundedCornerShape(999.dp),
489494
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),

presentation/image/src/main/java/com/nexters/fooddiary/presentation/image/ImagePickerState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ data class ImagePickerState(
88
val foodImageUris: List<Uri> = emptyList(),
99
val allImageUris: List<Uri> = emptyList(),
1010
val isLoading: Boolean = false,
11+
val isUploading: Boolean = false,
1112
val hasPermission: Boolean = false,
1213
val selectedUris: Set<Uri> = emptySet(),
1314
val filterDate: LocalDate? = null,

presentation/image/src/main/java/com/nexters/fooddiary/presentation/image/ImagePickerViewModel.kt

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.nexters.fooddiary.presentation.image
22

33
import android.content.Context
44
import android.net.Uri
5+
import androidx.work.WorkInfo
6+
import androidx.work.WorkManager
57
import com.airbnb.mvrx.MavericksViewModel
68
import com.airbnb.mvrx.MavericksViewModelFactory
79
import com.airbnb.mvrx.hilt.AssistedViewModelFactory
@@ -13,9 +15,12 @@ import dagger.assisted.AssistedFactory
1315
import dagger.assisted.AssistedInject
1416
import dagger.hilt.android.qualifiers.ApplicationContext
1517
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.flow.filterNotNull
19+
import kotlinx.coroutines.flow.first
1620
import kotlinx.coroutines.launch
1721
import kotlinx.coroutines.withContext
1822
import java.time.LocalDate
23+
import java.util.UUID
1924

2025
internal fun nextSelectionAfterToggle(current: Set<Uri>, uri: Uri, maxCount: Int): Set<Uri> =
2126
if (current.contains(uri)) current - uri
@@ -27,6 +32,7 @@ class ImagePickerViewModel @AssistedInject constructor(
2732
@ApplicationContext private val context: Context,
2833
private val getFoodPhotosUseCase: GetFoodPhotosUseCase,
2934
) : MavericksViewModel<ImagePickerState>(initialState) {
35+
private val workManager by lazy { WorkManager.getInstance(context) }
3036

3137
init {
3238
updatePermissionState()
@@ -47,6 +53,7 @@ class ImagePickerViewModel @AssistedInject constructor(
4753
allImageUris = emptyList(),
4854
foodImageUris = emptyList(),
4955
isLoading = true,
56+
isUploading = false,
5057
uploadSucceededDate = null,
5158
)
5259
}
@@ -95,9 +102,10 @@ class ImagePickerViewModel @AssistedInject constructor(
95102
val state = awaitState()
96103
val urisToUpload = state.selectedUris.map { it.toString() }
97104
val targetDate = state.filterDate ?: LocalDate.now()
98-
if (urisToUpload.isEmpty()) {
105+
if (urisToUpload.isEmpty() || state.isUploading) {
99106
return@launch
100107
}
108+
setState { copy(isUploading = true) }
101109

102110
val result = withContext(Dispatchers.Default) {
103111
runCatching {
@@ -110,8 +118,8 @@ class ImagePickerViewModel @AssistedInject constructor(
110118
}
111119

112120
result
113-
.onSuccess { setState { copy(uploadSucceededDate = targetDate) } }
114-
.onFailure { /* enqueue 실패 시 화면 유지 */ }
121+
.onSuccess { requestId -> observeUploadCompletion(requestId, targetDate) }
122+
.onFailure { setState { copy(isUploading = false) } }
115123
}
116124
}
117125

@@ -128,6 +136,29 @@ class ImagePickerViewModel @AssistedInject constructor(
128136
const val MAX_SELECTION_COUNT = 10
129137
}
130138

139+
private fun observeUploadCompletion(requestId: UUID, targetDate: LocalDate) {
140+
viewModelScope.launch {
141+
val workInfo = workManager
142+
.getWorkInfoByIdFlow(requestId)
143+
.filterNotNull()
144+
.first { it.state.isFinished }
145+
146+
when (workInfo.state) {
147+
WorkInfo.State.SUCCEEDED -> {
148+
setState {
149+
copy(
150+
isUploading = false,
151+
uploadSucceededDate = targetDate,
152+
)
153+
}
154+
}
155+
else -> {
156+
setState { copy(isUploading = false) }
157+
}
158+
}
159+
}
160+
}
161+
131162
private fun applyEmptyResult() {
132163
setState {
133164
copy(

0 commit comments

Comments
 (0)