diff --git a/app/src/main/java/com/flint/data/api/CollectionApi.kt b/app/src/main/java/com/flint/data/api/CollectionApi.kt index ce09beea..f30210ac 100644 --- a/app/src/main/java/com/flint/data/api/CollectionApi.kt +++ b/app/src/main/java/com/flint/data/api/CollectionApi.kt @@ -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 @@ -51,4 +52,11 @@ interface CollectionApi { // 최근 본 컬렉션 목록 조회 @GET("/api/v1/collections/recent") suspend fun getRecentCollectionList(): BaseResponse + + // 컬렉션 신고 + @POST("/api/v1/collections/{collectionId}/reports") + suspend fun postCollectionReport( + @Path("collectionId") collectionId: String, + @Body requestDto: CollectionReportRequestDto, + ): BaseEmptyResponse } diff --git a/app/src/main/java/com/flint/data/dto/collection/request/CollectionReportRequestDto.kt b/app/src/main/java/com/flint/data/dto/collection/request/CollectionReportRequestDto.kt new file mode 100644 index 00000000..349feda0 --- /dev/null +++ b/app/src/main/java/com/flint/data/dto/collection/request/CollectionReportRequestDto.kt @@ -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, + @SerialName("otherDetail") + val otherDetail: String? = null, +) diff --git a/app/src/main/java/com/flint/domain/mapper/collection/CollectionReportMapper.kt b/app/src/main/java/com/flint/domain/mapper/collection/CollectionReportMapper.kt new file mode 100644 index 00000000..1da731ad --- /dev/null +++ b/app/src/main/java/com/flint/domain/mapper/collection/CollectionReportMapper.kt @@ -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, + ) diff --git a/app/src/main/java/com/flint/domain/model/collection/CollectionReportModel.kt b/app/src/main/java/com/flint/domain/model/collection/CollectionReportModel.kt new file mode 100644 index 00000000..96cd7d75 --- /dev/null +++ b/app/src/main/java/com/flint/domain/model/collection/CollectionReportModel.kt @@ -0,0 +1,6 @@ +package com.flint.domain.model.collection + +data class CollectionReportRequestModel( + val reasons: List, + val otherDetail: String? = null, +) diff --git a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt index 2a0e315c..e32623d2 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -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 @@ -61,4 +63,13 @@ class CollectionRepository @Inject constructor( // 최근 본 컬렉션 목록 조회 suspend fun getRecentCollectionList(): Result = suspendRunCatching { apiService.getRecentCollectionList().data.toModel() } + + // 컬렉션 신고 + suspend fun postCollectionReport( + collectionId: String, + requestModel: CollectionReportRequestModel, + ): Result = + suspendRunCatching { + apiService.postCollectionReport(collectionId, requestModel.toDto()) + }.map {} } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 94e59db4..d6428046 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -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(), @@ -114,7 +115,7 @@ fun CollectionDetailRoute( showDeleteModal = true }, onReportClick = { - // TODO: 신고(Route.CollectionReport) 화면 복구 후 navigateToCollectionReport(collectionDetail.id) 연결 + navigateToCollectionReport(collectionDetail.id) }, ) } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt index ab328e11..1f58e441 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt @@ -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, @@ -34,6 +35,7 @@ fun NavGraphBuilder.collectionDetailNavGraph( navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateUp: () -> Unit, navigateToProfile: (userId: String) -> Unit, + navigateToCollectionReport: (collectionId: String) -> Unit, navController: NavController, ) { composable { backStackEntry -> @@ -54,6 +56,7 @@ fun NavGraphBuilder.collectionDetailNavGraph( navigateToCollectionEdit = { collectionId -> navController.navigateToCollectionEdit(collectionId) }, + navigateToCollectionReport = navigateToCollectionReport, ) } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt new file mode 100644 index 00000000..882e062a --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -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 = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt new file mode 100644 index 00000000..9a1e6e81 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt @@ -0,0 +1,7 @@ +package com.flint.presentation.collectiondetail.report + +data class CollectionReportUiState( + val selectedReportReason: ReportReason? = null, + val reportText: String = "", + val isLoading: Boolean = false, +) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt new file mode 100644 index 00000000..349d24ec --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt @@ -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().collectionId + + private val _uiState: MutableStateFlow = + MutableStateFlow(CollectionReportUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect: SharedFlow = _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) } + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/ReportReason.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/ReportReason.kt new file mode 100644 index 00000000..304298d0 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/ReportReason.kt @@ -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"), +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportBottomSection.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportBottomSection.kt new file mode 100644 index 00000000..529f7718 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportBottomSection.kt @@ -0,0 +1,121 @@ +package com.flint.presentation.collectiondetail.report.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.button.FlintButtonState +import com.flint.core.designsystem.component.button.FlintLargeButton +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun ReportBottomSection( + isEnabled: Boolean, + onSubmitClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + ReportInformationSection() + + Spacer(modifier = Modifier.height(18.dp)) + + FlintLargeButton( + text = "제출", + state = if (isEnabled) FlintButtonState.Able else FlintButtonState.Disable, + onClick = onSubmitClick, + enabled = isEnabled, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } +} + +@Composable +private fun ReportInformationSection( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(color = FlintTheme.colors.gray800, shape = RoundedCornerShape(12.dp)) + .padding(vertical = 14.dp, horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + ReportInformationItem( + content = "신고 전에 해당 콘텐츠가 신고 대상에 해당하는지 한 번 더 확인해 주세요." + ) + ReportInformationItem( + content = "신고된 콘텐츠는 플린트 이용 약관 및 운영 정책에 따라 검토되며, 필요 시 활동 제한 등의 조치가 적용될 수 있습니다." + ) + } +} + +@Composable +private fun ReportInformationItem( + content: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_small_info), + contentDescription = null, + tint = FlintTheme.colors.gray300, + ) + Text( + text = content, + style = FlintTheme.typography.caption1M12, + color = FlintTheme.colors.gray300, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportBottomSectionEnabledPreview() { + FlintTheme { + ReportBottomSection( + isEnabled = true, + onSubmitClick = {}, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportBottomSectionDisabledPreview() { + FlintTheme { + ReportBottomSection( + isEnabled = false, + onSubmitClick = {}, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportInformationSectionPreview() { + FlintTheme { + ReportInformationSection() + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt new file mode 100644 index 00000000..fc6a6aab --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt @@ -0,0 +1,118 @@ +package com.flint.presentation.collectiondetail.report.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.component.textfield.CollectionInputTextField +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.presentation.collectiondetail.report.ReportReason + +@Composable +fun ReportCheck( + selectedReportReason: ReportReason?, + onReportReasonSelected: (ReportReason) -> Unit, + reportText: String, + onReportTextChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + ) { + ReportReason.entries.forEach { reason -> + ReportCheckItem( + isChecked = selectedReportReason == reason, + onCheckClick = { onReportReasonSelected(reason) }, + reportContent = reason.displayText, + ) + } + CollectionInputTextField( + value = reportText, + onValueChanged = onReportTextChanged, + maxLength = 200, + placeholder = "신고 사유를 작성해주세요.", + singleLine = false, + maxLines = Int.MAX_VALUE, + isShowLengthTitle = true, + modifier = Modifier + .padding(horizontal = 12.dp) + .heightIn(min = 104.dp) + .onFocusChanged { + if (it.hasFocus) onReportReasonSelected(ReportReason.OTHER) + } + ) + } +} + +@Composable +private fun ReportCheckItem( + isChecked: Boolean, + onCheckClick: () -> Unit, + reportContent: String, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onCheckClick), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = ImageVector.vectorResource(if (isChecked) R.drawable.ic_check_fill else R.drawable.ic_check_empty), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + text = reportContent, + style = FlintTheme.typography.body1M16, + color = FlintTheme.colors.white, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportCheckPreview() { + FlintTheme { + var selectedReportReason by remember { mutableStateOf(null) } + var reportText by remember { mutableStateOf("") } + + ReportCheck( + selectedReportReason = selectedReportReason, + onReportReasonSelected = { selectedReportReason = it }, + reportText = reportText, + onReportTextChanged = { reportText = it }, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportCheckItemPreview() { + FlintTheme { + ReportCheckItem( + isChecked = false, + onCheckClick = {}, + reportContent = "욕설·혐오 표현이 포함된 콘텐츠", + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportTopAppBar.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportTopAppBar.kt new file mode 100644 index 00000000..c839bdfe --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportTopAppBar.kt @@ -0,0 +1,62 @@ +package com.flint.presentation.collectiondetail.report.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun ReportTopAppBar( + onCancelClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + ) { + Text( + text = "신고", + modifier = Modifier.align(Alignment.Center), + style = FlintTheme.typography.body1M16, + color = FlintTheme.colors.white, + ) + + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .noRippleClickable(onClick = onCancelClick) + .size(48.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), + contentDescription = "신고 닫기", + tint = FlintTheme.colors.white, + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportTopAppBarPreview() { + FlintTheme { + ReportTopAppBar( + onCancelClick = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/report/navigation/CollectionReportNavigation.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/navigation/CollectionReportNavigation.kt new file mode 100644 index 00000000..854ad259 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/navigation/CollectionReportNavigation.kt @@ -0,0 +1,37 @@ +package com.flint.presentation.collectiondetail.report.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.flint.core.navigation.Route +import com.flint.presentation.collectiondetail.report.CollectionReportRoute + +const val KEY_SHOW_REPORT_SUCCESS_TOAST = "showReportSuccessToast" + +fun NavController.navigateToCollectionReport( + collectionId: String, + navOptions: NavOptions? = null, +) { + navigate(Route.CollectionReport(collectionId = collectionId), navOptions) +} + +fun NavGraphBuilder.collectionReportNavGraph( + paddingValues: PaddingValues, + navController: NavController, + navigateUp: () -> Unit, +) { + composable { + CollectionReportRoute( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateUpWithSuccess = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(KEY_SHOW_REPORT_SUCCESS_TOAST, true) + navController.navigateUp() + }, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt index 8c86c6ce..23be7758 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt @@ -15,6 +15,8 @@ import com.flint.core.designsystem.theme.FlintTheme import com.flint.presentation.collectioncreate.navigation.collectionCreateNavGraph import com.flint.presentation.collectiondetail.navigation.KEY_SHOW_DELETE_SUCCESS_TOAST import com.flint.presentation.collectiondetail.navigation.collectionDetailNavGraph +import com.flint.presentation.collectiondetail.report.navigation.KEY_SHOW_REPORT_SUCCESS_TOAST +import com.flint.presentation.collectiondetail.report.navigation.collectionReportNavGraph import com.flint.presentation.collectionlist.navigation.collectionListNavGraph import com.flint.presentation.explore.navigation.exploreNavGraph import com.flint.presentation.home.navigation.homeNavGraph @@ -39,6 +41,9 @@ fun MainNavHost( val showDeleteSuccessToast = currentBackStackEntry ?.savedStateHandle ?.get(KEY_SHOW_DELETE_SUCCESS_TOAST) ?: false + val showReportSuccessToast = currentBackStackEntry + ?.savedStateHandle + ?.get(KEY_SHOW_REPORT_SUCCESS_TOAST) ?: false Box( modifier = @@ -88,7 +93,14 @@ fun MainNavHost( navigateToCollectionList = navigator::navigateToCollectionList, navigateUp = navigator::navigateUp, navigateToProfile = navigator::navigateToProfile, + navigateToCollectionReport = navigator::navigateToCollectionReport, + navController = navigator.navController, + ) + + collectionReportNavGraph( + paddingValues = paddingValues, navController = navigator.navController, + navigateUp = navigator::navigateUp, ) collectionCreateNavGraph( @@ -154,5 +166,17 @@ fun MainNavHost( }, ) } + + if (showReportSuccessToast) { + ShowToast( + text = "신고가 접수되었어요", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { + currentBackStackEntry?.savedStateHandle?.set(KEY_SHOW_REPORT_SUCCESS_TOAST, false) + }, + ) + } } } diff --git a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt index 0ca0d58c..91c1eaad 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -15,6 +15,7 @@ import com.flint.core.navigation.model.CollectionListRouteType import com.flint.presentation.collectioncreate.navigation.navigateToCollectionCreate import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit import com.flint.presentation.collectiondetail.navigation.navigateToCollectionDetail +import com.flint.presentation.collectiondetail.report.navigation.navigateToCollectionReport import com.flint.presentation.collectionlist.navigation.navigateToCollectionList import com.flint.presentation.explore.navigation.navigateToExplore import com.flint.presentation.home.navigation.navigateToHome @@ -156,6 +157,10 @@ class MainNavigator( navController.navigateToCollectionEdit(collectionId, navOptions) } + fun navigateToCollectionReport(collectionId: String, navOptions: NavOptions? = null) { + navController.navigateToCollectionReport(collectionId, navOptions) + } + fun navigateToSavedContent(navOptions: NavOptions? = null) { navController.navigateToSavedContentList(navOptions) }