Skip to content

Commit 4859c24

Browse files
committed
Feat: 제보 화면 상태에 따른 UI 분리 및 제출 로직 개선
- ReportState, ReportSideEffect, SubmitState를 model 패키지로 분리 - 제보 제출(submitReportWithImages) 시, 최소 1초의 딜레이 보장
1 parent 10ee8fe commit 4859c24

7 files changed

Lines changed: 174 additions & 117 deletions

File tree

presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import android.net.Uri
55
import androidx.activity.compose.rememberLauncherForActivityResult
66
import androidx.activity.result.PickVisualMediaRequest
77
import androidx.activity.result.contract.ActivityResultContracts
8+
import androidx.compose.animation.AnimatedContent
9+
import androidx.compose.animation.AnimatedContentTransitionScope
10+
import androidx.compose.animation.SizeTransform
11+
import androidx.compose.animation.core.Spring
12+
import androidx.compose.animation.core.spring
13+
import androidx.compose.animation.togetherWith
814
import androidx.compose.foundation.layout.Arrangement
915
import androidx.compose.foundation.layout.Column
1016
import androidx.compose.foundation.layout.PaddingValues
@@ -50,6 +56,12 @@ import com.threegap.bitnagil.presentation.report.component.PhotoItem
5056
import com.threegap.bitnagil.presentation.report.component.ReportCategoryBottomSheet
5157
import com.threegap.bitnagil.presentation.report.component.ReportCategorySelector
5258
import com.threegap.bitnagil.presentation.report.component.ReportField
59+
import com.threegap.bitnagil.presentation.report.component.template.CompleteReportContent
60+
import com.threegap.bitnagil.presentation.report.component.template.SubmittingReportContent
61+
import com.threegap.bitnagil.presentation.report.model.ReportSideEffect
62+
import com.threegap.bitnagil.presentation.report.model.ReportState
63+
import com.threegap.bitnagil.presentation.report.model.SubmitState
64+
import com.threegap.bitnagil.presentation.report.model.uiTitle
5365
import org.orbitmvi.orbit.compose.collectAsState
5466
import org.orbitmvi.orbit.compose.collectSideEffect
5567

@@ -130,17 +142,48 @@ fun ReportScreenContainer(
130142
)
131143
}
132144

133-
ReportScreen(
134-
uiState = uiState,
135-
onReportTitleChange = viewModel::updateReportTitle,
136-
onReportContentChange = viewModel::updateReportContent,
137-
onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet,
138-
onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet,
139-
onRemoveImage = viewModel::removeImage,
140-
onGetCurrentLocationClick = locationPermissionHandler::requestPermission,
141-
onSubmitClick = viewModel::submitReportWithImages,
142-
onBackClick = viewModel::navigateToBack,
143-
)
145+
AnimatedContent(
146+
modifier = Modifier
147+
.fillMaxSize()
148+
.statusBarsPadding(),
149+
targetState = uiState.submitState,
150+
label = "ReportSlideAnimation",
151+
transitionSpec = {
152+
(
153+
slideIntoContainer(
154+
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
155+
towards = AnimatedContentTransitionScope.SlideDirection.Start,
156+
) togetherWith slideOutOfContainer(
157+
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
158+
towards = AnimatedContentTransitionScope.SlideDirection.Start,
159+
)
160+
)
161+
.using(SizeTransform(clip = true))
162+
},
163+
) { submitState ->
164+
when (submitState) {
165+
SubmitState.IDLE -> {
166+
ReportScreen(
167+
uiState = uiState,
168+
onReportTitleChange = viewModel::updateReportTitle,
169+
onReportContentChange = viewModel::updateReportContent,
170+
onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet,
171+
onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet,
172+
onRemoveImage = viewModel::removeImage,
173+
onGetCurrentLocationClick = locationPermissionHandler::requestPermission,
174+
onSubmitClick = viewModel::submitReportWithImages,
175+
onBackClick = viewModel::navigateToBack,
176+
)
177+
}
178+
SubmitState.SUBMITTING -> SubmittingReportContent()
179+
SubmitState.COMPLETE -> {
180+
CompleteReportContent(
181+
uiState = uiState,
182+
onConfirmClick = viewModel::navigateToBack,
183+
)
184+
}
185+
}
186+
}
144187
}
145188

146189
@OptIn(ExperimentalMaterial3Api::class)
@@ -161,7 +204,6 @@ private fun ReportScreen(
161204
Column(
162205
modifier = Modifier
163206
.fillMaxSize()
164-
.statusBarsPadding()
165207
.windowInsetsPadding(WindowInsets.ime),
166208
) {
167209
BitnagilTopBar(

presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt

Lines changed: 52 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import com.threegap.bitnagil.domain.report.model.Report
99
import com.threegap.bitnagil.domain.report.model.ReportCategory
1010
import com.threegap.bitnagil.domain.report.usecase.SubmitReportUseCase
1111
import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile
12+
import com.threegap.bitnagil.presentation.report.model.ReportSideEffect
13+
import com.threegap.bitnagil.presentation.report.model.ReportState
14+
import com.threegap.bitnagil.presentation.report.model.SubmitState
1215
import dagger.hilt.android.lifecycle.HiltViewModel
1316
import dagger.hilt.android.qualifiers.ApplicationContext
14-
import javax.inject.Inject
1517
import kotlinx.coroutines.async
1618
import kotlinx.coroutines.awaitAll
1719
import kotlinx.coroutines.coroutineScope
20+
import kotlinx.coroutines.delay
1821
import org.orbitmvi.orbit.Container
1922
import org.orbitmvi.orbit.ContainerHost
2023
import org.orbitmvi.orbit.viewmodel.container
24+
import javax.inject.Inject
2125

2226
@HiltViewModel
2327
class ReportViewModel @Inject constructor(
@@ -114,111 +118,67 @@ class ReportViewModel @Inject constructor(
114118

115119
fun submitReportWithImages() {
116120
intent {
121+
reduce { state.copy(submitState = SubmitState.SUBMITTING) }
122+
117123
val category = state.selectedCategory ?: return@intent
118124
val address = state.currentAddress ?: return@intent
119125
val latitude = state.currentLatitude ?: return@intent
120126
val longitude = state.currentLongitude ?: return@intent
121127

122-
reduce { state.copy(submitState = SubmitState.SUBMITTING) }
128+
coroutineScope {
129+
val minDelayJob = async {
130+
delay(1000L)
131+
}
123132

124-
val imageFiles = coroutineScope {
125-
state.reportImages
126-
.map { uri ->
127-
async {
128-
convertUriToImageFile(uri = uri, prefix = "report", context = context)
133+
val processingJob = async {
134+
val imageFiles = state.reportImages
135+
.map { uri ->
136+
async { convertUriToImageFile(uri, "report", context) }
129137
}
130-
}
131-
.awaitAll()
132-
.filterNotNull()
133-
}
138+
.awaitAll()
139+
.filterNotNull()
134140

135-
if (imageFiles.size < state.reportImages.size) {
136-
reduce { state.copy(submitState = SubmitState.ERROR) }
137-
return@intent
138-
}
141+
if (imageFiles.size < state.reportImages.size) {
142+
return@async Result.failure(Exception("Image conversion failed"))
143+
}
139144

140-
val uploadedPaths = uploadReportImagesUseCase(imageFiles).getOrElse { error ->
141-
reduce { state.copy(submitState = SubmitState.ERROR) }
142-
return@intent
145+
val uploadedPaths = uploadReportImagesUseCase(imageFiles)
146+
.getOrElse { return@async Result.failure(it) }
147+
148+
val submitResult = submitReportUseCase(
149+
Report(
150+
title = state.reportTitle,
151+
content = state.reportContent,
152+
category = category,
153+
address = address,
154+
latitude = latitude,
155+
longitude = longitude,
156+
imageUrls = uploadedPaths,
157+
),
158+
)
159+
160+
submitResult.map { uploadedPaths }
161+
}
162+
163+
minDelayJob.await()
164+
val result = processingJob.await()
165+
166+
result.fold(
167+
onSuccess = { paths ->
168+
reduce {
169+
state.copy(
170+
submitState = SubmitState.COMPLETE,
171+
uploadedImagePaths = paths,
172+
)
173+
}
174+
},
175+
onFailure = { error -> },
176+
)
143177
}
144-
145-
reduce { state.copy(uploadedImagePaths = uploadedPaths) }
146-
147-
submitReportUseCase(
148-
Report(
149-
title = state.reportTitle,
150-
content = state.reportContent,
151-
category = category.toDomain(),
152-
address = address,
153-
latitude = latitude,
154-
longitude = longitude,
155-
imageUrls = uploadedPaths,
156-
),
157-
).fold(
158-
onSuccess = {
159-
reduce { state.copy(submitState = SubmitState.SUCCESS) }
160-
},
161-
onFailure = { it ->
162-
reduce { state.copy(submitState = SubmitState.ERROR) }
163-
},
164-
)
165178
}
166179
}
167180

168181
companion object {
169182
const val MAX_IMAGE_COUNT = 3
170183
}
171184
}
172-
173-
data class ReportState(
174-
val reportImages: List<Uri>,
175-
val reportTitle: String,
176-
val reportContent: String,
177-
val selectedCategory: ReportCategoryUi?,
178-
val imageSourceBottomSheetVisible: Boolean,
179-
val reportCategoryBottomSheetVisible: Boolean,
180-
val currentAddress: String?,
181-
val currentLatitude: Double?,
182-
val currentLongitude: Double?,
183-
val uploadedImagePaths: List<String>,
184-
val submitState: SubmitState,
185-
) {
186-
val canAddMoreImages: Boolean
187-
get() = reportImages.size < ReportViewModel.MAX_IMAGE_COUNT
188-
189-
val isSubmittable: Boolean
190-
get() = reportImages.isNotEmpty() &&
191-
reportTitle.isNotEmpty() &&
192-
reportContent.isNotEmpty() &&
193-
selectedCategory != null &&
194-
currentAddress != null &&
195-
currentLatitude != null &&
196-
currentLongitude != null
197-
198-
companion object {
199-
val Init = ReportState(
200-
reportImages = emptyList(),
201-
reportTitle = "",
202-
reportContent = "",
203-
selectedCategory = null,
204-
imageSourceBottomSheetVisible = false,
205-
reportCategoryBottomSheetVisible = false,
206-
currentAddress = null,
207-
currentLatitude = null,
208-
currentLongitude = null,
209-
uploadedImagePaths = emptyList(),
210-
submitState = SubmitState.IDLE,
211-
)
212-
}
213-
}
214-
215-
enum class SubmitState {
216-
IDLE,
217-
SUBMITTING,
218-
SUCCESS,
219-
ERROR,
220-
}
221-
222-
sealed interface ReportSideEffect {
223-
data object NavigateToBack : ReportSideEffect
224-
}

presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ import com.threegap.bitnagil.designsystem.R
2424
import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon
2525
import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton
2626
import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor
27-
import com.threegap.bitnagil.presentation.report.ReportState
2827
import com.threegap.bitnagil.presentation.report.component.CompleteReportCard
28+
import com.threegap.bitnagil.presentation.report.model.ReportState
2929

3030
@Composable
3131
fun CompleteReportContent(
3232
uiState: ReportState,
3333
onConfirmClick: () -> Unit,
34-
modifier: Modifier = Modifier
34+
modifier: Modifier = Modifier,
3535
) {
3636
val scrollState = rememberScrollState()
3737

@@ -47,32 +47,32 @@ fun CompleteReportContent(
4747
tint = null,
4848
modifier = Modifier
4949
.padding(bottom = 12.dp)
50-
.size(40.dp)
50+
.size(40.dp),
5151
)
5252

5353
Text(
5454
text = "제보가 완료되었습니다.",
5555
color = BitnagilTheme.colors.coolGray10,
5656
style = BitnagilTheme.typography.title2Bold,
57-
modifier = Modifier.padding(bottom = 16.dp)
57+
modifier = Modifier.padding(bottom = 16.dp),
5858
)
5959

6060
Text(
6161
text = "빛나길에서 접수 후 완료되면\n신고를 진행합니다.",
6262
color = BitnagilTheme.colors.coolGray40,
6363
style = BitnagilTheme.typography.body1Medium,
64-
textAlign = TextAlign.Center
64+
textAlign = TextAlign.Center,
6565
)
6666

6767
Box(
68-
contentAlignment = Alignment.TopCenter
68+
contentAlignment = Alignment.TopCenter,
6969
) {
7070
Image(
7171
painter = painterResource(R.drawable.onboarding_character),
7272
contentDescription = null,
7373
modifier = Modifier
7474
.padding(top = 34.dp)
75-
.size(134.dp, 148.dp)
75+
.size(134.dp, 148.dp),
7676
)
7777

7878
CompleteReportCard(
@@ -109,6 +109,6 @@ fun CompleteReportContent(
109109
private fun Preview() {
110110
CompleteReportContent(
111111
uiState = ReportState.Init,
112-
onConfirmClick = {}
112+
onConfirmClick = {},
113113
)
114114
}

presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon
2020

2121
@Composable
2222
fun SubmittingReportContent(
23-
modifier: Modifier = Modifier
23+
modifier: Modifier = Modifier,
2424
) {
2525
Column(
2626
modifier = modifier
@@ -31,28 +31,28 @@ fun SubmittingReportContent(
3131
BitnagilIcon(
3232
id = R.drawable.ic_loading,
3333
tint = null,
34-
modifier = Modifier.padding(bottom = 12.dp)
34+
modifier = Modifier.padding(bottom = 12.dp),
3535
)
3636

3737
Text(
3838
text = "제보중...",
3939
color = BitnagilTheme.colors.coolGray10,
4040
style = BitnagilTheme.typography.title2Bold,
41-
modifier = Modifier.padding(bottom = 16.dp)
41+
modifier = Modifier.padding(bottom = 16.dp),
4242
)
4343

4444
Text(
4545
text = "포모가 열심\n제보하고 있어요",
4646
color = BitnagilTheme.colors.coolGray40,
4747
style = BitnagilTheme.typography.body1Medium,
48-
textAlign = TextAlign.Center
48+
textAlign = TextAlign.Center,
4949
)
5050

5151
Spacer(modifier = Modifier.height(72.dp))
5252

5353
Image(
5454
painter = painterResource(R.drawable.img_pomo_loading),
55-
contentDescription = null
55+
contentDescription = null,
5656
)
5757
}
5858
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.threegap.bitnagil.presentation.report.model
2+
3+
sealed interface ReportSideEffect {
4+
data object NavigateToBack : ReportSideEffect
5+
}

0 commit comments

Comments
 (0)