diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ef020bc..c03c1f3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -129,6 +129,11 @@ dependencies { // calendar implementation(libs.kizitonwose.calendar) + // Paging + implementation(libs.androidx.room.paging) + implementation(libs.androidx.paging.runtime.android) + implementation(libs.androidx.paging.runtime) + // Google implementation(libs.androidx.credentials) implementation(libs.androidx.credentials.play.services.auth) diff --git a/app/src/main/java/org/sopt/certi/core/component/bottomsheet/RegisterTestInfoBottomSheet.kt b/app/src/main/java/org/sopt/certi/core/component/bottomsheet/RegisterTestInfoBottomSheet.kt index 3c5607c8..7d64132d 100644 --- a/app/src/main/java/org/sopt/certi/core/component/bottomsheet/RegisterTestInfoBottomSheet.kt +++ b/app/src/main/java/org/sopt/certi/core/component/bottomsheet/RegisterTestInfoBottomSheet.kt @@ -298,7 +298,9 @@ fun RegisterTestInfoBottomSheet( } .padding(horizontal = screenWidthDp(12.dp)) .noRippleClickable { - showPlaceP1List = true + if (!showPlaceP2List) { + showPlaceP1List = true + } }, verticalAlignment = Alignment.CenterVertically ) { @@ -328,7 +330,7 @@ fun RegisterTestInfoBottomSheet( .clip(RoundedCornerShape(4.dp)) .padding(horizontal = screenWidthDp(12.dp)) .noRippleClickable { - if (cityText.isNotEmpty()) { + if (cityText.isNotEmpty() && !showPlaceP1List) { showPlaceP2List = true } }, diff --git a/app/src/main/java/org/sopt/certi/core/component/dialog/CertiContentDialog.kt b/app/src/main/java/org/sopt/certi/core/component/dialog/CertiContentDialog.kt new file mode 100644 index 00000000..9f431611 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/core/component/dialog/CertiContentDialog.kt @@ -0,0 +1,99 @@ +package org.sopt.certi.core.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.sopt.certi.R +import org.sopt.certi.core.util.heightForScreenPercentage +import org.sopt.certi.core.util.screenHeightDp +import org.sopt.certi.core.util.screenWidthDp +import org.sopt.certi.ui.theme.CertiTheme + +@Composable +fun CertiContentDialog( + titleText: String, + contentText: String, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit +) { + Dialog(onDismissRequest = onDismissClick) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(CertiTheme.colors.white) + .padding(top = screenHeightDp(30.dp)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = titleText, + style = CertiTheme.typography.body.semibold_16, + color = CertiTheme.colors.gray600, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp)), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.heightForScreenPercentage(16.dp)) + + Text( + text = contentText, + style = CertiTheme.typography.caption.regular_14, + color = CertiTheme.colors.gray600, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp)), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.heightForScreenPercentage(26.dp)) + + HorizontalDivider( + thickness = 1.dp, + color = CertiTheme.colors.gray100 + ) + + Row(modifier = Modifier.height(IntrinsicSize.Max)) { + DialogButton( + text = stringResource(R.string.delete_dialog_cancel), + textColor = CertiTheme.colors.black, + onClick = onDismissClick, + modifier = Modifier.weight(1f) + ) + VerticalDivider( + thickness = 1.dp, + color = CertiTheme.colors.gray100 + ) + DialogButton( + text = stringResource(R.string.delete_dialog_confirm), + textColor = CertiTheme.colors.purpleBlue, + onClick = onConfirmClick, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Preview +@Composable +private fun PreviewCertiContentDialog() { + CertiContentDialog( + titleText = "프리뷰 프리뷰", + contentText = "콘텐트 콘텐트", + onConfirmClick = {}, + onDismissClick = {} + ) +} diff --git a/app/src/main/java/org/sopt/certi/core/component/dialog/CertiDialog.kt b/app/src/main/java/org/sopt/certi/core/component/dialog/CertiDialog.kt index 64b41f73..15c27d75 100644 --- a/app/src/main/java/org/sopt/certi/core/component/dialog/CertiDialog.kt +++ b/app/src/main/java/org/sopt/certi/core/component/dialog/CertiDialog.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import org.sopt.certi.R @@ -74,3 +75,13 @@ fun CertiDialog( } } } + +@Preview +@Composable +private fun PreviewCertiDialog() { + CertiDialog( + text = "프리뷰 프리뷰", + onConfirmClick = {}, + onDismissClick = {} + ) +} diff --git a/app/src/main/java/org/sopt/certi/core/network/TokenManager.kt b/app/src/main/java/org/sopt/certi/core/network/TokenManager.kt index 93531ee4..7d82b3f5 100644 --- a/app/src/main/java/org/sopt/certi/core/network/TokenManager.kt +++ b/app/src/main/java/org/sopt/certi/core/network/TokenManager.kt @@ -76,6 +76,14 @@ class TokenManager @Inject constructor( return sharedPreferences.getString("NICKNAME", "").orEmpty() } + fun saveUserId(userId: Long) { + sharedPreferences.edit().putLong("USERID", userId).apply() + } + + fun getUserId(): Long { + return sharedPreferences.getLong("USERID", 0L) + } + fun nicknameFlow(): Flow = callbackFlow { trySend(getNickName()) diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todomain/comment/CommentMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todomain/comment/CommentMapper.kt new file mode 100644 index 00000000..bb345914 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todomain/comment/CommentMapper.kt @@ -0,0 +1,32 @@ +package org.sopt.certi.data.mapper.todomain.comment + +import org.sopt.certi.data.remote.dto.response.comment.CommentItemResponseDto +import org.sopt.certi.data.remote.dto.response.comment.GetCommentListResponseDto +import org.sopt.certi.domain.model.comment.CommentData +import org.sopt.certi.domain.model.comment.CommentItemData +import org.sopt.certi.domain.type.CertStateType + +fun GetCommentListResponseDto.toDomain(): CommentData { + return CommentData( + content = content.map { it.toDomain() }, + totalPages = totalPages, + totalElements = totalElements, + isLast = isLast + ) +} + +fun CommentItemResponseDto.toDomain(): CommentItemData { + return CommentItemData( + commentId = commentId, + userId = userId, + nickName = nickName, + content = content, + userMajor = userMajor, + userJob = userJob, + state = CertStateType.fromStateName(state), + createdTime = createdTime, + lastModifiedTime = lastModifiedTime, + isLike = isLike, + likeCount = likeCount + ) +} diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/UserInfoResponseDtoMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/UserInfoResponseDtoMapper.kt index 7aeb5c3b..6cc610ea 100644 --- a/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/UserInfoResponseDtoMapper.kt +++ b/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/UserInfoResponseDtoMapper.kt @@ -4,10 +4,12 @@ import org.sopt.certi.data.remote.dto.response.UserInfoResponseDto import org.sopt.certi.domain.model.user.UserInfoData fun UserInfoResponseDto.toDomain() = UserInfoData( + userId = userId, nickname = nickname, name = name, university = university, major = major, + job = job, profileImageUrl = profileImage, birthday = birthDate ) diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todto/comment/CommentMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todto/comment/CommentMapper.kt new file mode 100644 index 00000000..de7ca611 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todto/comment/CommentMapper.kt @@ -0,0 +1,21 @@ +package org.sopt.certi.data.mapper.todto.comment + +import org.sopt.certi.data.remote.dto.request.comment.CommentListPageableRequestDto +import org.sopt.certi.data.remote.dto.request.comment.RegisterCommentRequestDto +import org.sopt.certi.domain.model.comment.CommentListPageableRequest +import org.sopt.certi.domain.model.comment.RegisterCommentRequest + +fun RegisterCommentRequest.toDto(): RegisterCommentRequestDto { + return RegisterCommentRequestDto( + content = content, + certificationId = certificationId + ) +} + +fun CommentListPageableRequest.toDto(): CommentListPageableRequestDto { + return CommentListPageableRequestDto( + page = page, + size = size, + sort = sort + ) +} diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todto/report/ReportMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todto/report/ReportMapper.kt new file mode 100644 index 00000000..f73ea7a2 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todto/report/ReportMapper.kt @@ -0,0 +1,11 @@ +package org.sopt.certi.data.mapper.todto.report + +import org.sopt.certi.data.remote.dto.request.report.ReportCommentRequestDto +import org.sopt.certi.domain.model.report.ReportCommentRequest + +fun ReportCommentRequest.toDto(): ReportCommentRequestDto { + return ReportCommentRequestDto( + content = this.content, + shouldBlockUser = this.shouldBlockUser + ) +} diff --git a/app/src/main/java/org/sopt/certi/data/pagingsource/CertiPaging.kt b/app/src/main/java/org/sopt/certi/data/pagingsource/CertiPaging.kt new file mode 100644 index 00000000..a67aabf2 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/pagingsource/CertiPaging.kt @@ -0,0 +1,54 @@ +package org.sopt.certi.data.pagingsource + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class CertiPagingSource( + private val pageSize: Int, + private val getList: suspend (Int) -> List +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + val anchor = state.anchorPosition ?: return null + val page = state.closestPageToPosition(anchor) ?: return null + return page.prevKey?.plus(1) ?: page.nextKey?.minus(1) + } + + override suspend fun load(params: LoadParams): LoadResult { + val currentPage = params.key ?: 0 + return try { + val list = getList(currentPage) + LoadResult.Page( + data = list, + prevKey = if (currentPage == 0) null else currentPage - 1, + nextKey = if (list.size < pageSize) null else currentPage + 1 + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} + +fun createPager( + limit: Int = 10, + initialLoadSize: Int = 20, + q: List? = null, + startPage: Int? = null, + pagingSourceFactory: suspend (page: Int, limit: Int, sort: List?) -> List +): Pager { + return Pager( + config = PagingConfig( + pageSize = limit, + initialLoadSize = initialLoadSize, + prefetchDistance = 1, + enablePlaceholders = false + ), + initialKey = startPage ?: 0, + pagingSourceFactory = { + CertiPagingSource(pageSize = limit) { page -> + pagingSourceFactory(page, limit, q) + } + } + ) +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasource/CommentRemoteDataSource.kt b/app/src/main/java/org/sopt/certi/data/remote/datasource/CommentRemoteDataSource.kt new file mode 100644 index 00000000..961321af --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasource/CommentRemoteDataSource.kt @@ -0,0 +1,14 @@ +package org.sopt.certi.data.remote.datasource + +import org.sopt.certi.data.remote.dto.base.ApiResponse +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.comment.CommentListPageableRequestDto +import org.sopt.certi.data.remote.dto.request.comment.RegisterCommentRequestDto +import org.sopt.certi.data.remote.dto.response.comment.GetCommentListResponseDto + +interface CommentRemoteDataSource { + suspend fun getCommentList(certificationId: Long, pageable: CommentListPageableRequestDto): ApiResponse + suspend fun registerComment(registerCommentRequest: RegisterCommentRequestDto): NullableApiResponse + suspend fun likeComment(commentId: Long): NullableApiResponse + suspend fun deleteComment(commentId: Long): NullableApiResponse +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasource/ReportRemoteDataSource.kt b/app/src/main/java/org/sopt/certi/data/remote/datasource/ReportRemoteDataSource.kt new file mode 100644 index 00000000..912b325f --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasource/ReportRemoteDataSource.kt @@ -0,0 +1,11 @@ +package org.sopt.certi.data.remote.datasource + +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.report.ReportCommentRequestDto + +interface ReportRemoteDataSource { + suspend fun reportComment( + certificationCommentId: Long, + reportCommentRequestDto: ReportCommentRequestDto + ): NullableApiResponse +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/CommentRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/CommentRemoteDataSourceImpl.kt new file mode 100644 index 00000000..bf3b3728 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/CommentRemoteDataSourceImpl.kt @@ -0,0 +1,26 @@ +package org.sopt.certi.data.remote.datasourceimpl + +import org.sopt.certi.data.remote.datasource.CommentRemoteDataSource +import org.sopt.certi.data.remote.dto.base.ApiResponse +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.comment.CommentListPageableRequestDto +import org.sopt.certi.data.remote.dto.request.comment.RegisterCommentRequestDto +import org.sopt.certi.data.remote.dto.response.comment.GetCommentListResponseDto +import org.sopt.certi.data.remote.service.CommentService +import javax.inject.Inject + +class CommentRemoteDataSourceImpl @Inject constructor( + private val commentService: CommentService +) : CommentRemoteDataSource { + override suspend fun getCommentList(certificationId: Long, pageable: CommentListPageableRequestDto): ApiResponse = + commentService.getCommentList(certificationId, page = pageable.page, size = pageable.size, sort = if (pageable.sort.isNotEmpty()) pageable.sort.toString() else null) + + override suspend fun registerComment(registerCommentRequest: RegisterCommentRequestDto): NullableApiResponse = + commentService.registerComment(registerCommentRequest) + + override suspend fun likeComment(commentId: Long): NullableApiResponse = + commentService.likeComment(commentId) + + override suspend fun deleteComment(commentId: Long): NullableApiResponse = + commentService.deleteComment(commentId) +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/ReportRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/ReportRemoteDataSourceImpl.kt new file mode 100644 index 00000000..674ff036 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/ReportRemoteDataSourceImpl.kt @@ -0,0 +1,15 @@ +package org.sopt.certi.data.remote.datasourceimpl + +import org.sopt.certi.data.remote.datasource.ReportRemoteDataSource +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.report.ReportCommentRequestDto +import org.sopt.certi.data.remote.service.ReportService +import javax.inject.Inject + +class ReportRemoteDataSourceImpl @Inject constructor( + private val reportService: ReportService +) : ReportRemoteDataSource { + override suspend fun reportComment(certificationCommentId: Long, reportCommentRequestDto: ReportCommentRequestDto): NullableApiResponse { + return reportService.reportComment(certificationCommentId, reportCommentRequestDto) + } +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/CommentListPageableRequestDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/CommentListPageableRequestDto.kt new file mode 100644 index 00000000..9d2070bd --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/CommentListPageableRequestDto.kt @@ -0,0 +1,14 @@ +package org.sopt.certi.data.remote.dto.request.comment + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CommentListPageableRequestDto( + @SerialName("page") + val page: Int, + @SerialName("size") + val size: Int, + @SerialName("sort") + val sort: List +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/RegisterCommentRequestDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/RegisterCommentRequestDto.kt new file mode 100644 index 00000000..648b88e3 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/request/comment/RegisterCommentRequestDto.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.data.remote.dto.request.comment + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterCommentRequestDto( + @SerialName("content") + val content: String, + @SerialName("certificationId") + val certificationId: Long +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/request/report/ReportCommentRequestDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/request/report/ReportCommentRequestDto.kt new file mode 100644 index 00000000..3bb20025 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/request/report/ReportCommentRequestDto.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.data.remote.dto.request.report + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReportCommentRequestDto( + @SerialName("content") + val content: String, + @SerialName("shouldBlockUser") + val shouldBlockUser: Boolean +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/response/UserInfoResponseDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/response/UserInfoResponseDto.kt index e6c77cd0..b07f674a 100644 --- a/app/src/main/java/org/sopt/certi/data/remote/dto/response/UserInfoResponseDto.kt +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/response/UserInfoResponseDto.kt @@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable @Serializable data class UserInfoResponseDto( + @SerialName("userId") + val userId: Long, @SerialName("nickname") val nickname: String, @SerialName("name") @@ -13,6 +15,8 @@ data class UserInfoResponseDto( val university: String, @SerialName("major") val major: String, + @SerialName("job") + val job: String, @SerialName("profileImage") val profileImage: String?, @SerialName("birthDate") diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/response/comment/GetCommentListResponseDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/response/comment/GetCommentListResponseDto.kt new file mode 100644 index 00000000..bc2a2354 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/response/comment/GetCommentListResponseDto.kt @@ -0,0 +1,42 @@ +package org.sopt.certi.data.remote.dto.response.comment + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetCommentListResponseDto( + @SerialName("content") + val content: List, + @SerialName("totalPages") + val totalPages: Int, + @SerialName("totalElements") + val totalElements: Int, + @SerialName("isLast") + val isLast: Boolean +) + +@Serializable +data class CommentItemResponseDto( + @SerialName("commentId") + val commentId: Long, + @SerialName("userId") + val userId: Long, + @SerialName("nickName") + val nickName: String, + @SerialName("content") + val content: String, + @SerialName("userMajor") + val userMajor: String, + @SerialName("userJob") + val userJob: String, + @SerialName("state") + val state: String, + @SerialName("likeCount") + val likeCount: Int, + @SerialName("createdTime") + val createdTime: String, + @SerialName("lastModifiedTime") + val lastModifiedTime: String, + @SerialName("isLike") + val isLike: Boolean +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/service/CommentService.kt b/app/src/main/java/org/sopt/certi/data/remote/service/CommentService.kt new file mode 100644 index 00000000..237787bc --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/service/CommentService.kt @@ -0,0 +1,31 @@ +package org.sopt.certi.data.remote.service + +import org.sopt.certi.data.remote.dto.base.ApiResponse +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.comment.RegisterCommentRequestDto +import org.sopt.certi.data.remote.dto.response.comment.GetCommentListResponseDto +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface CommentService { + @GET("api/v1/comments") + suspend fun getCommentList( + @Query("certificationId") certificationId: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort") sort: String? = null + ): ApiResponse + + @POST("api/v1/comments") + suspend fun registerComment(@Body registerCommentRequest: RegisterCommentRequestDto): NullableApiResponse + + @POST("api/v1/comments/{commentId}/like") + suspend fun likeComment(@Path("commentId") commentId: Long): NullableApiResponse + + @DELETE("api/v1/comments/{commentId}") + suspend fun deleteComment(@Path("commentId") commentId: Long): NullableApiResponse +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/service/ReportService.kt b/app/src/main/java/org/sopt/certi/data/remote/service/ReportService.kt new file mode 100644 index 00000000..cefa0a47 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/service/ReportService.kt @@ -0,0 +1,15 @@ +package org.sopt.certi.data.remote.service + +import org.sopt.certi.data.remote.dto.base.NullableApiResponse +import org.sopt.certi.data.remote.dto.request.report.ReportCommentRequestDto +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +interface ReportService { + @POST("/api/v1/report/comment/{certification_comment_id}") + suspend fun reportComment( + @Path("certification_comment_id") certificationCommentId: Long, + @Body reportCommentRequestDto: ReportCommentRequestDto + ): NullableApiResponse +} diff --git a/app/src/main/java/org/sopt/certi/data/repositoryimpl/CommentRepositoryImpl.kt b/app/src/main/java/org/sopt/certi/data/repositoryimpl/CommentRepositoryImpl.kt new file mode 100644 index 00000000..d765e9fb --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/repositoryimpl/CommentRepositoryImpl.kt @@ -0,0 +1,70 @@ +package org.sopt.certi.data.repositoryimpl + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import org.sopt.certi.data.mapper.todomain.comment.toDomain +import org.sopt.certi.data.mapper.todto.comment.toDto +import org.sopt.certi.data.pagingsource.createPager +import org.sopt.certi.data.remote.datasource.CommentRemoteDataSource +import org.sopt.certi.data.remote.dto.request.comment.CommentListPageableRequestDto +import org.sopt.certi.data.remote.util.HttpResponseHandler.handleNullableApiResponse +import org.sopt.certi.domain.model.comment.CommentItemData +import org.sopt.certi.domain.model.comment.RegisterCommentRequest +import org.sopt.certi.domain.repository.CommentRepository +import javax.inject.Inject + +class CommentRepositoryImpl @Inject constructor( + private val commentRemoteDataSource: CommentRemoteDataSource +) : CommentRepository { + + private var _totalCommentCount = MutableStateFlow(0) + + override suspend fun getCommentList(certificationId: Long, sort: List): Flow> { + return createPager( + limit = 12, + initialLoadSize = 12, + q = sort + ) { page, limit, sortParam -> + val response = commentRemoteDataSource.getCommentList( + certificationId, + CommentListPageableRequestDto( + page = page, + size = limit, + sort = sortParam ?: sort + ) + ).data.toDomain() + + _totalCommentCount.emit(response.totalElements) + response.content + }.flow + } + + override fun getTotalCommentCount(): Flow { + return _totalCommentCount + } + + override suspend fun registerComment(registerCommentRequest: RegisterCommentRequest): Result { + return runCatching { + commentRemoteDataSource.registerComment(registerCommentRequest.toDto()) + .handleNullableApiResponse() + .getOrThrow() + } + } + + override suspend fun likeComment(commentId: Long): Result { + return runCatching { + commentRemoteDataSource.likeComment(commentId) + .handleNullableApiResponse() + .getOrThrow() + } + } + + override suspend fun deleteComment(commentId: Long): Result { + return runCatching { + commentRemoteDataSource.deleteComment(commentId) + .handleNullableApiResponse() + .getOrThrow() + } + } +} diff --git a/app/src/main/java/org/sopt/certi/data/repositoryimpl/ReportRepositoryImpl.kt b/app/src/main/java/org/sopt/certi/data/repositoryimpl/ReportRepositoryImpl.kt new file mode 100644 index 00000000..9b64dd43 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/repositoryimpl/ReportRepositoryImpl.kt @@ -0,0 +1,20 @@ +package org.sopt.certi.data.repositoryimpl + +import org.sopt.certi.data.mapper.todto.report.toDto +import org.sopt.certi.data.remote.datasource.ReportRemoteDataSource +import org.sopt.certi.data.remote.util.HttpResponseHandler.handleNullableApiResponse +import org.sopt.certi.domain.model.report.ReportCommentRequest +import org.sopt.certi.domain.repository.ReportRepository +import javax.inject.Inject + +class ReportRepositoryImpl @Inject constructor( + private val reportRemoteDataSource: ReportRemoteDataSource +) : ReportRepository { + override suspend fun reportComment(certificationCommentId: Long, reportCommentRequest: ReportCommentRequest): Result { + return runCatching { + reportRemoteDataSource.reportComment(certificationCommentId, reportCommentRequest.toDto()) + .handleNullableApiResponse() + .getOrThrow() + } + } +} diff --git a/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt b/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt index 7585d394..586408ae 100644 --- a/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt +++ b/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt @@ -9,11 +9,13 @@ import org.sopt.certi.data.remote.datasource.ActivityRemoteDataSource import org.sopt.certi.data.remote.datasource.AuthRemoteDataSource import org.sopt.certi.data.remote.datasource.CareerRemoteDataSource import org.sopt.certi.data.remote.datasource.CertRemoteDataSource +import org.sopt.certi.data.remote.datasource.CommentRemoteDataSource import javax.inject.Singleton import org.sopt.certi.data.remote.datasource.DummyRemoteDataSource import org.sopt.certi.data.remote.datasource.HomeRemoteDataSource import org.sopt.certi.data.remote.datasource.PreCertEditRemoteDataSource import org.sopt.certi.data.remote.datasource.PreCertRemoteDataSource +import org.sopt.certi.data.remote.datasource.ReportRemoteDataSource import org.sopt.certi.data.remote.datasource.S3DataSource import org.sopt.certi.data.remote.datasourceimpl.AcquisitionRemoteDataSourceImpl import org.sopt.certi.data.remote.datasource.UserRemoteDataSource @@ -21,10 +23,12 @@ import org.sopt.certi.data.remote.datasourceimpl.ActivityRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.AuthRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.CareerRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.CertRemoteDataSourceImpl +import org.sopt.certi.data.remote.datasourceimpl.CommentRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.DummyRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.HomeRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.PreCertEditRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.PreCertRemoteDataSourceImpl +import org.sopt.certi.data.remote.datasourceimpl.ReportRemoteDataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.S3DataSourceImpl import org.sopt.certi.data.remote.datasourceimpl.UserRemoteDataSourceImpl @@ -74,4 +78,12 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindsS3DataSource(s3DataSourceImpl: S3DataSourceImpl): S3DataSource + + @Binds + @Singleton + abstract fun bindsCommentDataSource(commentRemoteDataSourceImpl: CommentRemoteDataSourceImpl): CommentRemoteDataSource + + @Binds + @Singleton + abstract fun bindsReportDataSource(reportRemoteDataSourceImpl: ReportRemoteDataSourceImpl): ReportRemoteDataSource } diff --git a/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt b/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt index 25b7b9dd..02d269f1 100644 --- a/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt @@ -9,10 +9,12 @@ import org.sopt.certi.data.repositoryimpl.AcquisitionRepositoryImpl import org.sopt.certi.data.repositoryimpl.AuthRepositoryImpl import org.sopt.certi.data.repositoryimpl.CareerRepositoryImpl import org.sopt.certi.data.repositoryimpl.CertRepositoryImpl +import org.sopt.certi.data.repositoryimpl.CommentRepositoryImpl import org.sopt.certi.data.repositoryimpl.DummyRepositoryImpl import org.sopt.certi.data.repositoryimpl.HomeRepositoryImpl import org.sopt.certi.data.repositoryimpl.PreCertEditRepositoryImpl import org.sopt.certi.data.repositoryimpl.PreCertRepositoryImpl +import org.sopt.certi.data.repositoryimpl.ReportRepositoryImpl import org.sopt.certi.data.repositoryimpl.S3RepositoryImpl import org.sopt.certi.domain.repository.AcquisitionRepository import org.sopt.certi.data.repositoryimpl.UserRepositoryImpl @@ -20,10 +22,12 @@ import org.sopt.certi.domain.repository.ActivityRepository import org.sopt.certi.domain.repository.AuthRepository import org.sopt.certi.domain.repository.CareerRepository import org.sopt.certi.domain.repository.CertRepository +import org.sopt.certi.domain.repository.CommentRepository import org.sopt.certi.domain.repository.DummyRepository import org.sopt.certi.domain.repository.HomeRepository import org.sopt.certi.domain.repository.PreCertEditRepository import org.sopt.certi.domain.repository.PreCertRepository +import org.sopt.certi.domain.repository.ReportRepository import org.sopt.certi.domain.repository.S3Repository import org.sopt.certi.domain.repository.UserRepository import javax.inject.Singleton @@ -73,4 +77,12 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindS3Repository(s3RepositoryImpl: S3RepositoryImpl): S3Repository + + @Binds + @Singleton + abstract fun bindCommentRepository(commentRepositoryImpl: CommentRepositoryImpl): CommentRepository + + @Binds + @Singleton + abstract fun bindReportRepository(reportRepositoryImpl: ReportRepositoryImpl): ReportRepository } diff --git a/app/src/main/java/org/sopt/certi/di/ServiceModule.kt b/app/src/main/java/org/sopt/certi/di/ServiceModule.kt index 4b121c4f..6dc7d7c2 100644 --- a/app/src/main/java/org/sopt/certi/di/ServiceModule.kt +++ b/app/src/main/java/org/sopt/certi/di/ServiceModule.kt @@ -9,11 +9,13 @@ import org.sopt.certi.data.remote.service.AcquisitionService import org.sopt.certi.data.remote.service.AuthService import org.sopt.certi.data.remote.service.CareerService import org.sopt.certi.data.remote.service.CertService +import org.sopt.certi.data.remote.service.CommentService import javax.inject.Singleton import org.sopt.certi.data.remote.service.DummyService import org.sopt.certi.data.remote.service.HomeService import org.sopt.certi.data.remote.service.PreCertEditService import org.sopt.certi.data.remote.service.PreCertService +import org.sopt.certi.data.remote.service.ReportService import org.sopt.certi.data.remote.service.S3Service import org.sopt.certi.data.remote.service.UserService import retrofit2.Retrofit @@ -77,4 +79,14 @@ object ServiceModule { fun provideS3Service(@Named("S3Retrofit") retrofit: Retrofit): S3Service { return retrofit.create(S3Service::class.java) } + + @Provides + @Singleton + fun provideCommentService(retrofit: Retrofit): CommentService = + retrofit.create(CommentService::class.java) + + @Provides + @Singleton + fun provideReportService(retrofit: Retrofit): ReportService = + retrofit.create(ReportService::class.java) } diff --git a/app/src/main/java/org/sopt/certi/di/UseCaseModule.kt b/app/src/main/java/org/sopt/certi/di/UseCaseModule.kt index b697ad33..f07c475d 100644 --- a/app/src/main/java/org/sopt/certi/di/UseCaseModule.kt +++ b/app/src/main/java/org/sopt/certi/di/UseCaseModule.kt @@ -9,10 +9,12 @@ import org.sopt.certi.domain.repository.AcquisitionRepository import org.sopt.certi.domain.repository.AuthRepository import org.sopt.certi.domain.repository.CareerRepository import org.sopt.certi.domain.repository.CertRepository +import org.sopt.certi.domain.repository.CommentRepository import org.sopt.certi.domain.repository.DummyRepository import org.sopt.certi.domain.repository.HomeRepository import org.sopt.certi.domain.repository.PreCertEditRepository import org.sopt.certi.domain.repository.PreCertRepository +import org.sopt.certi.domain.repository.ReportRepository import org.sopt.certi.domain.repository.UserRepository import org.sopt.certi.domain.usecase.activity.AddActivityUseCase import org.sopt.certi.domain.usecase.career.AddCareerUseCase @@ -42,7 +44,12 @@ import org.sopt.certi.domain.usecase.certification.GetRecommendCertListUseCase import org.sopt.certi.domain.usecase.certification.SearchCertListUseCase import org.sopt.certi.domain.usecase.certification.Top3JobCertListUseCase import org.sopt.certi.domain.usecase.certification.Top3TrackCertListUseCase +import org.sopt.certi.domain.usecase.comment.DeleteCommentUseCase +import org.sopt.certi.domain.usecase.comment.GetCommentListUseCase +import org.sopt.certi.domain.usecase.comment.LikeCommentUseCase +import org.sopt.certi.domain.usecase.comment.RegisterCommentUseCase import org.sopt.certi.domain.usecase.precert.AcquireExpectCertUseCase +import org.sopt.certi.domain.usecase.report.ReportCommentUseCase import org.sopt.certi.domain.usecase.user.GetInterestedJobListUseCase import org.sopt.certi.domain.usecase.user.ModifyInterestedJobListUseCase import javax.inject.Singleton @@ -231,4 +238,34 @@ object UseCaseModule { fun provideTop3JobCertListUseCase( certRepository: CertRepository ): Top3JobCertListUseCase = Top3JobCertListUseCase(certRepository) + + @Provides + @Singleton + fun provideGetCommentListUseCase( + commentRepository: CommentRepository + ): GetCommentListUseCase = GetCommentListUseCase(commentRepository) + + @Provides + @Singleton + fun provideRegisterCommentUseCase( + commentRepository: CommentRepository + ): RegisterCommentUseCase = RegisterCommentUseCase(commentRepository) + + @Provides + @Singleton + fun provideLikeCommentUseCase( + commentRepository: CommentRepository + ): LikeCommentUseCase = LikeCommentUseCase(commentRepository) + + @Provides + @Singleton + fun provideDeleteCommentUseCase( + commentRepository: CommentRepository + ): DeleteCommentUseCase = DeleteCommentUseCase(commentRepository) + + @Provides + @Singleton + fun provideReportCommentUseCase( + reportRepository: ReportRepository + ): ReportCommentUseCase = ReportCommentUseCase(reportRepository) } diff --git a/app/src/main/java/org/sopt/certi/domain/model/comment/CommentData.kt b/app/src/main/java/org/sopt/certi/domain/model/comment/CommentData.kt index 4bf0beb3..a6806400 100644 --- a/app/src/main/java/org/sopt/certi/domain/model/comment/CommentData.kt +++ b/app/src/main/java/org/sopt/certi/domain/model/comment/CommentData.kt @@ -1,6 +1,6 @@ package org.sopt.certi.domain.model.comment -import org.sopt.certi.domain.type.CertAcquireStateType +import org.sopt.certi.domain.type.CertStateType data class CommentData( val content: List, @@ -16,7 +16,7 @@ data class CommentItemData( val content: String, // 댓글 내용 val userMajor: String, // 사용자 전공 val userJob: String, // 사용자 직무 정보(현재 123순위 기능 구현 안해서, 그냥 직무 정보중 첫번째로 가져옴) - val state: CertAcquireStateType, // 취득 예정인지, 취득인지 + val state: CertStateType, // 취득 예정인지, 취득인지 val createdTime: String, // 생성일자 val lastModifiedTime: String, // 수정일자 val isLike: Boolean, // 댓글을 조회하는 사용자가 해당 댓글에 좋아요를 눌렀는지 diff --git a/app/src/main/java/org/sopt/certi/domain/model/comment/CommentListPageableRequest.kt b/app/src/main/java/org/sopt/certi/domain/model/comment/CommentListPageableRequest.kt new file mode 100644 index 00000000..dc0e333a --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/model/comment/CommentListPageableRequest.kt @@ -0,0 +1,7 @@ +package org.sopt.certi.domain.model.comment + +data class CommentListPageableRequest( + val page: Int, + val size: Int, + val sort: List +) diff --git a/app/src/main/java/org/sopt/certi/domain/model/comment/RegisterCommentRequest.kt b/app/src/main/java/org/sopt/certi/domain/model/comment/RegisterCommentRequest.kt new file mode 100644 index 00000000..ba67bf79 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/model/comment/RegisterCommentRequest.kt @@ -0,0 +1,6 @@ +package org.sopt.certi.domain.model.comment + +data class RegisterCommentRequest( + val content: String, + val certificationId: Long +) diff --git a/app/src/main/java/org/sopt/certi/domain/model/report/ReportCommentRequest.kt b/app/src/main/java/org/sopt/certi/domain/model/report/ReportCommentRequest.kt new file mode 100644 index 00000000..3e209e7d --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/model/report/ReportCommentRequest.kt @@ -0,0 +1,6 @@ +package org.sopt.certi.domain.model.report + +data class ReportCommentRequest( + val content: String, + val shouldBlockUser: Boolean +) diff --git a/app/src/main/java/org/sopt/certi/domain/model/user/UserInfoData.kt b/app/src/main/java/org/sopt/certi/domain/model/user/UserInfoData.kt index de855b6c..938199fe 100644 --- a/app/src/main/java/org/sopt/certi/domain/model/user/UserInfoData.kt +++ b/app/src/main/java/org/sopt/certi/domain/model/user/UserInfoData.kt @@ -1,12 +1,29 @@ package org.sopt.certi.domain.model.user data class UserInfoData( + val userId: Long = 0, val name: String, val nickname: String = "", val university: String = "", val major: String = "", val category: List = emptyList(), + val job: String = "", val track: String = "", val birthday: String? = null, val profileImageUrl: String? = null -) +) { + companion object { + fun defaultData() = UserInfoData( + userId = 0, + name = "", + nickname = "", + university = "", + major = "", + category = emptyList(), + job = "", + track = "", + birthday = null, + profileImageUrl = null + ) + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/repository/CommentRepository.kt b/app/src/main/java/org/sopt/certi/domain/repository/CommentRepository.kt new file mode 100644 index 00000000..47d71cf7 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/repository/CommentRepository.kt @@ -0,0 +1,15 @@ +package org.sopt.certi.domain.repository + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.certi.domain.model.comment.CommentItemData +import org.sopt.certi.domain.model.comment.RegisterCommentRequest + +interface CommentRepository { + suspend fun getCommentList(certificationId: Long, sort: List): Flow> + suspend fun registerComment(registerCommentRequest: RegisterCommentRequest): Result + suspend fun likeComment(commentId: Long): Result + suspend fun deleteComment(commentId: Long): Result + + fun getTotalCommentCount(): Flow +} diff --git a/app/src/main/java/org/sopt/certi/domain/repository/ReportRepository.kt b/app/src/main/java/org/sopt/certi/domain/repository/ReportRepository.kt new file mode 100644 index 00000000..4b45932c --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/repository/ReportRepository.kt @@ -0,0 +1,7 @@ +package org.sopt.certi.domain.repository + +import org.sopt.certi.domain.model.report.ReportCommentRequest + +interface ReportRepository { + suspend fun reportComment(certificationCommentId: Long, reportCommentRequest: ReportCommentRequest): Result +} diff --git a/app/src/main/java/org/sopt/certi/domain/type/CertAcquireStateType.kt b/app/src/main/java/org/sopt/certi/domain/type/CertAcquireStateType.kt deleted file mode 100644 index a5ba6dc4..00000000 --- a/app/src/main/java/org/sopt/certi/domain/type/CertAcquireStateType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.certi.domain.type - -enum class CertAcquireStateType { - ACQUIRED, - PRE -} diff --git a/app/src/main/java/org/sopt/certi/domain/type/CertStateType.kt b/app/src/main/java/org/sopt/certi/domain/type/CertStateType.kt index c62f0e5a..e22aeb4c 100644 --- a/app/src/main/java/org/sopt/certi/domain/type/CertStateType.kt +++ b/app/src/main/java/org/sopt/certi/domain/type/CertStateType.kt @@ -1,7 +1,14 @@ package org.sopt.certi.domain.type -enum class CertStateType { - NORMAL, // 아무것도 아님 - ANTICIPATED, // 취득 예정 - ACQUISITION // 취득 완료 +enum class CertStateType(val stateName: String) { + NORMAL("아무것도 아님"), // 아무것도 아님 + ANTICIPATED("취득 예정"), // 취득 예정 + ACQUISITION("취득 완료"); // 취득 완료 + + companion object { + fun fromStateName(stateName: String): CertStateType { + return entries.find { it.stateName == stateName } + ?: NORMAL + } + } } diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/comment/DeleteCommentUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/comment/DeleteCommentUseCase.kt new file mode 100644 index 00000000..f41a86e2 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/comment/DeleteCommentUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.domain.usecase.comment + +import org.sopt.certi.domain.repository.CommentRepository +import javax.inject.Inject + +class DeleteCommentUseCase @Inject constructor( + private val commentRepository: CommentRepository +) { + suspend operator fun invoke(commentId: Long): Result { + return commentRepository.deleteComment(commentId) + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/comment/GetCommentListUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/comment/GetCommentListUseCase.kt new file mode 100644 index 00000000..1b7618cd --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/comment/GetCommentListUseCase.kt @@ -0,0 +1,19 @@ +package org.sopt.certi.domain.usecase.comment + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.sopt.certi.domain.model.comment.CommentItemData +import org.sopt.certi.domain.repository.CommentRepository +import javax.inject.Inject + +class GetCommentListUseCase @Inject constructor( + private val commentRepository: CommentRepository +) { + suspend fun getCommentList(certificationId: Long, sort: List): Flow> { + return commentRepository.getCommentList(certificationId, sort) + } + + fun getTotalCommentCount(): Flow { + return commentRepository.getTotalCommentCount() + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/comment/LikeCommentUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/comment/LikeCommentUseCase.kt new file mode 100644 index 00000000..a1777188 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/comment/LikeCommentUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.domain.usecase.comment + +import org.sopt.certi.domain.repository.CommentRepository +import javax.inject.Inject + +class LikeCommentUseCase @Inject constructor( + private val commentRepository: CommentRepository +) { + suspend operator fun invoke(commentId: Long): Result { + return commentRepository.likeComment(commentId) + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/comment/RegisterCommentUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/comment/RegisterCommentUseCase.kt new file mode 100644 index 00000000..344567e6 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/comment/RegisterCommentUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.certi.domain.usecase.comment + +import org.sopt.certi.domain.model.comment.RegisterCommentRequest +import org.sopt.certi.domain.repository.CommentRepository +import javax.inject.Inject + +class RegisterCommentUseCase @Inject constructor( + private val commentRepository: CommentRepository +) { + suspend operator fun invoke(registerCommentRequest: RegisterCommentRequest): Result { + return commentRepository.registerComment(registerCommentRequest) + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/report/ReportCommentUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/report/ReportCommentUseCase.kt new file mode 100644 index 00000000..17cbb9b4 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/report/ReportCommentUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.domain.usecase.report + +import org.sopt.certi.domain.model.report.ReportCommentRequest +import org.sopt.certi.domain.repository.ReportRepository +import javax.inject.Inject + +class ReportCommentUseCase @Inject constructor( + private val reportRepository: ReportRepository +) { + suspend operator fun invoke(certificationCommentId: Long, reportCommentRequest: ReportCommentRequest): Result = + reportRepository.reportComment(certificationCommentId, reportCommentRequest) +} diff --git a/app/src/main/java/org/sopt/certi/presentation/type/CommentSortType.kt b/app/src/main/java/org/sopt/certi/presentation/type/CommentSortType.kt new file mode 100644 index 00000000..354cd6ad --- /dev/null +++ b/app/src/main/java/org/sopt/certi/presentation/type/CommentSortType.kt @@ -0,0 +1,5 @@ +package org.sopt.certi.presentation.type + +enum class CommentSortType { + Recent, Famous +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailScreen.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailScreen.kt index 321f78f9..a17682c5 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailScreen.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailScreen.kt @@ -191,13 +191,13 @@ fun CertDetailScreen( LaunchedEffect(acquireExpectSuccess) { if (acquireExpectSuccess) { - certState = CertStateType.ACQUISITION + certState = CertStateType.ANTICIPATED } } LaunchedEffect(acquireSuccess) { if (acquireSuccess) { - certState = CertStateType.ANTICIPATED + certState = CertStateType.ACQUISITION } } @@ -243,7 +243,7 @@ fun CertDetailScreen( ) } DetailTabType.Comment -> { - CertDetailCommentRoute() + CertDetailCommentRoute(certificationId = certData.certificationId, certStateType = certState) } } } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailViewModel.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailViewModel.kt index ee26591a..0c27a317 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailViewModel.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/CertDetailViewModel.kt @@ -2,20 +2,34 @@ package org.sopt.certi.presentation.ui.certdetail import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.sopt.certi.core.network.TokenManager +import org.sopt.certi.domain.model.comment.CommentItemData import org.sopt.certi.core.state.UiState import org.sopt.certi.domain.model.certification.CertificationData +import org.sopt.certi.domain.model.comment.RegisterCommentRequest +import org.sopt.certi.domain.model.report.ReportCommentRequest import org.sopt.certi.domain.usecase.acquisition.AcquiredCertUseCase import org.sopt.certi.domain.usecase.certification.GetCertInfoUseCase +import org.sopt.certi.domain.usecase.comment.DeleteCommentUseCase +import org.sopt.certi.domain.usecase.comment.GetCommentListUseCase +import org.sopt.certi.domain.usecase.comment.LikeCommentUseCase +import org.sopt.certi.domain.usecase.comment.RegisterCommentUseCase import org.sopt.certi.domain.usecase.precert.AcquireExpectCertUseCase +import org.sopt.certi.domain.usecase.report.ReportCommentUseCase +import org.sopt.certi.presentation.type.CommentSortType import org.sopt.certi.presentation.ui.certdetail.sideeffect.DetailSideEffect import org.sopt.certi.presentation.ui.certdetail.state.DetailUiState import javax.inject.Inject @@ -24,11 +38,29 @@ import javax.inject.Inject class CertDetailViewModel @Inject constructor( private val getCertInfoUseCase: GetCertInfoUseCase, private val acquiredCertUseCase: AcquiredCertUseCase, - private val acquireExpectCertUseCase: AcquireExpectCertUseCase + private val acquireExpectCertUseCase: AcquireExpectCertUseCase, + private val getCommentListUseCase: GetCommentListUseCase, + private val registerCommentUseCase: RegisterCommentUseCase, + private val likeCommentUseCase: LikeCommentUseCase, + private val deleteCommentUseCase: DeleteCommentUseCase, + private val reportCommentUseCase: ReportCommentUseCase, + private val tokenManager: TokenManager ) : ViewModel() { private val _certDetailInfo = MutableStateFlow>(UiState.Init) + private val _commentPagingData = MutableStateFlow>(PagingData.empty()) + val commentPagingData: StateFlow> = _commentPagingData.asStateFlow() + + private val _totalCommentCount = MutableStateFlow(0) + val totalCommentCount: StateFlow = _totalCommentCount.asStateFlow() + + private val _myUserId = MutableStateFlow(0L) + val myUserId: StateFlow = _myUserId.asStateFlow() + + private val _updateCommentSuccess = MutableStateFlow>(UiState.Init) + val updateCommentSuccess: StateFlow> = _updateCommentSuccess.asStateFlow() + val detailUiState: StateFlow = combine( _certDetailInfo @@ -88,6 +120,68 @@ class CertDetailViewModel @Inject constructor( ) } - fun getCommentList() = viewModelScope.launch { + fun getMyUserId() = viewModelScope.launch { + _myUserId.value = tokenManager.getUserId() + } + + fun getCommentList(certId: Long, commentSortType: CommentSortType) = viewModelScope.launch { + val sortValue = if (commentSortType == CommentSortType.Famous) { + listOf("likeCount", "desc") + } else { + listOf() + } + + getCommentListUseCase.getCommentList(certId, sortValue) + .distinctUntilChanged() + .cachedIn(viewModelScope) + .collect { pagingData -> + _commentPagingData.value = pagingData + + getTotalCommentCount() + _updateCommentSuccess.emit(UiState.Init) + } + } + + private fun getTotalCommentCount() = viewModelScope.launch { + getCommentListUseCase.getTotalCommentCount() + .collect { + _totalCommentCount.value = it + } + } + + fun registerComment(certId: Long, content: String) = viewModelScope.launch { + registerCommentUseCase.invoke(registerCommentRequest = RegisterCommentRequest(content, certId)).fold( + onSuccess = { + _updateCommentSuccess.emit(UiState.Success(true)) + }, + onFailure = { + _updateCommentSuccess.emit(UiState.Failure(it.message.toString())) + } + ) + } + + fun likeComment(commentId: Long) = viewModelScope.launch { + likeCommentUseCase.invoke(commentId).fold( + onSuccess = {}, + onFailure = {} + ) + } + + fun deleteComment(commentId: Long) = viewModelScope.launch { + deleteCommentUseCase.invoke(commentId).fold( + onSuccess = { + _updateCommentSuccess.emit(UiState.Success(true)) + }, + onFailure = { + _updateCommentSuccess.emit(UiState.Failure(it.message.toString())) + } + ) + } + + fun reportComment(commentId: Long, content: String, block: Boolean) = viewModelScope.launch { + reportCommentUseCase.invoke(commentId, ReportCommentRequest(content, block)).fold( + onSuccess = {}, + onFailure = {} + ) } } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/chip/CommentArrayButton.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/chip/CommentArrayButton.kt index da31a533..0c7a9cf8 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/chip/CommentArrayButton.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/chip/CommentArrayButton.kt @@ -13,22 +13,19 @@ import org.sopt.certi.core.util.noRippleClickable import org.sopt.certi.core.util.roundedBackgroundWithBorder import org.sopt.certi.core.util.screenHeightDp import org.sopt.certi.core.util.screenWidthDp +import org.sopt.certi.presentation.type.CommentSortType import org.sopt.certi.ui.theme.CertiTheme -enum class CommentArrayButtonType { - Famous, Recent -} - @Composable fun CommentArrayButton( - commentArrayButtonType: CommentArrayButtonType, + commentSortType: CommentSortType, isSelected: Boolean, - selectOnClick: (CommentArrayButtonType) -> Unit, + selectOnClick: (CommentSortType) -> Unit, modifier: Modifier = Modifier ) { - val label = when (commentArrayButtonType) { - CommentArrayButtonType.Famous -> stringResource(R.string.comment_label_famous) - CommentArrayButtonType.Recent -> stringResource(R.string.comment_label_recent) + val label = when (commentSortType) { + CommentSortType.Famous -> stringResource(R.string.comment_label_famous) + CommentSortType.Recent -> stringResource(R.string.comment_label_recent) } Box( @@ -40,7 +37,7 @@ fun CommentArrayButton( backgroundColor = CertiTheme.colors.white ) .noRippleClickable { - selectOnClick(commentArrayButtonType) + selectOnClick(commentSortType) } ) { Text( @@ -56,7 +53,7 @@ fun CommentArrayButton( @Composable private fun PreviewCommentArrayButton() { CommentArrayButton( - commentArrayButtonType = CommentArrayButtonType.Recent, + commentSortType = CommentSortType.Recent, isSelected = true, selectOnClick = {} ) diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentEmptyView.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentEmptyView.kt new file mode 100644 index 00000000..c810133a --- /dev/null +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentEmptyView.kt @@ -0,0 +1,51 @@ +package org.sopt.certi.presentation.ui.certdetail.component.comment + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.certi.R +import org.sopt.certi.core.util.heightForScreenPercentage +import org.sopt.certi.ui.theme.CertiTheme + +@Composable +fun CommentEmptyView( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.img_empty), + contentDescription = null + ) + + Spacer(Modifier.heightForScreenPercentage(20.dp)) + + Text( + text = stringResource(R.string.comment_empty), + style = CertiTheme.typography.caption.regular_14, + color = CertiTheme.colors.gray400, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +private fun PreviewCommentEmptyView() { + CommentEmptyView() +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentItem.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentItem.kt index 69001c02..0f578e04 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentItem.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/comment/CommentItem.kt @@ -34,7 +34,7 @@ import org.sopt.certi.core.util.screenHeightDp import org.sopt.certi.core.util.screenWidthDp import org.sopt.certi.core.util.widthForScreenPercentage import org.sopt.certi.domain.model.comment.CommentItemData -import org.sopt.certi.domain.type.CertAcquireStateType +import org.sopt.certi.domain.type.CertStateType import org.sopt.certi.ui.theme.CertiTheme @Composable @@ -53,14 +53,18 @@ fun CommentItem( var likeCountStatus by remember { mutableStateOf(commentData.likeCount) } when (commentData.state) { - CertAcquireStateType.ACQUIRED -> { - acquireStateText = stringResource(R.string.comment_state_acquired) + CertStateType.ANTICIPATED -> { + acquireStateText = stringResource(R.string.comment_state_pre) acquireStateTextColor = CertiTheme.colors.purpleBlue } - CertAcquireStateType.PRE -> { - acquireStateText = stringResource(R.string.comment_state_pre) + CertStateType.ACQUISITION -> { + acquireStateText = stringResource(R.string.comment_state_acquired) acquireStateTextColor = CertiTheme.colors.gray300 } + CertStateType.NORMAL -> { + acquireStateText = "ERROR" + acquireStateTextColor = CertiTheme.colors.error + } } Column( @@ -140,19 +144,21 @@ fun CommentItem( Spacer(Modifier.widthForScreenPercentage(8.dp)) - Text( - text = stringResource(R.string.comment_report), - style = CertiTheme.typography.caption.semibold_12, - color = CertiTheme.colors.gray400, - modifier = Modifier.noRippleClickable { - reportOnClick() - } - ) - - Spacer(Modifier.widthForScreenPercentage(8.dp)) + if (commentData.userId != myUserId) { + Text( + text = stringResource(R.string.comment_report), + style = CertiTheme.typography.caption.semibold_12, + color = CertiTheme.colors.gray400, + modifier = Modifier.noRippleClickable { + reportOnClick() + } + ) + + Spacer(Modifier.widthForScreenPercentage(8.dp)) + } Text( - text = commentData.createdTime, + text = commentData.createdTime.split("T")[0].replace("-", "."), style = CertiTheme.typography.caption.semibold_12, color = CertiTheme.colors.gray400 ) @@ -203,8 +209,8 @@ fun CommentItemPreview() { content = "이 자격증 너무 좋은 것 같아요! 다들 꼭 따세요~이 자격증 너무 좋은 것 같아요! 다들 꼭 따세요~이 자격증 너무 좋은 것 같아요! 다들 꼭 따세요~", userMajor = "컴퓨터공학과", userJob = "개발자", - state = CertAcquireStateType.ACQUIRED, - createdTime = "2024.07.21", + state = CertStateType.ANTICIPATED, + createdTime = "2026-02-05T03:25:01.699655", lastModifiedTime = "2024.07.21", isLike = true, likeCount = 15 diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/dialog/ReportCommentDialog.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/dialog/ReportCommentDialog.kt new file mode 100644 index 00000000..daf4d46b --- /dev/null +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/component/dialog/ReportCommentDialog.kt @@ -0,0 +1,222 @@ +package org.sopt.certi.presentation.ui.certdetail.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.sopt.certi.R +import org.sopt.certi.core.util.heightForScreenPercentage +import org.sopt.certi.core.util.noRippleClickable +import org.sopt.certi.core.util.screenHeightDp +import org.sopt.certi.core.util.screenWidthDp +import org.sopt.certi.core.util.widthForScreenPercentage +import org.sopt.certi.ui.theme.CertiTheme + +@Composable +fun ReportCommentDialog( + onReportClick: (content: String, block: Boolean) -> Unit, + onDismissClick: () -> Unit +) { + var contentText by remember { mutableStateOf("") } + var blockState by remember { mutableStateOf(false) } + + Dialog(onDismissRequest = onDismissClick) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(CertiTheme.colors.white) + .padding( + top = screenHeightDp(18.dp), + bottom = screenHeightDp(20.dp), + start = screenHeightDp(20.dp), + end = screenHeightDp(20.dp) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.dialog_comment_report_title), + style = CertiTheme.typography.caption.semibold_14, + color = CertiTheme.colors.black + ) + + Spacer(Modifier.weight(1f)) + + Icon( + painter = painterResource(R.drawable.ic_close_20), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .widthForScreenPercentage(24.dp) + .heightForScreenPercentage(24.dp) + ) + } + + Spacer(Modifier.heightForScreenPercentage(20.dp)) + + Text( + text = stringResource(R.string.dialog_comment_report_content_title), + style = CertiTheme.typography.caption.semibold_12, + color = CertiTheme.colors.gray400, + modifier = Modifier.align(Alignment.Start) + ) + + Spacer(Modifier.heightForScreenPercentage(4.dp)) + + BasicTextField( + value = contentText, + onValueChange = { + if (contentText.length < 100) { + contentText = it + } + }, + decorationBox = { innerTextField -> + if (contentText.isEmpty()) { + Text( + text = stringResource(R.string.dialog_comment_report_content_hint), + style = CertiTheme.typography.caption.regular_12, + color = CertiTheme.colors.gray300 + ) + } + innerTextField() + }, + cursorBrush = SolidColor(CertiTheme.colors.gray600), + textStyle = CertiTheme.typography.caption.regular_12.copy( + color = CertiTheme.colors.gray600 + ), + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = screenHeightDp(114.dp)) + .clip(RoundedCornerShape(12.dp)) + .background(CertiTheme.colors.gray0) + .padding(vertical = screenHeightDp(12.dp), horizontal = screenHeightDp(12.dp)) + ) + + Spacer(Modifier.heightForScreenPercentage(4.dp)) + + Text( + text = "${contentText.length}/100", + style = CertiTheme.typography.caption.regular_10, + color = CertiTheme.colors.gray300, + modifier = Modifier.align(Alignment.End) + ) + + Spacer(Modifier.heightForScreenPercentage(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(if (blockState) R.drawable.ic_checkbox_fill else R.drawable.ic_checkbox_empty), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .noRippleClickable { + blockState = !blockState + } + ) + + Spacer(Modifier.widthForScreenPercentage(8.dp)) + + Text( + text = stringResource(R.string.dialog_comment_report_block_title), + style = CertiTheme.typography.caption.semibold_12, + color = CertiTheme.colors.gray600 + ) + } + + Spacer(Modifier.heightForScreenPercentage(8.dp)) + + Row { + Text( + text = " ∙ ", + style = CertiTheme.typography.caption.regular_12, + color = CertiTheme.colors.gray400 + ) + + Text( + text = stringResource(R.string.dialog_comment_report_note_p1), + style = CertiTheme.typography.caption.regular_12, + color = CertiTheme.colors.gray400 + ) + } + + Spacer(Modifier.heightForScreenPercentage(8.dp)) + + Row { + Text( + text = " ∙ ", + style = CertiTheme.typography.caption.regular_12, + color = CertiTheme.colors.gray400 + ) + + Text( + text = stringResource(R.string.dialog_comment_report_note_p2), + style = CertiTheme.typography.caption.regular_12, + color = CertiTheme.colors.gray400 + ) + } + + Spacer(Modifier.heightForScreenPercentage(14.dp)) + + Box( + modifier = Modifier + .background( + color = CertiTheme.colors.white, + shape = RoundedCornerShape(100.dp) + ) + .border(width = 1.dp, color = CertiTheme.colors.mainBlue, shape = RoundedCornerShape(100.dp)) + .padding(vertical = screenHeightDp(8.dp), horizontal = screenWidthDp(22.dp)) + .align(Alignment.End) + .noRippleClickable { + onReportClick(contentText, blockState) + }, + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.dialog_comment_report_confirm), + style = CertiTheme.typography.caption.semibold_12, + color = CertiTheme.colors.mainBlue + ) + } + } + } +} + +@Preview +@Composable +private fun PreviewReportCommentDialog() { + ReportCommentDialog( + onReportClick = { _, _ -> + }, + onDismissClick = { + } + ) +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/screen/CertDetailCommentScreen.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/screen/CertDetailCommentScreen.kt index e394a1ea..ebcdaf1e 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/screen/CertDetailCommentScreen.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/screen/CertDetailCommentScreen.kt @@ -19,12 +19,14 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Icon 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 @@ -42,109 +44,134 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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 androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf import org.sopt.certi.R +import org.sopt.certi.core.component.dialog.CertiContentDialog +import org.sopt.certi.core.state.UiState import org.sopt.certi.core.util.heightForScreenPercentage import org.sopt.certi.core.util.noRippleClickable import org.sopt.certi.core.util.screenHeightDp import org.sopt.certi.core.util.screenWidthDp import org.sopt.certi.core.util.widthForScreenPercentage -import org.sopt.certi.domain.model.comment.CommentData import org.sopt.certi.domain.model.comment.CommentItemData -import org.sopt.certi.domain.type.CertAcquireStateType +import org.sopt.certi.domain.type.CertStateType +import org.sopt.certi.presentation.type.CommentSortType +import org.sopt.certi.presentation.ui.certdetail.CertDetailViewModel import org.sopt.certi.presentation.ui.certdetail.component.chip.CommentArrayButton -import org.sopt.certi.presentation.ui.certdetail.component.chip.CommentArrayButtonType +import org.sopt.certi.presentation.ui.certdetail.component.comment.CommentEmptyView import org.sopt.certi.presentation.ui.certdetail.component.comment.CommentItem +import org.sopt.certi.presentation.ui.certdetail.component.dialog.ReportCommentDialog +import org.sopt.certi.presentation.ui.certdetail.sideeffect.CommentDialogState import org.sopt.certi.ui.theme.CertiTheme @Composable -fun CertDetailCommentRoute() { - val dummyCommentData = CommentData( - content = listOf( - CommentItemData( - commentId = 57, - userId = 1, - nickName = "이성민", - content = "댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 58, - userId = 1, - nickName = "이성민2", - content = "댓글입니다.2댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 59, - userId = 1, - nickName = "이성민3", - content = "댓글입니다.3댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 60, - userId = 1, - nickName = "이성민4", - content = "댓글입니다.4댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 61, - userId = 1, - nickName = "이성민5", - content = "댓글입니다.5댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ) - ), - totalPages = 1, - totalElements = 5, - isLast = false +fun CertDetailCommentRoute( + certificationId: Long, + certStateType: CertStateType, + viewModel: CertDetailViewModel = hiltViewModel() +) { + var commentSortType by remember { mutableStateOf(CommentSortType.Famous) } + val commentList = viewModel.commentPagingData.collectAsLazyPagingItems() + val totalCommentCount by viewModel.totalCommentCount.collectAsStateWithLifecycle() + val myUserId by viewModel.myUserId.collectAsStateWithLifecycle() + + val listState = rememberLazyListState() + + var commentDialogState by remember { mutableStateOf(CommentDialogState.Hidden) } + + LaunchedEffect(commentSortType) { + viewModel.getMyUserId() + viewModel.getCommentList(certificationId, commentSortType) + } + + LaunchedEffect(Unit) { + viewModel.updateCommentSuccess.collect { uiState -> + when (uiState) { + is UiState.Success -> { + commentList.refresh() + + delay(500) + listState.animateScrollToItem(commentList.itemCount) + } + else -> {} + } + } + } + + CertDetailCommentScreen( + commentData = commentList, + totalCommentCount = totalCommentCount, + myUserId = myUserId, + certStateType = certStateType, + listState = listState, + changeSortType = { changedSortType -> + commentSortType = changedSortType + }, + writeComment = { content -> + viewModel.registerComment(certId = certificationId, content = content) + }, + likeOnClick = { like, commentId -> + viewModel.likeComment(commentId) + }, + reportOnClick = { commentId -> + commentDialogState = CommentDialogState.ShowReportCommentDialog(commentId) + }, + deleteOnClick = { commentId -> + commentDialogState = CommentDialogState.ShowDeleteCommentDialog(commentId) + } ) - CertDetailCommentScreen(commentData = dummyCommentData, myUserId = 0) + when (val state = commentDialogState) { + is CommentDialogState.Hidden -> { } + is CommentDialogState.ShowDeleteCommentDialog -> { + CertiContentDialog( + titleText = stringResource(R.string.dialog_comment_delete_title), + contentText = stringResource(R.string.dialog_comment_delete_content), + onConfirmClick = { + viewModel.deleteComment(state.commentId) + commentDialogState = CommentDialogState.Hidden + }, + onDismissClick = { + commentDialogState = CommentDialogState.Hidden + } + ) + } + is CommentDialogState.ShowReportCommentDialog -> { + ReportCommentDialog( + onReportClick = { content, block -> + viewModel.reportComment(state.commentId, content, block) + commentDialogState = CommentDialogState.Hidden + }, + onDismissClick = { + commentDialogState = CommentDialogState.Hidden + } + ) + } + } } @OptIn(ExperimentalLayoutApi::class) @Composable fun CertDetailCommentScreen( - commentData: CommentData, + commentData: LazyPagingItems, + totalCommentCount: Int, myUserId: Long, + certStateType: CertStateType, + listState: LazyListState = rememberLazyListState(), + changeSortType: (CommentSortType) -> Unit = {}, writeComment: (content: String) -> Unit = {}, likeOnClick: (like: Boolean, commentId: Long) -> Unit = { _, _ -> }, reportOnClick: (commentId: Long) -> Unit = {}, deleteOnClick: (commentId: Long) -> Unit = {} ) { - var commentSortType by remember { mutableStateOf(CommentArrayButtonType.Famous) } + var commentSortType by remember { mutableStateOf(CommentSortType.Famous) } var commentText by remember { mutableStateOf("") } @@ -177,50 +204,62 @@ fun CertDetailCommentScreen( verticalAlignment = Alignment.CenterVertically ) { CommentArrayButton( - commentArrayButtonType = CommentArrayButtonType.Famous, - isSelected = commentSortType == CommentArrayButtonType.Famous, + commentSortType = CommentSortType.Famous, + isSelected = commentSortType == CommentSortType.Famous, selectOnClick = { - commentSortType = CommentArrayButtonType.Famous + commentSortType = CommentSortType.Famous + changeSortType(commentSortType) } ) Spacer(Modifier.widthForScreenPercentage(8.dp)) CommentArrayButton( - commentArrayButtonType = CommentArrayButtonType.Recent, - isSelected = commentSortType == CommentArrayButtonType.Recent, + commentSortType = CommentSortType.Recent, + isSelected = commentSortType == CommentSortType.Recent, selectOnClick = { - commentSortType = CommentArrayButtonType.Recent + commentSortType = CommentSortType.Recent + changeSortType(commentSortType) } ) Spacer(Modifier.weight(1f)) Text( - text = stringResource(R.string.comment_count, commentData.totalElements), + text = stringResource(R.string.comment_count, totalCommentCount), style = CertiTheme.typography.caption.regular_14, color = CertiTheme.colors.gray400 ) } - LazyColumn( - contentPadding = PaddingValues(top = screenHeightDp(12.dp)), - verticalArrangement = Arrangement.spacedBy(screenHeightDp(12.dp)) - ) { - itemsIndexed(commentData.content) { _, item -> - CommentItem( - commentData = item, - myUserId = myUserId, - likeOnClick = { like -> - likeOnClick(like, item.commentId) - }, - reportOnClick = { - reportOnClick(item.commentId) - }, - deleteOnClick = { - deleteOnClick(item.commentId) + if (commentData.itemCount == 0) { + CommentEmptyView() + } else { + LazyColumn( + state = listState, + contentPadding = PaddingValues(top = screenHeightDp(12.dp)), + verticalArrangement = Arrangement.spacedBy(screenHeightDp(12.dp)) + ) { + items( + count = commentData.itemCount, + key = commentData.itemKey { it.commentId } + ) { index -> + commentData[index]?.let { comment -> + CommentItem( + commentData = comment, + myUserId = myUserId, + likeOnClick = { like -> + likeOnClick(like, comment.commentId) + }, + reportOnClick = { + reportOnClick(comment.commentId) + }, + deleteOnClick = { + deleteOnClick(comment.commentId) + } + ) } - ) + } } } } @@ -258,44 +297,67 @@ fun CertDetailCommentScreen( .padding(end = screenWidthDp(12.dp)), verticalAlignment = Alignment.CenterVertically ) { - BasicTextField( - value = commentText, - onValueChange = { commentText = it }, - singleLine = true, - maxLines = 1, - textStyle = CertiTheme.typography.caption.regular_14.copy( - color = CertiTheme.colors.black - ), - cursorBrush = SolidColor(CertiTheme.colors.black), - decorationBox = { innerTextField -> - if (commentText.isEmpty()) { - Text( - text = stringResource(R.string.comment_hint), - style = CertiTheme.typography.caption.semibold_14, - color = CertiTheme.colors.gray300 - ) - } + if (certStateType == CertStateType.NORMAL) { + Spacer(Modifier.widthForScreenPercentage(12.dp)) - innerTextField() - }, - modifier = Modifier - .weight(1f) - .padding(horizontal = screenWidthDp(10.dp)) - ) + Icon( + painter = painterResource(R.drawable.ic_lock), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .widthForScreenPercentage(20.dp) + .heightForScreenPercentage(20.dp) + ) - Spacer(Modifier.widthForScreenPercentage(12.dp)) + Spacer(Modifier.widthForScreenPercentage(8.dp)) - Icon( - painter = painterResource(R.drawable.ic_send), - tint = if (commentText.isEmpty()) Color.Unspecified else CertiTheme.colors.purpleBlue, - modifier = Modifier - .widthForScreenPercentage(24.dp) - .heightForScreenPercentage(24.dp) - .noRippleClickable { - writeComment(commentText) + Text( + text = stringResource(R.string.comment_write_unavailable), + style = CertiTheme.typography.caption.semibold_14, + color = CertiTheme.colors.gray300 + ) + } else { + BasicTextField( + value = commentText, + onValueChange = { commentText = it }, + singleLine = true, + maxLines = 1, + textStyle = CertiTheme.typography.caption.regular_14.copy( + color = CertiTheme.colors.black + ), + cursorBrush = SolidColor(CertiTheme.colors.black), + decorationBox = { innerTextField -> + if (commentText.isEmpty()) { + Text( + text = stringResource(R.string.comment_hint), + style = CertiTheme.typography.caption.semibold_14, + color = CertiTheme.colors.gray300 + ) + } + + innerTextField() }, - contentDescription = null - ) + modifier = Modifier + .weight(1f) + .padding(horizontal = screenWidthDp(10.dp)) + ) + + Spacer(Modifier.widthForScreenPercentage(12.dp)) + + Icon( + painter = painterResource(R.drawable.ic_send), + tint = if (commentText.isEmpty()) Color.Unspecified else CertiTheme.colors.purpleBlue, + modifier = Modifier + .widthForScreenPercentage(24.dp) + .heightForScreenPercentage(24.dp) + .noRippleClickable { + writeComment(commentText) + + commentText = "" + }, + contentDescription = null + ) + } } } } @@ -304,78 +366,22 @@ fun CertDetailCommentScreen( @Preview(showBackground = true) @Composable private fun PreviewCertDetailCommentScreen() { - val dummyCommentData = CommentData( - content = listOf( - CommentItemData( - commentId = 57, - userId = 1, - nickName = "이성민", - content = "댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 58, - userId = 1, - nickName = "이성민2", - content = "댓글입니다.2댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 59, - userId = 1, - nickName = "이성민3", - content = "댓글입니다.3댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 60, - userId = 1, - nickName = "이성민4", - content = "댓글입니다.4댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ), - CommentItemData( - commentId = 61, - userId = 1, - nickName = "이성민5", - content = "댓글입니다.5댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.댓글입니다.", - userMajor = "전산학/컴퓨터공학", - userJob = "IT/인터넷", - state = CertAcquireStateType.ACQUIRED, - likeCount = 3, - createdTime = "2025-11-15T23:00:38.042089", - lastModifiedTime = "2025-11-15T23:00:38.042089", - isLike = false - ) - ), - totalPages = 1, - totalElements = 4, - isLast = false + val dummyItem = CommentItemData( + commentId = 1L, + userId = 1L, + nickName = "홍길동", + content = "이 자격증 정말 추천합니다! 취업에 많은 도움이 되었어요.", + userMajor = "컴퓨터공학", + userJob = "백엔드 개발자", + state = CertStateType.ACQUISITION, + createdTime = "2024-01-15", + lastModifiedTime = "2024-01-15", + isLike = false, + likeCount = 5 ) + val dummyPagingData = PagingData.from(listOf(dummyItem)) + val dummyFlow = flowOf(dummyPagingData) + val dummyList = dummyFlow.collectAsLazyPagingItems() - CertDetailCommentScreen(commentData = dummyCommentData, myUserId = 0) + CertDetailCommentScreen(commentData = dummyList, totalCommentCount = 1, myUserId = 0, certStateType = CertStateType.NORMAL) } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/sideeffect/CommentDialogState.kt b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/sideeffect/CommentDialogState.kt new file mode 100644 index 00000000..a28f411f --- /dev/null +++ b/app/src/main/java/org/sopt/certi/presentation/ui/certdetail/sideeffect/CommentDialogState.kt @@ -0,0 +1,7 @@ +package org.sopt.certi.presentation.ui.certdetail.sideeffect + +sealed interface CommentDialogState { + data object Hidden : CommentDialogState + data class ShowDeleteCommentDialog(val commentId: Long) : CommentDialogState + data class ShowReportCommentDialog(val commentId: Long) : CommentDialogState +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/home/HomeViewModel.kt b/app/src/main/java/org/sopt/certi/presentation/ui/home/HomeViewModel.kt index 7f26844e..4fe2d548 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/home/HomeViewModel.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/home/HomeViewModel.kt @@ -75,6 +75,7 @@ class HomeViewModel @Inject constructor( .onSuccess { result -> _userInfoLoadState.value = UiState.Success(result) tokenManager.saveNickName(result.nickname) + tokenManager.saveUserId(result.userId) } .onFailure { _userInfoLoadState.value = UiState.Failure(it.toString()) diff --git a/app/src/main/res/drawable/ic_checkbox_empty.xml b/app/src/main/res/drawable/ic_checkbox_empty.xml new file mode 100644 index 00000000..5917a48d --- /dev/null +++ b/app/src/main/res/drawable/ic_checkbox_empty.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_checkbox_fill.xml b/app/src/main/res/drawable/ic_checkbox_fill.xml new file mode 100644 index 00000000..882124aa --- /dev/null +++ b/app/src/main/res/drawable/ic_checkbox_fill.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 00000000..c8bb32eb --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d426670d..581763d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -279,7 +279,7 @@ 취득 완료 - 취득 예장 + 취득 예정 삭제 좋아요 %s 신고 @@ -288,6 +288,18 @@ 최신순 댓글(%s) 댓글을 입력하세요 - + 아직 댓글이 없습니다.\n가장 먼저 댓글을 작성해보세요. + 자격증 취득예정/완료 후 댓글 작성이 가능합니다. + 삭제하시겠습니까? + 삭제하게 되면 복구할 수 없어요. + 신고하기 + 신고 내용 + 내용을 입력해주세요. + 해당 유저 차단하기 + 신고된 콘텐츠는 관리자가 검토하며, 필요 시 댓글 삭제, 계정 제한 등의 조치가 이루어집니다. + 차단 기능을 사용하면, 관리자의 확인을 거쳐 해당 사용자의 댓글이 사용자에게 노출되지 않도록 처리됩니다. + 제출 + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d9ee61e..153bc937 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,10 @@ firebaseCrashlyticsBuildtools = "3.0.4" # Calendar calendar = "2.6.0" +# Paging +roomRuntime = "2.8.2" +pagingCommonAndroid = "3.3.6" + [libraries] # Core androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -112,6 +116,12 @@ firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "fireb # Calendar kizitonwose-calendar = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendar" } +# Paging +androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomRuntime" } +androidx-paging-runtime-android = { module = "androidx.paging:paging-runtime", version.ref = "pagingCommonAndroid" } +androidx-paging-runtime = { module = "androidx.paging:paging-compose", version.ref = "pagingCommonAndroid" } +androidx-paging-common = { module = "androidx.paging:paging-common-ktx", version.ref = "pagingCommonAndroid"} + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }