From ddaa476c5d30e215ed64d94f0c5a4f5e6b8aa3e8 Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 04:16:43 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[feat]=20ReportCheck=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/ReportCheck.kt | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt 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..a350f146 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt @@ -0,0 +1,140 @@ +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 + +@Composable +fun ReportCheck( + selectedReportReason: String?, + onReportReasonSelected: (String) -> Unit, + reportText: String, + onReportTextChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + ) { + ReportCheckItem( + isChecked = selectedReportReason == "욕설·혐오 표현이 포함된 콘텐츠", + onCheckClick = { onReportReasonSelected("욕설·혐오 표현이 포함된 콘텐츠") }, + reportContent = "욕설·혐오 표현이 포함된 콘텐츠", + ) + + ReportCheckItem( + isChecked = selectedReportReason == "음란하거나 선정적인 콘텐츠", + onCheckClick = { onReportReasonSelected("음란하거나 선정적인 콘텐츠") }, + reportContent = "음란하거나 선정적인 콘텐츠", + ) + + ReportCheckItem( + isChecked = selectedReportReason == "광고·홍보 또는 스팸성 콘텐츠", + onCheckClick = { onReportReasonSelected("광고·홍보 또는 스팸성 콘텐츠") }, + reportContent = "광고·홍보 또는 스팸성 콘텐츠", + ) + + ReportCheckItem( + isChecked = selectedReportReason == "저작권을 침해한 콘텐츠", + onCheckClick = { onReportReasonSelected("저작권을 침해한 콘텐츠") }, + reportContent = "저작권을 침해한 콘텐츠", + ) + + ReportCheckItem( + isChecked = selectedReportReason == "기타", + onCheckClick = { onReportReasonSelected("기타") }, + reportContent = "기타", + ) + 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("기타") + } + ) + } +} + +@Composable +private fun ReportCheckItem( + isChecked: Boolean, + onCheckClick: () -> Unit, + reportContent: String, +) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .noRippleClickable(onClick = onCheckClick) + .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 = "욕설·혐오 표현이 포함된 콘텐츠", + ) + } +} From 38aef78edc1bb91e854121cdcfc8e0957d15f44c Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 04:17:01 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[feat]=20ReportBottomSection.kt=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/ReportBottomSection.kt | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportBottomSection.kt 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() + } +} From f6395b7f933e7a90d2b99a21938c7e4c6f4d8dab Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 04:17:15 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[feat]=20ReportTopAppBar.kt=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/ReportTopAppBar.kt | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportTopAppBar.kt 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..53a483df --- /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 = null, + tint = FlintTheme.colors.white, + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun ReportTopAppBarPreview() { + FlintTheme { + ReportTopAppBar( + onCancelClick = {}, + ) + } +} From 0381058c15553aaf28831a520bdd395db78eb20c Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 04:17:44 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[feat]=20CollectionReport=20Screen=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/CollectionReportScreen.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt 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..c70fd22d --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -0,0 +1,125 @@ +package com.flint.presentation.collectiondetail.report + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.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.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( + navigateUp: () -> Unit, + viewModel: CollectionReportViewModel = hiltViewModel(), +) { + val uiState: CollectionReportUiState by viewModel.uiState.collectAsStateWithLifecycle() + + CollectionReportScreen( + selectedReportReason = uiState.selectedReportReason, + reportText = uiState.reportText, + onReportReasonSelected = viewModel::selectReportReason, + onReportTextChanged = viewModel::updateReportText, + onCancelClick = navigateUp, + onSubmitClick = {}, + ) +} + +@Composable +private fun CollectionReportScreen( + selectedReportReason: String?, + reportText: String, + onReportReasonSelected: (String) -> Unit, + onReportTextChanged: (String) -> Unit, + onCancelClick: () -> Unit, + onSubmitClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(FlintTheme.colors.background), + ) { + 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, + ) + } + } + + ReportBottomSection( + isEnabled = selectedReportReason != null, + onSubmitClick = onSubmitClick, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun CollectionReportScreenPreview() { + FlintTheme { + var selectedReportReason: String? by remember { mutableStateOf(null) } + var reportText: String by remember { mutableStateOf("") } + + CollectionReportScreen( + selectedReportReason = selectedReportReason, + reportText = reportText, + onReportReasonSelected = { selectedReportReason = it }, + onReportTextChanged = { reportText = it }, + onCancelClick = {}, + onSubmitClick = {}, + ) + } +} From 26cc108576ad1856b8fdacbd85ae441028f0035a Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 04:18:00 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[feat]=20CollectionReport=20uiState=20&=20V?= =?UTF-8?q?iewmodel=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/CollectionReportUiState.kt | 6 ++++ .../report/CollectionReportViewModel.kt | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt 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..b83c5456 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt @@ -0,0 +1,6 @@ +package com.flint.presentation.collectiondetail.report + +data class CollectionReportUiState( + val selectedReportReason: String? = null, + val reportText: String = "", +) 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..eeaa2e32 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt @@ -0,0 +1,33 @@ +package com.flint.presentation.collectiondetail.report + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class CollectionReportViewModel @Inject constructor() : ViewModel() { + private val _uiState: MutableStateFlow = + MutableStateFlow(CollectionReportUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun selectReportReason(reason: String) { + _uiState.update { + it.copy( + selectedReportReason = reason, + reportText = if (reason == ETC_REASON) it.reportText else "", + ) + } + } + + fun updateReportText(text: String) { + _uiState.update { it.copy(reportText = text) } + } + + companion object { + private const val ETC_REASON = "기타" + } +} From 93d1b90e0d158563fc14b832b11d4e84a8073291 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 02:09:14 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[feat]=20CollectionReport=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/flint/data/api/CollectionApi.kt | 9 +++ .../request/CollectionReportRequestDto.kt | 12 ++++ .../collection/CollectionReportMapper.kt | 10 +++ .../model/collection/CollectionReportModel.kt | 6 ++ .../domain/repository/CollectionRepository.kt | 10 +++ .../report/CollectionReportScreen.kt | 22 ++++++- .../report/CollectionReportUiState.kt | 1 + .../report/CollectionReportViewModel.kt | 62 ++++++++++++++++++- .../navigation/CollectionReportNavigation.kt | 25 ++++++++ 9 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/flint/data/dto/collection/request/CollectionReportRequestDto.kt create mode 100644 app/src/main/java/com/flint/domain/mapper/collection/CollectionReportMapper.kt create mode 100644 app/src/main/java/com/flint/domain/model/collection/CollectionReportModel.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/navigation/CollectionReportNavigation.kt 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 47bb9bba..da994dee 100644 --- a/app/src/main/java/com/flint/data/api/CollectionApi.kt +++ b/app/src/main/java/com/flint/data/api/CollectionApi.kt @@ -1,7 +1,9 @@ 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 @@ -35,4 +37,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 46c0ce2c..861cb066 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -3,6 +3,7 @@ package com.flint.domain.repository 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.request.CollectionReportRequestDto import com.flint.data.dto.collection.response.CollectionDetailResponseDto import com.flint.domain.mapper.collection.toModel import com.flint.domain.model.collection.CollectionCreateModel @@ -44,4 +45,13 @@ class CollectionRepository @Inject constructor( // 최근 본 컬렉션 목록 조회 suspend fun getRecentCollectionList(): Result = suspendRunCatching { apiService.getRecentCollectionList().data.toModel() } + + // 컬렉션 신고 + suspend fun postCollectionReport( + collectionId: String, + requestDto: CollectionReportRequestDto, + ): Result = + suspendRunCatching { + apiService.postCollectionReport(collectionId, requestDto) + }.map {} } 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 index c70fd22d..01134f53 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -10,6 +10,7 @@ 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 @@ -31,13 +32,24 @@ fun CollectionReportRoute( ) { val uiState: CollectionReportUiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { event: CollectionReportSideEffect -> + when (event) { + CollectionReportSideEffect.ReportSuccess -> navigateUp() + CollectionReportSideEffect.ReportFailure -> { + // TODO: 신고 접수 실패 토스트/다이얼로그 띄우기 + } + } + } + } + CollectionReportScreen( selectedReportReason = uiState.selectedReportReason, reportText = uiState.reportText, onReportReasonSelected = viewModel::selectReportReason, onReportTextChanged = viewModel::updateReportText, onCancelClick = navigateUp, - onSubmitClick = {}, + onSubmitClick = viewModel::submitReport, ) } @@ -98,8 +110,14 @@ private fun CollectionReportScreen( } } + val isEnabled = when (selectedReportReason) { + null -> false + "기타" -> reportText.trim().length >= 2 + else -> true + } + ReportBottomSection( - isEnabled = selectedReportReason != null, + isEnabled = isEnabled, onSubmitClick = onSubmitClick, modifier = Modifier.padding(horizontal = 16.dp) ) 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 index b83c5456..4b39c2e0 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt @@ -3,4 +3,5 @@ package com.flint.presentation.collectiondetail.report data class CollectionReportUiState( val selectedReportReason: String? = 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 index eeaa2e32..c2c064f4 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt @@ -1,19 +1,45 @@ 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.mapper.collection.toDto +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() : ViewModel() { +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: String) { _uiState.update { it.copy( @@ -27,7 +53,41 @@ class CollectionReportViewModel @Inject constructor() : ViewModel() { _uiState.update { it.copy(reportText = text) } } + fun submitReport() { + val state = _uiState.value + if (state.isLoading) return + + val reasonCode = REASON_CODE_MAP[state.selectedReportReason] ?: return + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + val requestModel = CollectionReportRequestModel( + reasons = listOf(reasonCode), + otherDetail = state.reportText.ifBlank { null }, + ) + + collectionRepository.postCollectionReport(collectionId, requestModel.toDto()) + .onSuccess { + _sideEffect.emit(CollectionReportSideEffect.ReportSuccess) + } + .onFailure { + _sideEffect.emit(CollectionReportSideEffect.ReportFailure) + } + + _uiState.update { it.copy(isLoading = false) } + } + } + companion object { private const val ETC_REASON = "기타" + + private val REASON_CODE_MAP: Map = mapOf( + "욕설·혐오 표현이 포함된 콘텐츠" to "ABUSE", + "음란하거나 선정적인 콘텐츠" to "OBSCENE", + "광고·홍보 또는 스팸성 콘텐츠" to "SPAM", + "저작권을 침해한 콘텐츠" to "COPYRIGHT", + ETC_REASON to "OTHER", + ) } } 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..06882cb2 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/navigation/CollectionReportNavigation.kt @@ -0,0 +1,25 @@ +package com.flint.presentation.collectiondetail.report.navigation + +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 + +fun NavController.navigateToCollectionReport( + collectionId: String, + navOptions: NavOptions? = null, +) { + navigate(Route.CollectionReport(collectionId = collectionId), navOptions) +} + +fun NavGraphBuilder.collectionReportNavGraph( + navigateUp: () -> Unit, +) { + composable { + CollectionReportRoute( + navigateUp = navigateUp, + ) + } +} From db729d6b436338590758b1e1ce764e78ccfb670f Mon Sep 17 00:00:00 2001 From: chanmi Date: Thu, 18 Jun 2026 00:22:38 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[feat]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 3 ++- .../navigation/CollectionDetailNavigation.kt | 3 +++ .../report/CollectionReportScreen.kt | 11 +++++++-- .../navigation/CollectionReportNavigation.kt | 12 ++++++++++ .../flint/presentation/main/MainNavHost.kt | 24 +++++++++++++++++++ .../flint/presentation/main/MainNavigator.kt | 5 ++++ 6 files changed, 55 insertions(+), 3 deletions(-) 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 index 01134f53..400b9dc6 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -2,6 +2,7 @@ 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 @@ -27,7 +28,9 @@ 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() @@ -35,7 +38,7 @@ fun CollectionReportRoute( LaunchedEffect(Unit) { viewModel.sideEffect.collect { event: CollectionReportSideEffect -> when (event) { - CollectionReportSideEffect.ReportSuccess -> navigateUp() + CollectionReportSideEffect.ReportSuccess -> navigateUpWithSuccess() CollectionReportSideEffect.ReportFailure -> { // TODO: 신고 접수 실패 토스트/다이얼로그 띄우기 } @@ -44,6 +47,7 @@ fun CollectionReportRoute( } CollectionReportScreen( + paddingValues = paddingValues, selectedReportReason = uiState.selectedReportReason, reportText = uiState.reportText, onReportReasonSelected = viewModel::selectReportReason, @@ -55,6 +59,7 @@ fun CollectionReportRoute( @Composable private fun CollectionReportScreen( + paddingValues: PaddingValues, selectedReportReason: String?, reportText: String, onReportReasonSelected: (String) -> Unit, @@ -66,7 +71,8 @@ private fun CollectionReportScreen( Column( modifier = modifier .fillMaxSize() - .background(FlintTheme.colors.background), + .background(FlintTheme.colors.background) + .padding(paddingValues), ) { ReportTopAppBar( onCancelClick = onCancelClick @@ -132,6 +138,7 @@ private fun CollectionReportScreenPreview() { var reportText: String by remember { mutableStateOf("") } CollectionReportScreen( + paddingValues = PaddingValues(), selectedReportReason = selectedReportReason, reportText = reportText, onReportReasonSelected = { selectedReportReason = it }, 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 index 06882cb2..854ad259 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -7,6 +8,8 @@ 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, @@ -15,11 +18,20 @@ fun NavController.navigateToCollectionReport( } 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) } From 3f6b3a3d97df086452861c5b8b8906d862af16ba Mon Sep 17 00:00:00 2001 From: chanmi Date: Thu, 18 Jun 2026 15:36:16 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=EC=BD=94=EB=A6=AC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/CollectionRepository.kt | 7 +-- .../report/CollectionReportScreen.kt | 13 +++--- .../report/CollectionReportUiState.kt | 2 +- .../report/CollectionReportViewModel.kt | 21 ++------- .../collectiondetail/report/ReportReason.kt | 9 ++++ .../report/component/ReportCheck.kt | 45 +++++-------------- 6 files changed, 38 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/report/ReportReason.kt 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 2acb8f3a..e32623d2 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -3,12 +3,13 @@ package com.flint.domain.repository 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.request.CollectionReportRequestDto 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 @@ -66,9 +67,9 @@ class CollectionRepository @Inject constructor( // 컬렉션 신고 suspend fun postCollectionReport( collectionId: String, - requestDto: CollectionReportRequestDto, + requestModel: CollectionReportRequestModel, ): Result = suspendRunCatching { - apiService.postCollectionReport(collectionId, requestDto) + apiService.postCollectionReport(collectionId, requestModel.toDto()) }.map {} } 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 index 400b9dc6..ba3720bc 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -48,6 +48,7 @@ fun CollectionReportRoute( CollectionReportScreen( paddingValues = paddingValues, + isLoading = uiState.isLoading, selectedReportReason = uiState.selectedReportReason, reportText = uiState.reportText, onReportReasonSelected = viewModel::selectReportReason, @@ -60,9 +61,10 @@ fun CollectionReportRoute( @Composable private fun CollectionReportScreen( paddingValues: PaddingValues, - selectedReportReason: String?, + isLoading: Boolean, + selectedReportReason: ReportReason?, reportText: String, - onReportReasonSelected: (String) -> Unit, + onReportReasonSelected: (ReportReason) -> Unit, onReportTextChanged: (String) -> Unit, onCancelClick: () -> Unit, onSubmitClick: () -> Unit, @@ -116,9 +118,9 @@ private fun CollectionReportScreen( } } - val isEnabled = when (selectedReportReason) { + val isEnabled = !isLoading && when (selectedReportReason) { null -> false - "기타" -> reportText.trim().length >= 2 + ReportReason.OTHER -> reportText.trim().length >= 2 else -> true } @@ -134,11 +136,12 @@ private fun CollectionReportScreen( @Composable private fun CollectionReportScreenPreview() { FlintTheme { - var selectedReportReason: String? by remember { mutableStateOf(null) } + 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 }, 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 index 4b39c2e0..9a1e6e81 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportUiState.kt @@ -1,7 +1,7 @@ package com.flint.presentation.collectiondetail.report data class CollectionReportUiState( - val selectedReportReason: String? = null, + 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 index c2c064f4..349d24ec 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.flint.core.navigation.Route -import com.flint.domain.mapper.collection.toDto import com.flint.domain.model.collection.CollectionReportRequestModel import com.flint.domain.repository.CollectionRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -40,11 +39,11 @@ class CollectionReportViewModel @Inject constructor( private val _sideEffect: MutableSharedFlow = MutableSharedFlow() val sideEffect: SharedFlow = _sideEffect.asSharedFlow() - fun selectReportReason(reason: String) { + fun selectReportReason(reason: ReportReason) { _uiState.update { it.copy( selectedReportReason = reason, - reportText = if (reason == ETC_REASON) it.reportText else "", + reportText = if (reason == ReportReason.OTHER) it.reportText else "", ) } } @@ -57,7 +56,7 @@ class CollectionReportViewModel @Inject constructor( val state = _uiState.value if (state.isLoading) return - val reasonCode = REASON_CODE_MAP[state.selectedReportReason] ?: return + val reasonCode = state.selectedReportReason?.code ?: return viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } @@ -67,7 +66,7 @@ class CollectionReportViewModel @Inject constructor( otherDetail = state.reportText.ifBlank { null }, ) - collectionRepository.postCollectionReport(collectionId, requestModel.toDto()) + collectionRepository.postCollectionReport(collectionId, requestModel) .onSuccess { _sideEffect.emit(CollectionReportSideEffect.ReportSuccess) } @@ -78,16 +77,4 @@ class CollectionReportViewModel @Inject constructor( _uiState.update { it.copy(isLoading = false) } } } - - companion object { - private const val ETC_REASON = "기타" - - private val REASON_CODE_MAP: Map = mapOf( - "욕설·혐오 표현이 포함된 콘텐츠" to "ABUSE", - "음란하거나 선정적인 콘텐츠" to "OBSCENE", - "광고·홍보 또는 스팸성 콘텐츠" to "SPAM", - "저작권을 침해한 콘텐츠" to "COPYRIGHT", - ETC_REASON to "OTHER", - ) - } } 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/ReportCheck.kt b/app/src/main/java/com/flint/presentation/collectiondetail/report/component/ReportCheck.kt index a350f146..27afc722 100644 --- 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 @@ -25,11 +25,12 @@ 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: String?, - onReportReasonSelected: (String) -> Unit, + selectedReportReason: ReportReason?, + onReportReasonSelected: (ReportReason) -> Unit, reportText: String, onReportTextChanged: (String) -> Unit, modifier: Modifier = Modifier @@ -37,35 +38,13 @@ fun ReportCheck( Column( modifier = modifier ) { - ReportCheckItem( - isChecked = selectedReportReason == "욕설·혐오 표현이 포함된 콘텐츠", - onCheckClick = { onReportReasonSelected("욕설·혐오 표현이 포함된 콘텐츠") }, - reportContent = "욕설·혐오 표현이 포함된 콘텐츠", - ) - - ReportCheckItem( - isChecked = selectedReportReason == "음란하거나 선정적인 콘텐츠", - onCheckClick = { onReportReasonSelected("음란하거나 선정적인 콘텐츠") }, - reportContent = "음란하거나 선정적인 콘텐츠", - ) - - ReportCheckItem( - isChecked = selectedReportReason == "광고·홍보 또는 스팸성 콘텐츠", - onCheckClick = { onReportReasonSelected("광고·홍보 또는 스팸성 콘텐츠") }, - reportContent = "광고·홍보 또는 스팸성 콘텐츠", - ) - - ReportCheckItem( - isChecked = selectedReportReason == "저작권을 침해한 콘텐츠", - onCheckClick = { onReportReasonSelected("저작권을 침해한 콘텐츠") }, - reportContent = "저작권을 침해한 콘텐츠", - ) - - ReportCheckItem( - isChecked = selectedReportReason == "기타", - onCheckClick = { onReportReasonSelected("기타") }, - reportContent = "기타", - ) + ReportReason.entries.forEach { reason -> + ReportCheckItem( + isChecked = selectedReportReason == reason, + onCheckClick = { onReportReasonSelected(reason) }, + reportContent = reason.displayText, + ) + } CollectionInputTextField( value = reportText, onValueChanged = onReportTextChanged, @@ -78,7 +57,7 @@ fun ReportCheck( .padding(horizontal = 12.dp) .heightIn(min = 104.dp) .onFocusChanged { - if (it.hasFocus) onReportReasonSelected("기타") + if (it.hasFocus) onReportReasonSelected(ReportReason.OTHER) } ) } @@ -115,7 +94,7 @@ private fun ReportCheckItem( @Composable private fun ReportCheckPreview() { FlintTheme { - var selectedReportReason by remember { mutableStateOf(null) } + var selectedReportReason by remember { mutableStateOf(null) } var reportText by remember { mutableStateOf("") } ReportCheck( From d94a58c71c0750e9b4f78696e913cefe60431e89 Mon Sep 17 00:00:00 2001 From: chanmi Date: Thu, 18 Jun 2026 15:52:02 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=EC=BD=94=EB=A6=AC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/CollectionReportScreen.kt | 16 +++++++++++++--- .../report/component/ReportCheck.kt | 7 +++---- .../report/component/ReportTopAppBar.kt | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) 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 index ba3720bc..882e062a 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/report/CollectionReportScreen.kt @@ -21,6 +21,7 @@ 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 @@ -34,14 +35,13 @@ fun CollectionReportRoute( 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 -> { - // TODO: 신고 접수 실패 토스트/다이얼로그 띄우기 - } + CollectionReportSideEffect.ReportFailure -> showFailureToast = true } } } @@ -56,6 +56,16 @@ fun CollectionReportRoute( onCancelClick = navigateUp, onSubmitClick = viewModel::submitReport, ) + + if (showFailureToast) { + ShowToast( + text = "신고 접수에 실패했어요. 다시 시도해주세요.", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { showFailureToast = false }, + ) + } } @Composable 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 index 27afc722..fc6a6aab 100644 --- 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 @@ -71,13 +71,12 @@ private fun ReportCheckItem( ) { Row( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .noRippleClickable(onClick = onCheckClick), verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier - .noRippleClickable(onClick = onCheckClick) - .size(48.dp), + 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, 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 index 53a483df..c839bdfe 100644 --- 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 @@ -44,7 +44,7 @@ fun ReportTopAppBar( Icon( modifier = Modifier.size(24.dp), imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), - contentDescription = null, + contentDescription = "신고 닫기", tint = FlintTheme.colors.white, ) }