Skip to content
8 changes: 8 additions & 0 deletions app/src/main/java/com/flint/data/api/CollectionApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flint.data.api
import com.flint.data.dto.base.BaseEmptyResponse
import com.flint.data.dto.base.BaseResponse
import com.flint.data.dto.collection.request.CollectionCreateRequestDto
import com.flint.data.dto.collection.request.CollectionReportRequestDto
import com.flint.data.dto.collection.response.CollectionCreateResponseDto
import com.flint.data.dto.collection.response.CollectionDetailResponseDto
import com.flint.data.dto.collection.response.CollectionsResponseDto
Expand Down Expand Up @@ -51,4 +52,11 @@ interface CollectionApi {
// 최근 본 컬렉션 목록 조회
@GET("/api/v1/collections/recent")
suspend fun getRecentCollectionList(): BaseResponse<RecentCollectionListResponseDto>

// 컬렉션 신고
@POST("/api/v1/collections/{collectionId}/reports")
suspend fun postCollectionReport(
@Path("collectionId") collectionId: String,
@Body requestDto: CollectionReportRequestDto,
): BaseEmptyResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.flint.data.dto.collection.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CollectionReportRequestDto(
@SerialName("reasons")
val reasons: List<String>,
@SerialName("otherDetail")
val otherDetail: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flint.domain.mapper.collection

import com.flint.data.dto.collection.request.CollectionReportRequestDto
import com.flint.domain.model.collection.CollectionReportRequestModel

fun CollectionReportRequestModel.toDto(): CollectionReportRequestDto =
CollectionReportRequestDto(
reasons = reasons,
otherDetail = otherDetail,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.flint.domain.model.collection

data class CollectionReportRequestModel(
val reasons: List<String>,
val otherDetail: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import com.flint.core.common.util.suspendRunCatching
import com.flint.data.api.CollectionApi
import com.flint.data.dto.collection.request.CollectionCreateRequestDto
import com.flint.data.dto.collection.response.CollectionDetailResponseDto
import com.flint.domain.mapper.collection.toDto
import com.flint.domain.mapper.collection.toModel
import com.flint.domain.model.collection.CollectionCreateModel
import com.flint.domain.model.collection.CollectionDetailModelNew
import com.flint.domain.model.collection.CollectionListModel
import com.flint.domain.model.collection.CollectionReportRequestModel
import com.flint.domain.model.collection.CollectionsModel
import javax.inject.Inject

Expand Down Expand Up @@ -61,4 +63,13 @@ class CollectionRepository @Inject constructor(
// 최근 본 컬렉션 목록 조회
suspend fun getRecentCollectionList(): Result<CollectionListModel> =
suspendRunCatching { apiService.getRecentCollectionList().data.toModel() }

// 컬렉션 신고
suspend fun postCollectionReport(
collectionId: String,
requestModel: CollectionReportRequestModel,
): Result<Unit> =
suspendRunCatching {
apiService.postCollectionReport(collectionId, requestModel.toDto())
}.map {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ fun CollectionDetailRoute(
navigateUp: () -> Unit,
navigateUpWithDeleteSuccess: () -> Unit,
navigateToCollectionEdit: (collectionId: String) -> Unit,
navigateToCollectionReport: (collectionId: String) -> Unit,
targetImageUrl: String? = null,
showEditSuccessToast: Boolean = false,
viewModel: CollectionDetailViewModel = hiltViewModel(),
Expand Down Expand Up @@ -114,7 +115,7 @@ fun CollectionDetailRoute(
showDeleteModal = true
},
onReportClick = {
// TODO: 신고(Route.CollectionReport) 화면 복구 후 navigateToCollectionReport(collectionDetail.id) 연결
navigateToCollectionReport(collectionDetail.id)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.flint.core.navigation.Route
import com.flint.core.navigation.model.CollectionListRouteType
import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit
import com.flint.presentation.collectiondetail.CollectionDetailRoute
import com.flint.presentation.collectiondetail.report.navigation.navigateToCollectionReport

fun NavController.navigateToCollectionDetail(
collectionId: String,
Expand All @@ -34,6 +35,7 @@ fun NavGraphBuilder.collectionDetailNavGraph(
navigateToCollectionList: (CollectionListRouteType) -> Unit,
navigateUp: () -> Unit,
navigateToProfile: (userId: String) -> Unit,
navigateToCollectionReport: (collectionId: String) -> Unit,
navController: NavController,
) {
composable<Route.CollectionDetail> { backStackEntry ->
Expand All @@ -54,6 +56,7 @@ fun NavGraphBuilder.collectionDetailNavGraph(
navigateToCollectionEdit = { collectionId ->
navController.navigateToCollectionEdit(collectionId)
},
navigateToCollectionReport = navigateToCollectionReport,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.flint.presentation.collectiondetail.report

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flint.core.designsystem.component.toast.ShowToast
import com.flint.core.designsystem.theme.FlintTheme
import com.flint.presentation.collectiondetail.report.component.ReportBottomSection
import com.flint.presentation.collectiondetail.report.component.ReportCheck
import com.flint.presentation.collectiondetail.report.component.ReportTopAppBar

@Composable
fun CollectionReportRoute(
paddingValues: PaddingValues,
navigateUp: () -> Unit,
navigateUpWithSuccess: () -> Unit,
viewModel: CollectionReportViewModel = hiltViewModel(),
) {
val uiState: CollectionReportUiState by viewModel.uiState.collectAsStateWithLifecycle()
var showFailureToast by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
viewModel.sideEffect.collect { event: CollectionReportSideEffect ->
when (event) {
CollectionReportSideEffect.ReportSuccess -> navigateUpWithSuccess()
CollectionReportSideEffect.ReportFailure -> showFailureToast = true
}
}
}

CollectionReportScreen(
paddingValues = paddingValues,
isLoading = uiState.isLoading,
selectedReportReason = uiState.selectedReportReason,
reportText = uiState.reportText,
onReportReasonSelected = viewModel::selectReportReason,
onReportTextChanged = viewModel::updateReportText,
onCancelClick = navigateUp,
onSubmitClick = viewModel::submitReport,
)

if (showFailureToast) {
ShowToast(
text = "신고 접수에 실패했어요. 다시 시도해주세요.",
imageVector = null,
paddingValues = paddingValues,
yOffset = 12.dp,
hide = { showFailureToast = false },
)
}
}

@Composable
private fun CollectionReportScreen(
paddingValues: PaddingValues,
isLoading: Boolean,
selectedReportReason: ReportReason?,
reportText: String,
onReportReasonSelected: (ReportReason) -> Unit,
onReportTextChanged: (String) -> Unit,
onCancelClick: () -> Unit,
onSubmitClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(FlintTheme.colors.background)
.padding(paddingValues),
) {
ReportTopAppBar(
onCancelClick = onCancelClick
)

LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
item {
Column {
Spacer(modifier = Modifier.height(12.dp))

Text(
text = "이 컬렉션을 신고하시겠어요?",
style = FlintTheme.typography.head1Sb22,
color = FlintTheme.colors.white,
)

Spacer(modifier = Modifier.height(8.dp))

Text(
text = "신고해주신 내용은 검토를 통해 반영됩니다.",
style = FlintTheme.typography.body1M16,
color = FlintTheme.colors.white,
)

Spacer(modifier = Modifier.height(20.dp))
}
}

item {
ReportCheck(
selectedReportReason = selectedReportReason,
onReportReasonSelected = onReportReasonSelected,
reportText = reportText,
onReportTextChanged = onReportTextChanged,
)
}
}

val isEnabled = !isLoading && when (selectedReportReason) {
null -> false
ReportReason.OTHER -> reportText.trim().length >= 2
else -> true
}

ReportBottomSection(
isEnabled = isEnabled,
onSubmitClick = onSubmitClick,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}

@Preview(showBackground = true)
@Composable
private fun CollectionReportScreenPreview() {
FlintTheme {
var selectedReportReason: ReportReason? by remember { mutableStateOf(null) }
var reportText: String by remember { mutableStateOf("") }

CollectionReportScreen(
paddingValues = PaddingValues(),
isLoading = false,
selectedReportReason = selectedReportReason,
reportText = reportText,
onReportReasonSelected = { selectedReportReason = it },
onReportTextChanged = { reportText = it },
onCancelClick = {},
onSubmitClick = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flint.presentation.collectiondetail.report

data class CollectionReportUiState(
val selectedReportReason: ReportReason? = null,
val reportText: String = "",
val isLoading: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.flint.presentation.collectiondetail.report

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.flint.core.navigation.Route
import com.flint.domain.model.collection.CollectionReportRequestModel
import com.flint.domain.repository.CollectionRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

sealed interface CollectionReportSideEffect {
data object ReportSuccess : CollectionReportSideEffect

data object ReportFailure : CollectionReportSideEffect
}

@HiltViewModel
class CollectionReportViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val collectionRepository: CollectionRepository,
) : ViewModel() {
private val collectionId: String =
savedStateHandle.toRoute<Route.CollectionReport>().collectionId

private val _uiState: MutableStateFlow<CollectionReportUiState> =
MutableStateFlow(CollectionReportUiState())
val uiState: StateFlow<CollectionReportUiState> = _uiState.asStateFlow()

private val _sideEffect: MutableSharedFlow<CollectionReportSideEffect> = MutableSharedFlow()
val sideEffect: SharedFlow<CollectionReportSideEffect> = _sideEffect.asSharedFlow()

fun selectReportReason(reason: ReportReason) {
_uiState.update {
it.copy(
selectedReportReason = reason,
reportText = if (reason == ReportReason.OTHER) it.reportText else "",
)
}
}

fun updateReportText(text: String) {
_uiState.update { it.copy(reportText = text) }
}

fun submitReport() {
val state = _uiState.value
if (state.isLoading) return

val reasonCode = state.selectedReportReason?.code ?: return

viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }

val requestModel = CollectionReportRequestModel(
reasons = listOf(reasonCode),
otherDetail = state.reportText.ifBlank { null },
)

collectionRepository.postCollectionReport(collectionId, requestModel)
.onSuccess {
_sideEffect.emit(CollectionReportSideEffect.ReportSuccess)
}
.onFailure {
_sideEffect.emit(CollectionReportSideEffect.ReportFailure)
}

_uiState.update { it.copy(isLoading = false) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flint.presentation.collectiondetail.report

enum class ReportReason(val displayText: String, val code: String) {
ABUSE("욕설·혐오 표현이 포함된 콘텐츠", "ABUSE"),
OBSCENE("음란하거나 선정적인 콘텐츠", "OBSCENE"),
SPAM("광고·홍보 또는 스팸성 콘텐츠", "SPAM"),
COPYRIGHT("저작권을 침해한 콘텐츠", "COPYRIGHT"),
OTHER("기타", "OTHER"),
}
Loading
Loading