Skip to content

Commit

Permalink
[FEAT/#137] 신고하기 페이지 신고하기 기능 및 API 구현
Browse files Browse the repository at this point in the history
Roel4990 committed Jan 23, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent f90da09 commit da401fd
Showing 18 changed files with 189 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.spoony.spoony.data.datasource

import com.spoony.spoony.data.dto.base.BaseResponse

interface ReportDataSource {
suspend fun postReportPost(postId: Int, userId: Int, reportType: String, reportDetail: String): BaseResponse<Boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.spoony.spoony.data.datasourceimpl

import com.spoony.spoony.data.datasource.ReportDataSource
import com.spoony.spoony.data.dto.base.BaseResponse
import com.spoony.spoony.data.dto.request.PostReportPostRequestDto
import com.spoony.spoony.data.service.ReportService
import javax.inject.Inject

class ReportDataSourceImpl @Inject constructor(
private val reportService: ReportService
) : ReportDataSource {
override suspend fun postReportPost(postId: Int, userId: Int, reportType: String, reportDetail: String): BaseResponse<Boolean> =
reportService.postReportPost(
PostReportPostRequestDto(postId = postId, userId = userId, reportType = reportType, reportDetail = reportDetail)
)
}
Original file line number Diff line number Diff line change
@@ -4,10 +4,12 @@ import com.spoony.spoony.data.datasource.CategoryDataSource
import com.spoony.spoony.data.datasource.DummyRemoteDataSource
import com.spoony.spoony.data.datasource.PlaceDataSource
import com.spoony.spoony.data.datasource.PostRemoteDataSource
import com.spoony.spoony.data.datasource.ReportDataSource
import com.spoony.spoony.data.datasourceimpl.CategoryDataSourceImpl
import com.spoony.spoony.data.datasourceimpl.DummyRemoteDataSourceImpl
import com.spoony.spoony.data.datasourceimpl.PlaceDataSourceImpl
import com.spoony.spoony.data.datasourceimpl.PostRemoteDataSourceImpl
import com.spoony.spoony.data.datasourceimpl.ReportDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -36,4 +38,10 @@ abstract class DataSourceModule {
abstract fun bindCategoryDataSource(
categoryDataSourceImpl: CategoryDataSourceImpl
): CategoryDataSource

@Binds
@Singleton
abstract fun bindReportDataSource(
reportDataSourceImpl: ReportDataSourceImpl
): ReportDataSource
}
Original file line number Diff line number Diff line change
@@ -6,12 +6,14 @@ import com.spoony.spoony.data.repositoryimpl.ExploreRepositoryImpl
import com.spoony.spoony.data.repositoryimpl.MapRepositoryImpl
import com.spoony.spoony.data.repositoryimpl.PostRepositoryImpl
import com.spoony.spoony.data.repositoryimpl.RegisterRepositoryImpl
import com.spoony.spoony.data.repositoryimpl.ReportRepositoryImpl
import com.spoony.spoony.domain.repository.CategoryRepository
import com.spoony.spoony.domain.repository.DummyRepository
import com.spoony.spoony.domain.repository.ExploreRepository
import com.spoony.spoony.domain.repository.MapRepository
import com.spoony.spoony.domain.repository.PostRepository
import com.spoony.spoony.domain.repository.RegisterRepository
import com.spoony.spoony.domain.repository.ReportRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -44,4 +46,8 @@ abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindCategoryRepository(categoryRepositoryImpl: CategoryRepositoryImpl): CategoryRepository

@Binds
@Singleton
abstract fun bindReportRepository(reportRepositoryImpl: ReportRepositoryImpl): ReportRepository
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/spoony/spoony/data/di/ServiceModule.kt
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import com.spoony.spoony.data.service.CategoryService
import com.spoony.spoony.data.service.DummyService
import com.spoony.spoony.data.service.PlaceService
import com.spoony.spoony.data.service.PostService
import com.spoony.spoony.data.service.ReportService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -33,4 +34,9 @@ object ServiceModule {
@Singleton
fun provideCategoryService(retrofit: Retrofit): CategoryService =
retrofit.create(CategoryService::class.java)

@Provides
@Singleton
fun provideReportService(retrofit: Retrofit): ReportService =
retrofit.create(ReportService::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.spoony.spoony.data.dto.request

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

@Serializable
data class PostReportPostRequestDto(
@SerialName("postId")
val postId: Int,
@SerialName("userId")
val userId: Int,
@SerialName("reportType")
val reportType: String,
@SerialName("reportDetail")
val reportDetail: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.spoony.spoony.data.repositoryimpl

import com.spoony.spoony.data.datasource.ReportDataSource
import com.spoony.spoony.domain.repository.ReportRepository
import javax.inject.Inject

class ReportRepositoryImpl @Inject constructor(
private val reportDataSource: ReportDataSource
) : ReportRepository {
override suspend fun postReportPost(postId: Int, userId: Int, reportType: String, reportDetail: String): Result<Boolean> =
runCatching {
reportDataSource.postReportPost(postId = postId, userId = userId, reportType = reportType, reportDetail = reportDetail).success
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/spoony/spoony/data/service/ReportService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.spoony.spoony.data.service

import com.spoony.spoony.data.dto.base.BaseResponse
import com.spoony.spoony.data.dto.request.PostReportPostRequestDto
import retrofit2.http.Body
import retrofit2.http.POST

interface ReportService {
@POST("/api/v1/report")
suspend fun postReportPost(
@Body postReportRequestDto: PostReportPostRequestDto
): BaseResponse<Boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.spoony.spoony.domain.repository

interface ReportRepository {
suspend fun postReportPost(postId: Int, userId: Int, reportType: String, reportDetail: String): Result<Boolean>
}
Original file line number Diff line number Diff line change
@@ -55,8 +55,12 @@ class MainNavigator(
currentDestination?.hasRoute(it::class) == true
}

fun navigateToReport(navOptions: NavOptions? = null) {
navController.navigateToReport(navOptions)
fun navigateToReport(
postId: Int,
userId: Int,
navOptions: NavOptions? = null
) {
navController.navigateToReport(postId = postId, userId = userId)
}

fun navigateToExplore(navOptions: NavOptions? = null) {
Original file line number Diff line number Diff line change
@@ -117,7 +117,12 @@ fun MainScreen(
placeDetailNavGraph(
paddingValues = paddingValues,
navigateUp = navigator::navigateUp,
navigateToReport = navigator::navigateToReport
navigateToReport = { postId, userId ->
navigator.navigateToReport(
postId = postId,
userId = userId
)
}
)

reportNavGraph(
Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ import kotlinx.coroutines.launch
@Composable
fun PlaceDetailRoute(
paddingValues: PaddingValues,
navigateToReport: () -> Unit,
navigateToReport: (postId: Int, userId: Int) -> Unit,
navigateUp: () -> Unit,
viewModel: PlaceDetailViewModel = hiltViewModel()
) {
@@ -185,7 +185,7 @@ fun PlaceDetailRoute(
placeName = data.placeName,
isScooped = state.isScooped,
dropdownMenuList = state.dropDownMenuList,
onReportButtonClick = navigateToReport
onReportButtonClick = { navigateToReport(postId, userId) }
)
}
)
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ fun NavController.navigateToPlaceDetail(

fun NavGraphBuilder.placeDetailNavGraph(
paddingValues: PaddingValues,
navigateToReport: () -> Unit,
navigateToReport: (postId: Int, userId: Int) -> Unit,
navigateUp: () -> Unit
) {
composable<PlaceDetail> {
Original file line number Diff line number Diff line change
@@ -30,20 +30,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import com.spoony.spoony.R
import com.spoony.spoony.core.designsystem.component.button.SpoonyButton
import com.spoony.spoony.core.designsystem.component.textfield.SpoonyLargeTextField
import com.spoony.spoony.core.designsystem.component.topappbar.TitleTopAppBar
import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme
import com.spoony.spoony.core.designsystem.type.ButtonSize
import com.spoony.spoony.core.designsystem.type.ButtonStyle
import com.spoony.spoony.core.state.UiState
import com.spoony.spoony.core.util.extension.addFocusCleaner
import com.spoony.spoony.presentation.report.component.ReportCompleteDialog
import com.spoony.spoony.presentation.report.component.ReportRadioButton
@@ -63,6 +66,23 @@ fun ReportRoute(
val state by viewModel.state.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
var reportSuccessDialogVisibility by remember { mutableStateOf(false) }

val postId = (state.postId as? UiState.Success)?.data ?: return
val userId = (state.userId as? UiState.Success)?.data ?: return

val keyboardController = LocalSoftwareKeyboardController.current

LaunchedEffect(viewModel.sideEffect, lifecycleOwner) {
viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { effect ->
when (effect) {
is ReportSideEffect.ShowDialog -> {
// 키보드 존재한다면 닫기
keyboardController?.hide()
reportSuccessDialogVisibility = true
}
}
}
}

ReportScreen(
paddingValues = paddingValues,
reportOptions = state.reportOptions,
@@ -71,15 +91,15 @@ fun ReportRoute(
reportButtonEnabled = state.reportButtonEnabled,
onReportOptionSelected = viewModel::updateSelectedReportOption,
onContextChanged = viewModel::updateReportContext,
onBackButtonClick = navigateUp,
onOpenDialogClick = { reportSuccessDialogVisibility = true }
onReportClick = { viewModel.reportPost(postId, userId, state.selectedReportOption.code, state.reportContext) },
onBackButtonClick = navigateUp
)

if (reportSuccessDialogVisibility) {
ReportCompleteDialog(
onClick = {
navigateToExplore()
reportSuccessDialogVisibility = false
navigateToExplore()
}
)
}
@@ -94,12 +114,12 @@ private fun ReportScreen(
reportButtonEnabled: Boolean,
onReportOptionSelected: (ReportOption) -> Unit,
onContextChanged: (String) -> Unit,
onBackButtonClick: () -> Unit,
onOpenDialogClick: () -> Unit
onReportClick: () -> Unit,
onBackButtonClick: () -> Unit
) {
val focusManager = LocalFocusManager.current
val scrollState = rememberScrollState()
val imeInsets = WindowInsets.ime // 키보드 상태를 관찰
val imeInsets = WindowInsets.ime
val imeHeight = imeInsets.getBottom(LocalDensity.current)

LaunchedEffect(imeHeight) {
@@ -198,7 +218,7 @@ private fun ReportScreen(

SpoonyButton(
text = "신고하기",
onClick = onOpenDialogClick,
onClick = onReportClick,
enabled = reportButtonEnabled,
style = ButtonStyle.Secondary,
size = ButtonSize.Xlarge,
@@ -231,7 +251,7 @@ private fun ReportScreenPreview() {
onBackButtonClick = {},
paddingValues = PaddingValues(),
reportButtonEnabled = false,
onOpenDialogClick = {}
onReportClick = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.spoony.spoony.presentation.report

sealed class ReportSideEffect {
data object ShowDialog : ReportSideEffect()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.spoony.spoony.presentation.report

import com.spoony.spoony.core.state.UiState
import com.spoony.spoony.presentation.report.type.ReportOption
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

data class ReportState(
val postId: UiState<Int> = UiState.Loading,
val userId: UiState<Int> = UiState.Loading,
val reportOptions: ImmutableList<ReportOption> = ReportOption.entries.toImmutableList(),
val selectedReportOption: ReportOption = ReportOption.ADVERTISEMENT,
val reportContext: String = "",
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
package com.spoony.spoony.presentation.report

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.spoony.spoony.core.state.UiState
import com.spoony.spoony.domain.repository.ReportRepository
import com.spoony.spoony.presentation.placeDetail.navigation.PlaceDetail
import com.spoony.spoony.presentation.report.type.ReportOption
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber

@HiltViewModel
class ReportViewModel @Inject constructor() : ViewModel() {
class ReportViewModel @Inject constructor(
private val reportRepository: ReportRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _state: MutableStateFlow<ReportState> = MutableStateFlow(ReportState())
val state: StateFlow<ReportState>
get() = _state

private val _sideEffect = MutableSharedFlow<ReportSideEffect>()
val sideEffect: SharedFlow<ReportSideEffect>
get() = _sideEffect

init {
val reportArgs = savedStateHandle.toRoute<PlaceDetail>()
_state.value = _state.value.copy(
postId = UiState.Success(data = reportArgs.postId),
userId = UiState.Success(data = reportArgs.userId)
)
}

fun updateSelectedReportOption(newOption: ReportOption) {
_state.value = _state.value.copy(selectedReportOption = newOption)
_state.update {
it.copy(selectedReportOption = newOption)
}
}

fun updateReportContext(newContext: String) {
_state.value = _state.value.copy(reportContext = newContext)
_state.update {
it.copy(reportContext = newContext)
}
when (isValidLength(newContext)) {
true -> _state.value = _state.value.copy(reportButtonEnabled = true)
false -> _state.value = _state.value.copy(reportButtonEnabled = false)
@@ -28,4 +58,14 @@ class ReportViewModel @Inject constructor() : ViewModel() {
private fun isValidLength(input: String, minLength: Int = 1, maxLength: Int = 300): Boolean {
return input.length in minLength..maxLength
}

fun reportPost(postId: Int, userId: Int, reportType: String, reportDetail: String) {
viewModelScope.launch {
reportRepository.postReportPost(postId = postId, userId = userId, reportType = reportType, reportDetail = reportDetail)
.onSuccess {
_sideEffect.emit(ReportSideEffect.ShowDialog)
}
.onFailure(Timber::e)
}
}
}
Original file line number Diff line number Diff line change
@@ -10,9 +10,11 @@ import com.spoony.spoony.presentation.report.ReportRoute
import kotlinx.serialization.Serializable

fun NavController.navigateToReport(
postId: Int,
userId: Int,
navOptions: NavOptions? = null
) {
navigate(Report, navOptions)
navigate(Report(postId, userId), navOptions)
}

fun NavGraphBuilder.reportNavGraph(
@@ -30,4 +32,4 @@ fun NavGraphBuilder.reportNavGraph(
}

@Serializable
data object Report : Route
data class Report(val postId: Int, val userId: Int) : Route

0 comments on commit da401fd

Please sign in to comment.