diff --git a/.idea/misc.xml b/.idea/misc.xml index 8bc4a8ec4..cf0ba30d9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,10 +1,11 @@ + diff --git a/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepository.kt b/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepository.kt index ef3dbe146..1568281c9 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepository.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepository.kt @@ -3,6 +3,7 @@ package com.android.mediproject.core.data.remote.comments import androidx.paging.PagingData import com.android.mediproject.core.model.comments.CommentChangedResponse import com.android.mediproject.core.model.comments.CommentListResponse +import com.android.mediproject.core.model.comments.LikeResponse import com.android.mediproject.core.model.comments.MyCommentDto import com.android.mediproject.core.model.requestparameters.DeleteCommentParameter import com.android.mediproject.core.model.requestparameters.EditCommentParameter @@ -39,5 +40,5 @@ interface CommentsRepository { /** * 댓글 좋아요 클릭 */ - fun likeComment(parameter: LikeCommentParameter): Flow> + fun likeComment(parameter: LikeCommentParameter): Flow> } \ No newline at end of file diff --git a/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepositoryImpl.kt b/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepositoryImpl.kt index 65be1cc32..c56b6cc24 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepositoryImpl.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/remote/comments/CommentsRepositoryImpl.kt @@ -7,6 +7,7 @@ import com.android.mediproject.core.common.AWS_LOAD_PAGE_SIZE import com.android.mediproject.core.data.remote.sign.SignRepository import com.android.mediproject.core.model.comments.CommentChangedResponse import com.android.mediproject.core.model.comments.CommentListResponse +import com.android.mediproject.core.model.comments.LikeResponse import com.android.mediproject.core.model.comments.MyCommentDto import com.android.mediproject.core.model.remote.token.TokenState import com.android.mediproject.core.model.requestparameters.DeleteCommentParameter @@ -23,7 +24,8 @@ import kotlinx.coroutines.flow.lastOrNull import javax.inject.Inject class CommentsRepositoryImpl @Inject constructor( - private val commentsDataSource: CommentsDataSource, private val signRepository: SignRepository) : CommentsRepository { + private val commentsDataSource: CommentsDataSource, private val signRepository: SignRepository +) : CommentsRepository { override fun getCommentsForAMedicine(medicineId: Long): Flow> = Pager(config = PagingConfig(pageSize = AWS_LOAD_PAGE_SIZE, prefetchDistance = 5), pagingSourceFactory = { CommentsListDataSourceImpl(commentsDataSource, medicineId) @@ -70,7 +72,7 @@ class CommentsRepositoryImpl @Inject constructor( } } - override fun likeComment(parameter: LikeCommentParameter): Flow> = channelFlow { + override fun likeComment(parameter: LikeCommentParameter): Flow> = channelFlow { checkToken().collectLatest { tokenState -> tokenState.onSuccess { commentsDataSource.likeComment(parameter).collectLatest { diff --git a/core/data/src/main/java/com/android/mediproject/core/data/remote/sign/SignRepositoryImpl.kt b/core/data/src/main/java/com/android/mediproject/core/data/remote/sign/SignRepositoryImpl.kt index 6015c749b..e80dbf9c0 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/remote/sign/SignRepositoryImpl.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/remote/sign/SignRepositoryImpl.kt @@ -45,7 +45,7 @@ class SignRepositoryImpl @Inject constructor( signInResult.onSuccess { // 내 계정 정보 메모리에 저장 userInfoRepository.updateMyAccountInfo(AccountState.SignedIn(it._userId!!.toLong(), it._nickName!!, it._email!!)) - saveMyAccountInfo(if (signInParameter.isSavedEmail) it._email!! else "", it._nickName!!, it._userId!!.toLong()) + saveMyAccountInfo(it._email!!, it._nickName!!, it._userId!!.toLong()) } } @@ -78,7 +78,7 @@ class SignRepositoryImpl @Inject constructor( signUpResult.onSuccess { // 내 계정 정보 메모리에 저장 userInfoRepository.updateMyAccountInfo(AccountState.SignedIn(it._userId!!.toLong(), it._nickName!!, it._email!!)) - saveMyAccountInfo("", it._nickName!!, it._userId!!.toLong()) + saveMyAccountInfo(it._email!!, it._nickName!!, it._userId!!.toLong()) } } diff --git a/core/data/src/main/java/com/android/mediproject/core/data/remote/user/UserInfoRepositoryImpl.kt b/core/data/src/main/java/com/android/mediproject/core/data/remote/user/UserInfoRepositoryImpl.kt index f04d1ef08..4c035fda0 100644 --- a/core/data/src/main/java/com/android/mediproject/core/data/remote/user/UserInfoRepositoryImpl.kt +++ b/core/data/src/main/java/com/android/mediproject/core/data/remote/user/UserInfoRepositoryImpl.kt @@ -16,7 +16,6 @@ class UserInfoRepositoryImpl @Inject constructor( private val userInfoDataSource: UserInfoDataSource, private val appDataStore: AppDataStore ) : UserInfoRepository { - private val _myAccountInfo = MutableStateFlow(AccountState.Unknown) override val myAccountInfo get() = _myAccountInfo as StateFlow diff --git a/core/datastore/src/main/java/com/android/mediproject/core/datastore/TokenServer.kt b/core/datastore/src/main/java/com/android/mediproject/core/datastore/TokenServer.kt index 23790ac4f..15d37a87a 100644 --- a/core/datastore/src/main/java/com/android/mediproject/core/datastore/TokenServer.kt +++ b/core/datastore/src/main/java/com/android/mediproject/core/datastore/TokenServer.kt @@ -19,7 +19,7 @@ class TokenServerImpl @Inject constructor() : TokenServer { */ override val currentTokens: EndpointTokenState get() { - val token = tokens.replayCache.firstOrNull() + val token = tokens.replayCache.lastOrNull() return if (token == null) { EndpointTokenState.NoToken } else { diff --git a/core/domain/src/main/java/com/android/mediproject/core/domain/CommentsUseCase.kt b/core/domain/src/main/java/com/android/mediproject/core/domain/CommentsUseCase.kt index 7c11d2e3f..51a70b05d 100644 --- a/core/domain/src/main/java/com/android/mediproject/core/domain/CommentsUseCase.kt +++ b/core/domain/src/main/java/com/android/mediproject/core/domain/CommentsUseCase.kt @@ -5,6 +5,7 @@ import androidx.paging.flatMap import com.android.mediproject.core.common.network.Dispatcher import com.android.mediproject.core.common.network.MediDispatchers import com.android.mediproject.core.data.remote.comments.CommentsRepository +import com.android.mediproject.core.data.remote.user.UserInfoRepository import com.android.mediproject.core.model.comments.CommentDto import com.android.mediproject.core.model.comments.MyCommentDto import com.android.mediproject.core.model.comments.toDto @@ -13,6 +14,8 @@ import com.android.mediproject.core.model.requestparameters.EditCommentParameter import com.android.mediproject.core.model.requestparameters.LikeCommentParameter import com.android.mediproject.core.model.requestparameters.NewCommentParameter import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest @@ -21,19 +24,31 @@ import javax.inject.Inject class CommentsUseCase @Inject constructor( private val commentsRepository: CommentsRepository, - @Dispatcher(MediDispatchers.Default) private val defaultDispatcher: CoroutineDispatcher) { + private val userInfoRepository: UserInfoRepository, + @Dispatcher(MediDispatchers.Default) private val defaultDispatcher: CoroutineDispatcher +) { + + val scrollChannel = Channel(capacity = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) /** * 약에 대한 댓글을 가져오는 메서드입니다. * * @param medicineId 약의 고유 번호 */ - fun getCommentsForAMedicine(medicineId: Long): Flow> = channelFlow { + fun getCommentsForAMedicine(medicineId: Long, myUserId: Long): Flow> = channelFlow { commentsRepository.getCommentsForAMedicine(medicineId).collectLatest { pagingData -> val result = pagingData.flatMap { (it.replies.map { reply -> - reply.toDto() - }.reversed()) + listOf(it.toDto()) + reply.toDto().apply { + reply.likeList.forEach { like -> + if (like.userId == myUserId) this.isLiked = true + } + } + }.reversed()) + listOf(it.toDto().apply { + it.likeList.forEach { like -> + if (like.userId == myUserId) this.isLiked = true + } + }) } send(result) } @@ -61,7 +76,9 @@ class CommentsUseCase @Inject constructor( */ fun applyNewComment(parameter: NewCommentParameter): Flow> = commentsRepository.applyNewComment(parameter).mapLatest { result -> - result.fold(onSuccess = { Result.success(Unit) }, onFailure = { Result.failure(it) }) + result.fold(onSuccess = { + Result.success(Unit) + }, onFailure = { Result.failure(it) }) } diff --git a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentChangedResponse.kt b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentChangedResponse.kt index 3ddbc3676..1e4479c1e 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentChangedResponse.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentChangedResponse.kt @@ -8,5 +8,5 @@ import kotlinx.serialization.Serializable */ @Serializable data class CommentChangedResponse( - val commentId: Long + val commentId: Long = 0L, ) : BaseAwsQueryResponse() \ No newline at end of file diff --git a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentDto.kt b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentDto.kt index e25774e78..551abb43d 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentDto.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentDto.kt @@ -32,14 +32,16 @@ data class CommentDto( val content: String, val createdAt: String, val updatedAt: String, - var onClickReply: ((Int) -> Unit)?, - var onClickLike: ((Long) -> Unit)?, + var onClickReply: ((String, Long) -> Unit)?, + var onClickLike: ((Long, Boolean) -> Unit)?, var onClickDelete: ((Long) -> Unit)?, var onClickEdit: ((CommentDto, Int) -> Unit)?, var onClickApplyEdited: ((CommentDto) -> Unit)?, - var isMine: Boolean = false) { + var isMine: Boolean = false +) { var isEditing: Boolean = false + var isLiked: Boolean = false } diff --git a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentListResponse.kt b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentListResponse.kt index 13c0bc17b..eb293a6ad 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentListResponse.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/comments/CommentListResponse.kt @@ -7,31 +7,38 @@ import kotlinx.serialization.Serializable @Serializable data class CommentListResponse( - @SerialName("commentList") val commentList: List) : BaseAwsQueryResponse() { + @SerialName("commentList") val commentList: List +) : BaseAwsQueryResponse() { @Serializable data class Comment( @SerialName("CONTENT") val content: String, // 테스트 - 메인 댓글 1 @SerialName("createdAt") val createdAt: String, // 2023-06-02T10:43:12.435Z @SerialName("ID") val id: Long, // 2 - @SerialName("likeList") val likeList: List, @SerialName("MEDICINEID") val medicineId: Long, // 41 @SerialName("replies") val replies: List, @SerialName("SUBORDINATION") val subordination: Long, // 0 @SerialName("USERID") val userId: Long, // 2 + @SerialName("likeList") val likeList: List, @SerialName("nickname") val nickName: String, - @SerialName("updatedAt") val updatedAt: String) { - + @SerialName("updatedAt") val updatedAt: String + ) { @Serializable data class Reply( @SerialName("CONTENT") val content: String, // 테스트 - 서브 댓글 1 - 1 @SerialName("createdAt") val createdAt: String, // 2023-06-02T10:43:54.211Z @SerialName("ID") val id: Long, // 5 - @SerialName("likeList") val likeList: List, - @SerialName("MEDICINEID") val medicineId: Long, // 41 + @SerialName("likeList") val likeList: List, @SerialName("MEDICINEID") val medicineId: Long, // 41 @SerialName("SUBORDINATION") val subordination: Long, // 2 @SerialName("USERID") val userId: Long, // 3 - @SerialName("nickname") val nickName: String, - @SerialName("updatedAt") val updatedAt: String) + @SerialName("nickname") val nickName: String, @SerialName("updatedAt") val updatedAt: String + ) } -} \ No newline at end of file +} + +@Serializable +data class Like( + @SerialName("COMMENTID") val commentId: Long, // 2 + @SerialName("ID") val id: Long, // 1 + @SerialName("USERID") val userId: Long +) \ No newline at end of file diff --git a/core/model/src/main/java/com/android/mediproject/core/model/comments/LikeResponse.kt b/core/model/src/main/java/com/android/mediproject/core/model/comments/LikeResponse.kt new file mode 100644 index 000000000..e48daaf89 --- /dev/null +++ b/core/model/src/main/java/com/android/mediproject/core/model/comments/LikeResponse.kt @@ -0,0 +1,12 @@ +package com.android.mediproject.core.model.comments + +import com.android.mediproject.core.model.awscommon.BaseAwsQueryResponse +import kotlinx.serialization.Serializable + +/** + * 댓글 등록,수정,삭제,좋아요 처리 후 서버로 부터 받는 응답 + */ +@Serializable +data class LikeResponse( + val likeId: Long = 0L, +) : BaseAwsQueryResponse() \ No newline at end of file diff --git a/core/model/src/main/java/com/android/mediproject/core/model/requestparameters/LikeCommentParameter.kt b/core/model/src/main/java/com/android/mediproject/core/model/requestparameters/LikeCommentParameter.kt index 6c5940f88..2e02a2422 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/requestparameters/LikeCommentParameter.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/requestparameters/LikeCommentParameter.kt @@ -1,15 +1,16 @@ package com.android.mediproject.core.model.requestparameters -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** * 댓글 좋아요를 위한 파라미터 클래스입니다. * * @property commentId 댓글 id - * @property userId 사용자 id + * @property medicineId 약 id + * @property toLike 좋아요를 누를지 취소할지 여부 */ @Serializable data class LikeCommentParameter( - @SerialName("commentId") val commentId: Long, // 5 - @SerialName("userId") val userId: Long) \ No newline at end of file + val commentId: Long, // 5 + val medicineId: Long, val toLike: Boolean +) \ No newline at end of file diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSource.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSource.kt index ec4589049..32f5e6325 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSource.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSource.kt @@ -3,6 +3,7 @@ package com.android.mediproject.core.network.datasource.comments import androidx.paging.PagingData import com.android.mediproject.core.model.comments.CommentChangedResponse import com.android.mediproject.core.model.comments.CommentListResponse +import com.android.mediproject.core.model.comments.LikeResponse import com.android.mediproject.core.model.requestparameters.DeleteCommentParameter import com.android.mediproject.core.model.requestparameters.EditCommentParameter import com.android.mediproject.core.model.requestparameters.LikeCommentParameter @@ -20,5 +21,5 @@ interface CommentsDataSource { fun applyNewComment(parameter: NewCommentParameter): Flow> fun deleteComment(parameter: DeleteCommentParameter): Flow> - fun likeComment(likeCommentParameter: LikeCommentParameter): Flow> + fun likeComment(likeCommentParameter: LikeCommentParameter): Flow> } \ No newline at end of file diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSourceImpl.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSourceImpl.kt index 21edbce9c..cc897b187 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSourceImpl.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/comments/CommentsDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.android.mediproject.core.network.datasource.comments import androidx.paging.PagingData import com.android.mediproject.core.model.comments.CommentChangedResponse import com.android.mediproject.core.model.comments.CommentListResponse +import com.android.mediproject.core.model.comments.LikeResponse import com.android.mediproject.core.model.requestparameters.DeleteCommentParameter import com.android.mediproject.core.model.requestparameters.EditCommentParameter import com.android.mediproject.core.model.requestparameters.LikeCommentParameter @@ -14,7 +15,8 @@ import kotlinx.coroutines.flow.flow import javax.inject.Inject class CommentsDataSourceImpl @Inject constructor( - private val awsNetworkApi: AwsNetworkApi) : CommentsDataSource { + private val awsNetworkApi: AwsNetworkApi +) : CommentsDataSource { /** * 약품에 대한 댓글 리스트를 가져온다. @@ -57,8 +59,11 @@ class CommentsDataSourceImpl @Inject constructor( }).also { emit(it) } } - override fun likeComment(parameter: LikeCommentParameter): Flow> = flow { - awsNetworkApi.likeComment(parameter).onResponse().fold(onSuccess = { response -> + override fun likeComment(parameter: LikeCommentParameter): Flow> = flow { + parameter.let { + if (it.toLike) awsNetworkApi.likeComment(parameter.medicineId, parameter.commentId) + else awsNetworkApi.unlikeComment(parameter.medicineId, parameter.commentId) + }.onResponse().fold(onSuccess = { response -> Result.success(response) }, onFailure = { Result.failure(it) diff --git a/core/network/src/main/java/com/android/mediproject/core/network/module/AwsNetwork.kt b/core/network/src/main/java/com/android/mediproject/core/network/module/AwsNetwork.kt index 2ae9ed358..d6b26e48c 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/module/AwsNetwork.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/module/AwsNetwork.kt @@ -5,6 +5,7 @@ import com.android.mediproject.core.common.util.AesCoder import com.android.mediproject.core.datastore.TokenDataSourceImpl import com.android.mediproject.core.model.comments.CommentChangedResponse import com.android.mediproject.core.model.comments.CommentListResponse +import com.android.mediproject.core.model.comments.LikeResponse import com.android.mediproject.core.model.medicine.InterestedMedicine.InterestedMedicineListResponse import com.android.mediproject.core.model.medicine.MedicineIdResponse import com.android.mediproject.core.model.remote.sign.SignInResponse @@ -15,7 +16,6 @@ import com.android.mediproject.core.model.requestparameters.ChangePasswordReques import com.android.mediproject.core.model.requestparameters.DeleteCommentParameter import com.android.mediproject.core.model.requestparameters.EditCommentParameter import com.android.mediproject.core.model.requestparameters.GetMedicineIdParameter -import com.android.mediproject.core.model.requestparameters.LikeCommentParameter import com.android.mediproject.core.model.requestparameters.NewCommentParameter import com.android.mediproject.core.model.user.remote.ChangeNicknameResponse import com.android.mediproject.core.model.user.remote.ChangePasswordResponse @@ -164,12 +164,22 @@ interface AwsNetworkApi { ): Response /** - * 댓글 좋아요 + * 댓글 좋아요 추가 */ - @POST(value = "medicine/comment") + @POST(value = "medicine/comment/{medicineId}/like/{commentId}") suspend fun likeComment( - @Body likeCommentParameter: LikeCommentParameter - ): Response + @Path("medicineId", encoded = true) medicineId: Long, + @Path("commentId", encoded = true) commentId: Long, + ): Response + + /** + * 댓글 좋아요 해제 + */ + @DELETE(value = "medicine/comment/{medicineId}/like/{commentId}") + suspend fun unlikeComment( + @Path("medicineId", encoded = true) medicineId: Long, + @Path("commentId", encoded = true) commentId: Long, + ): Response /** * 댓글 등록 diff --git a/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/CommentsAdapter.kt b/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/CommentsAdapter.kt index 3338c1ff8..faa576adf 100644 --- a/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/CommentsAdapter.kt +++ b/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/CommentsAdapter.kt @@ -58,12 +58,12 @@ class CommentsAdapter : PagingDataAdapter { - adapter.notifyItemChanged(action.position) + val text = + HtmlCompat.fromHtml(getString(R.string.replyHeader) + action.comment, HtmlCompat.FROM_HTML_MODE_LEGACY) + replayInfoHeader.text = text + replyHeader.isVisible = true } is CommentActionState.CLICKED_EDIT_COMMENT -> { @@ -94,7 +99,10 @@ class MedicineCommentsFragment : } is CommentActionState.COMPLETED_APPLY_COMMENT_REPLY -> { + replayInfoHeader.isVisible = false action.result.fold(onSuccess = { + replyHeader.isVisible = false + toast(getString(R.string.appliedComment)) adapter.refresh() }, onFailure = { toast(it.message.toString()) @@ -104,6 +112,7 @@ class MedicineCommentsFragment : is CommentActionState.COMPLETED_APPLY_EDITED_COMMENT -> { action.result.fold(onSuccess = { adapter.refresh() + toast(getString(R.string.appliedEditComment)) }, onFailure = { toast(it.message.toString()) }) @@ -112,20 +121,24 @@ class MedicineCommentsFragment : is CommentActionState.COMPLETED_DELETE_COMMENT -> { action.result.fold(onSuccess = { adapter.refresh() + toast(getString(R.string.deletedComment)) }, onFailure = { toast(it.message.toString()) }) } - is CommentActionState.NONE -> { } + + is CommentActionState.CANCELED_REPLY -> { + replyHeader.isVisible = false + } } } } launch { - fragmentViewModel.comments.collectLatest { + fragmentViewModel.comments.collect { adapter.submitData(it) } } diff --git a/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/MedicineCommentsViewModel.kt b/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/MedicineCommentsViewModel.kt index eb8688f30..e005589c4 100644 --- a/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/MedicineCommentsViewModel.kt +++ b/feature/comments/src/main/java/com/android/mediproject/feature/comments/commentsofamedicine/MedicineCommentsViewModel.kt @@ -25,22 +25,22 @@ import com.android.mediproject.feature.comments.commentsofamedicine.CommentActio import com.android.mediproject.feature.comments.commentsofamedicine.CommentActionState.COMPLETED_APPLY_EDITED_COMMENT import com.android.mediproject.feature.comments.commentsofamedicine.CommentActionState.COMPLETED_DELETE_COMMENT import com.android.mediproject.feature.comments.commentsofamedicine.CommentActionState.COMPLETED_LIKE - import com.android.mediproject.feature.comments.commentsofamedicine.CommentActionState.NONE import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -64,6 +64,8 @@ class MedicineCommentsViewModel @Inject constructor( private val _accountState = MutableStateFlow(AccountState.SignedOut) val accountState get() = _accountState.asStateFlow() + private val replyId = MutableStateFlow(-1) + init { viewModelScope.launch { getAccountStateUseCase.invoke().collectLatest { @@ -76,10 +78,9 @@ class MedicineCommentsViewModel @Inject constructor( } - val comments: StateFlow> = medicineBasicInfo.flatMapLatest { info -> - commentsUseCase.getCommentsForAMedicine(info.medicineIdInAws).mapLatest { pagingData -> + val comments: SharedFlow> = medicineBasicInfo.flatMapLatest { info -> + commentsUseCase.getCommentsForAMedicine(info.medicineIdInAws, myUserId.value).mapLatest { pagingData -> val signedIn = accountState.value is AccountState.SignedIn - pagingData.map { comment -> comment.apply { onClickReply = ::onClickedReply @@ -96,23 +97,45 @@ class MedicineCommentsViewModel @Inject constructor( } } } - }.flowOn(defaultDispatcher).cachedIn(viewModelScope).stateIn(viewModelScope, SharingStarted.Lazily, PagingData.empty()) + }.flowOn(defaultDispatcher).cachedIn(viewModelScope).shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) /** * 답글 등록 * * @param comment 답글 내용 - * @param subOrdinationId 부모 댓글의 id */ - private fun applyReplyComment(comment: String, subOrdinationId: Long) { + private fun applyReplyComment(comment: String) { viewModelScope.launch { commentsUseCase.applyNewComment(NewCommentParameter(medicineId = medicineBasicInfo.replayCache.last().medicineIdInAws.toString(), userId = myUserId.value.toString(), content = comment, - subOrdinationId = subOrdinationId.toString())).collectLatest { result -> + subOrdinationId = replyId.value.toString())).collectLatest { result -> + result.onSuccess { + // 댓글 등록 성공 + delay(200) + commentsUseCase.scrollChannel.send(Unit) + _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.success(Unit))) + }.onFailure { + _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.failure(it))) + } + } + } + } + + /** + * 새 댓글 등록 + */ + private fun applyNewComment(content: String) { + viewModelScope.launch { + commentsUseCase.applyNewComment(NewCommentParameter(medicineId = medicineBasicInfo.replayCache.last().medicineIdInAws.toString(), + userId = myUserId.value.toString(), + content = content, + subOrdinationId = "0")).collectLatest { result -> result.onSuccess { // 댓글 등록 성공 + delay(200) + commentsUseCase.scrollChannel.send(Unit) _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.success(Unit))) }.onFailure { _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.failure(it))) @@ -133,6 +156,7 @@ class MedicineCommentsViewModel @Inject constructor( medicineId = medicineBasicInfo.replayCache.last().medicineIdInAws)).collectLatest { result -> result.onSuccess { // 댓글 수정 성공 + delay(200) _action.emit(CommentActionState.COMPLETED_APPLY_EDITED_COMMENT(Result.success(Unit))) }.onFailure { _action.emit(CommentActionState.COMPLETED_APPLY_EDITED_COMMENT(Result.failure(it))) @@ -146,11 +170,13 @@ class MedicineCommentsViewModel @Inject constructor( * 답글 작성하기 버튼 클릭 * - 답글 등록하기 버튼 클릭이 아님 * - * @param position 답글 작성 아이템 뷰가 표시될 리스트내 절대 위치 + * @param comment 답글을 작성할 댓글의 id + * @param commentId 답글을 작성할 댓글의 id */ - private fun onClickedReply(position: Int) { + private fun onClickedReply(comment: String, commentId: Long) { viewModelScope.launch { - _action.tryEmit(CommentActionState.CLICKED_REPLY(position)) + replyId.emit(commentId) + _action.emit(CommentActionState.CLICKED_REPLY(comment)) } } @@ -166,12 +192,17 @@ class MedicineCommentsViewModel @Inject constructor( } } + /** + * 댓글 삭제 처리 + */ fun deleteComment(commentId: Long) { viewModelScope.launch { commentsUseCase.deleteComment(DeleteCommentParameter(commentId, medicineBasicInfo.replayCache.last().medicineIdInAws)) .collectLatest { result -> result.onSuccess { // 댓글 삭제 성공 + delay(200) + commentsUseCase.scrollChannel.send(Unit) _action.emit(CommentActionState.COMPLETED_DELETE_COMMENT(Result.success(Unit))) }.onFailure { _action.emit(CommentActionState.COMPLETED_DELETE_COMMENT(Result.failure(it))) @@ -186,16 +217,18 @@ class MedicineCommentsViewModel @Inject constructor( * * @param commentId LIKE를 등록또는 해제 할 댓글의 id */ - private fun onClickedLike(commentId: Long) { + private fun onClickedLike(commentId: Long, isLiked: Boolean) { viewModelScope.launch { - commentsUseCase.likeComment(LikeCommentParameter(commentId, myUserId.value)).collectLatest { result -> - result.onSuccess { - // like 처리 완료 - _action.emit(CommentActionState.COMPLETED_LIKE(Result.success(Unit))) - }.onFailure { - _action.emit(CommentActionState.COMPLETED_LIKE(Result.failure(it))) + commentsUseCase.likeComment(LikeCommentParameter(commentId, medicineBasicInfo.replayCache.last().medicineIdInAws, isLiked)) + .collectLatest { result -> + result.onSuccess { + // like 처리 완료 + delay(200) + _action.emit(CommentActionState.COMPLETED_LIKE(Result.success(Unit))) + }.onFailure { + _action.emit(CommentActionState.COMPLETED_LIKE(Result.failure(it))) + } } - } } } @@ -223,16 +256,12 @@ class MedicineCommentsViewModel @Inject constructor( */ override fun onClickedSendButton(text: CharSequence) { viewModelScope.launch { - if (text.isEmpty()) _action.tryEmit(COMPLETED_APPLY_COMMENT_REPLY(Result.failure(IllegalArgumentException("댓글 내용이 없습니다.")))) - else commentsUseCase.applyNewComment(NewCommentParameter(medicineId = medicineBasicInfo.replayCache.last().medicineIdInAws.toString(), - userId = myUserId.value.toString(), - content = text.toString(), - subOrdinationId = "0")).collectLatest { result -> - result.onSuccess { - // 댓글 등록 성공 - _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.success(Unit))) - }.onFailure { - _action.emit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.failure(it))) + if (text.isEmpty()) _action.tryEmit(CommentActionState.COMPLETED_APPLY_COMMENT_REPLY(Result.failure(IllegalArgumentException("댓글 내용이 없습니다.")))) + else { + if (replyId.value == -1L) { + applyNewComment(text.toString()) + } else { + applyReplyComment(text.toString()) } } } @@ -243,6 +272,13 @@ class MedicineCommentsViewModel @Inject constructor( _medicineBasicInfo.emit(medicineBasicInfo) } } + + fun cancelReply() { + viewModelScope.launch { + _action.emit(CommentActionState.CANCELED_REPLY) + replyId.value = -1 + } + } } /** @@ -256,7 +292,6 @@ class MedicineCommentsViewModel @Inject constructor( * @property COMPLETED_APPLY_EDITED_COMMENT 댓글 수정 완료 * @property COMPLETED_LIKE 댓글 좋아요 완료 * @property COMPLETED_DELETE_COMMENT 댓글 삭제 완료 - * @property ERROR 댓글 등록, 수정, 삭제, 좋아요 에러 * @property NONE 초기 상태 */ @Suppress("ClassName") @@ -268,9 +303,10 @@ sealed class CommentActionState { data class CLICKED_DELETE_MY_COMMENT(val commentId: Long) : CommentActionState() /** - * @property position 답글 입력하기 클릭한 댓글의 리스트 내 절대 위치 + * @property comment 답글 내용 */ - data class CLICKED_REPLY(val position: Int) : CommentActionState() + data class CLICKED_REPLY(val comment: String) : CommentActionState() + object CANCELED_REPLY : CommentActionState() object CLICKED_LIKE : CommentActionState() /** diff --git a/feature/comments/src/main/java/com/android/mediproject/feature/comments/recentcommentlist/RecentCommentsAdapter.kt b/feature/comments/src/main/java/com/android/mediproject/feature/comments/recentcommentlist/RecentCommentsAdapter.kt index 9082f19bc..0e3ea7a8a 100644 --- a/feature/comments/src/main/java/com/android/mediproject/feature/comments/recentcommentlist/RecentCommentsAdapter.kt +++ b/feature/comments/src/main/java/com/android/mediproject/feature/comments/recentcommentlist/RecentCommentsAdapter.kt @@ -7,12 +7,10 @@ import androidx.recyclerview.widget.RecyclerView import com.android.mediproject.core.model.comments.CommentDto import com.android.mediproject.core.ui.base.view.SimpleListItemView -class RecentCommentsAdapter : - ListAdapter(AsyncDiffer) { +class RecentCommentsAdapter : ListAdapter(AsyncDiffer) { - class RecentCommentListViewHolder(private val view: SimpleListItemView) : - RecyclerView.ViewHolder(view) { + class RecentCommentListViewHolder(private val view: SimpleListItemView) : RecyclerView.ViewHolder(view) { private var item: CommentDto? = null @@ -20,8 +18,8 @@ class RecentCommentsAdapter : view.setOnItemClickListener { item?.apply { onClickDelete?.invoke(commentId) - onClickLike?.invoke(commentId) - onClickReply?.invoke(absoluteAdapterPosition) + onClickLike?.invoke(commentId, !isLiked) + onClickReply?.invoke(content, commentId) } } } diff --git a/feature/comments/src/main/res/drawable/baseline_thumb_up_24.xml b/feature/comments/src/main/res/drawable/baseline_thumb_up_24.xml new file mode 100644 index 000000000..8fc592b4c --- /dev/null +++ b/feature/comments/src/main/res/drawable/baseline_thumb_up_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/feature/comments/src/main/res/layout/fragment_medicine_comments.xml b/feature/comments/src/main/res/layout/fragment_medicine_comments.xml index 1122165e3..fe0eea45d 100644 --- a/feature/comments/src/main/res/layout/fragment_medicine_comments.xml +++ b/feature/comments/src/main/res/layout/fragment_medicine_comments.xml @@ -1,5 +1,6 @@ - + @@ -8,30 +9,78 @@ type="com.android.mediproject.feature.comments.commentsofamedicine.MedicineCommentsViewModel" /> - + android:paddingHorizontal="16dp"> + + + app:layout_constraintTop_toBottomOf="@id/pagingListView"> + + + + + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/replyHeader"> 댓글 삭제 처리 오류 댓글을 삭제하시겠습니까? 댓글 처리 오류 + 답글 대상 : + 등록 완료 + 댓글 수정 완료 + 댓글 삭제 완료 \ No newline at end of file diff --git a/feature/medicine/src/main/java/com/android/mediproject/feature/medicine/main/MedicineInfoViewModel.kt b/feature/medicine/src/main/java/com/android/mediproject/feature/medicine/main/MedicineInfoViewModel.kt index 02dab03bf..c42efdd88 100644 --- a/feature/medicine/src/main/java/com/android/mediproject/feature/medicine/main/MedicineInfoViewModel.kt +++ b/feature/medicine/src/main/java/com/android/mediproject/feature/medicine/main/MedicineInfoViewModel.kt @@ -6,6 +6,7 @@ import asEventFlow import com.android.mediproject.core.common.network.Dispatcher import com.android.mediproject.core.common.network.MediDispatchers import com.android.mediproject.core.common.viewmodel.UiState +import com.android.mediproject.core.domain.CommentsUseCase import com.android.mediproject.core.domain.GetMedicineDetailsUseCase import com.android.mediproject.core.model.local.navargs.MedicineInfoArgs import com.android.mediproject.core.model.medicine.medicinedetailinfo.MedicineDetatilInfoDto @@ -20,6 +21,8 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,7 +30,18 @@ import javax.inject.Inject @HiltViewModel class MedicineInfoViewModel @Inject constructor( private val getMedicineDetailsUseCase: GetMedicineDetailsUseCase, - @Dispatcher(MediDispatchers.Default) private val defaultDispatcher: CoroutineDispatcher) : BaseViewModel() { + private val commentsUseCase: CommentsUseCase, + @Dispatcher(MediDispatchers.Default) private val defaultDispatcher: CoroutineDispatcher +) : BaseViewModel() { + + init { + viewModelScope.launch { + // 댓글 업데이트 시 스크롤을 맨 아래로 내립니다. + commentsUseCase.scrollChannel.receiveAsFlow().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 0).collect { + _eventState.emit(EventState.ScrollToBottom) + } + } + } private val _eventState = MutableEventFlow(replay = 1) val eventState get() = _eventState.asEventFlow() diff --git a/feature/search/src/main/java/com/android/mediproject/feature/search/SearchMedicinesFragment.kt b/feature/search/src/main/java/com/android/mediproject/feature/search/SearchMedicinesFragment.kt index 39844aff4..ce112a5b4 100644 --- a/feature/search/src/main/java/com/android/mediproject/feature/search/SearchMedicinesFragment.kt +++ b/feature/search/src/main/java/com/android/mediproject/feature/search/SearchMedicinesFragment.kt @@ -41,8 +41,7 @@ class SearchMedicinesFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - systemBarStyler.changeMode(listOf(SystemBarStyler.ChangeView(binding.root, SystemBarStyler.SpacingType.PADDING)), - listOf(SystemBarStyler.ChangeView(binding.root, SystemBarStyler.SpacingType.PADDING))) + systemBarStyler.changeMode(listOf(SystemBarStyler.ChangeView(binding.root, SystemBarStyler.SpacingType.PADDING))) // contents_fragment_container_view 에 최근 검색 목록과 검색 결과 목록 화면 두 개를 띄운다. initSearchBar()