Skip to content

Commit

Permalink
Merge pull request #139 from SOPT-all/feat/#137-report-post-api
Browse files Browse the repository at this point in the history
[FEAT/#137] 신고하기 페이지 신고하기 기능 및  API 구현
  • Loading branch information
Roel4990 authored Jan 23, 2025
2 parents a2398e6 + 9109c26 commit b78469c
Show file tree
Hide file tree
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.ReportPostRequestDto
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(
ReportPostRequestDto(postId = postId, userId = userId, reportType = reportType, reportDetail = reportDetail)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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 ReportPostRequestDto(
@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.ReportPostRequestDto
import retrofit2.http.Body
import retrofit2.http.POST

interface ReportService {
@POST("/api/v1/report")
suspend fun postReportPost(
@Body reportPostRequestDto: ReportPostRequestDto
): 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
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
) {
Expand Down Expand Up @@ -185,7 +185,7 @@ fun PlaceDetailRoute(
placeName = data.placeName,
isScooped = state.isScooped,
dropdownMenuList = state.dropDownMenuList,
onReportButtonClick = navigateToReport
onReportButtonClick = { navigateToReport(postId, userId) }
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fun NavController.navigateToPlaceDetail(

fun NavGraphBuilder.placeDetailNavGraph(
paddingValues: PaddingValues,
navigateToReport: () -> Unit,
navigateToReport: (postId: Int, userId: Int) -> Unit,
navigateUp: () -> Unit
) {
composable<PlaceDetail> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
}
)
}
Expand All @@ -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) {
Expand Down Expand Up @@ -198,7 +218,7 @@ private fun ReportScreen(

SpoonyButton(
text = "신고하기",
onClick = onOpenDialogClick,
onClick = onReportClick,
enabled = reportButtonEnabled,
style = ButtonStyle.Secondary,
size = ButtonSize.Xlarge,
Expand Down Expand Up @@ -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 = "",
Expand Down
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)
Expand All @@ -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)
}
}
}
Loading

0 comments on commit b78469c

Please sign in to comment.