From b805065c7ce3bc4e168bb7adbbccac3cd27b3a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=ED=98=84?= Date: Mon, 21 Oct 2024 17:53:54 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20chatting=20view=20type=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=9C=84=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 안드로이드 프로젝트 구조 기초 세팅 (#2) * feat: 백엔드 프로젝트 구조 기초 세팅 (#5) * feat: LocalDateTime -> String 으로 만드는 util 함수 구현 * feat: CommentUiModel 구현 * feat: viewmodel 과 adapter로직 변경 * feat: 변경에 따른 뷰 반영 * test: 로직 변경에 따른 test 코드 수정 * stye: ktlint 적용 * refactor: 필요없는 파일 삭제 * feat: 필요없는 파일 삭제 * refactor: LocalDate와 LocalTime을 String으로 바꾸는 함수명 변경 * style: ktlint 적용 --------- Co-authored-by: Dora Choo --- .../util/LocalDateTimeToString.kt | 14 ++++ .../commentdetail/CommentDetailActivity.kt | 2 +- .../commentdetail/CommentDetailViewModel.kt | 15 ++-- .../adapter/comment/CommentAdapter.kt | 83 +++++++------------ .../adapter/comment/CommentViewType.kt | 18 +--- .../comment/DateSeparatorViewHolder.kt | 4 +- .../adapter/comment/MyCommentViewHolder.kt | 4 +- .../adapter/comment/OtherCommentViewHolder.kt | 4 +- .../model/comment/CommentUiModel.kt | 59 +++++++++++++ .../main/res/layout/item_date_separator.xml | 4 +- .../src/main/res/layout/item_my_comment.xml | 4 +- .../main/res/layout/item_other_comment.xml | 4 +- .../CommentDetailViewModelTest.kt | 5 +- .../repository/FakeCommentDetailRepository.kt | 5 ++ .../com/zzang/chongdae/util/TestFixture.kt | 19 +++++ 15 files changed, 153 insertions(+), 91 deletions(-) create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/util/LocalDateTimeToString.kt create mode 100644 android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/model/comment/CommentUiModel.kt diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/util/LocalDateTimeToString.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/util/LocalDateTimeToString.kt new file mode 100644 index 000000000..15304c46d --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/util/LocalDateTimeToString.kt @@ -0,0 +1,14 @@ +package com.zzang.chongdae.presentation.util + +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +fun LocalDate.toFormattedDate(): String { + return this.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일")) +} + +fun LocalTime.toFormattedTime(): String { + return this.format(DateTimeFormatter.ofPattern(("a h:mm"), Locale.KOREAN)) +} diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt index 8d5056e87..b2e80ef2c 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailActivity.kt @@ -110,7 +110,7 @@ class CommentDetailActivity : AppCompatActivity(), OnUpdateStatusClickListener { private fun observeComments() { viewModel.comments.observe(this) { comments -> - commentAdapter.submitComments(comments) + commentAdapter.submitList(comments) binding.rvComments.doOnPreDraw { binding.rvComments.scrollToPosition(comments.size - 1) } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt index e81fbfba5..f72bfc77e 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModel.kt @@ -19,6 +19,9 @@ import com.zzang.chongdae.domain.repository.OfferingRepository import com.zzang.chongdae.domain.repository.ParticipantRepository import com.zzang.chongdae.presentation.util.Event import com.zzang.chongdae.presentation.view.commentdetail.event.CommentDetailEvent +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel.Companion.toUiModel +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel.Companion.toUiModelListWithSeparators import com.zzang.chongdae.presentation.view.commentdetail.model.information.CommentOfferingInfoUiModel import com.zzang.chongdae.presentation.view.commentdetail.model.information.CommentOfferingInfoUiModel.Companion.toUiModel import com.zzang.chongdae.presentation.view.commentdetail.model.meeting.MeetingsUiModel @@ -53,8 +56,8 @@ class CommentDetailViewModel val commentContent = MutableLiveData("") - private val _comments: MutableLiveData> = MutableLiveData() - val comments: LiveData> get() = _comments + private val _comments: MutableLiveData> = MutableLiveData() + val comments: LiveData> get() = _comments private var cachedComments: List = emptyList() private val _commentOfferingInfo = MutableLiveData() @@ -144,15 +147,11 @@ class CommentDetailViewModel is Result.Success -> { val newComments = result.data if (cachedComments != newComments) { - _comments.value = newComments + _comments.value = newComments.toUiModelListWithSeparators() cachedComments = newComments } } - - is Result.Error -> - handleNetworkError(result.error) { - loadComments() - } + is Result.Error -> handleNetworkError(result.error) { loadComments() } } } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentAdapter.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentAdapter.kt index 9357a609e..9df225fea 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentAdapter.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentAdapter.kt @@ -8,48 +8,37 @@ import androidx.recyclerview.widget.RecyclerView import com.zzang.chongdae.databinding.ItemDateSeparatorBinding import com.zzang.chongdae.databinding.ItemMyCommentBinding import com.zzang.chongdae.databinding.ItemOtherCommentBinding -import com.zzang.chongdae.domain.model.Comment - -class CommentAdapter : ListAdapter(DIFF_CALLBACK) { - fun submitComments(comments: List) { - val newItems = mutableListOf() - - for (i in comments.indices) { - val currentComment = comments[i] - val previousComment = if (i > 0) comments[i - 1] else null - - if (previousComment == null || isDifferentDates(currentComment, previousComment)) { - newItems.add(CommentViewType.DateSeparator(currentComment)) - } - - newItems.add(CommentViewType.fromComment(currentComment)) - } - - submitList(newItems) - } - - private fun isDifferentDates( - currentComment: Comment, - previousComment: Comment, - ) = currentComment.commentCreatedAt.date != previousComment.commentCreatedAt.date +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel +class CommentAdapter : ListAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_MY_COMMENT -> { - val binding = ItemMyCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemMyCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false) MyCommentViewHolder(binding) } VIEW_TYPE_OTHER_COMMENT -> { - val binding = ItemOtherCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemOtherCommentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ) OtherCommentViewHolder(binding) } VIEW_TYPE_DATE_SEPARATOR -> { - val binding = ItemDateSeparatorBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemDateSeparatorBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ) DateSeparatorViewHolder(binding) } @@ -61,18 +50,18 @@ class CommentAdapter : ListAdapter(DIF holder: RecyclerView.ViewHolder, position: Int, ) { - when (val item = getItem(position)) { - is CommentViewType.MyComment -> (holder as MyCommentViewHolder).bind(item.comment) - is CommentViewType.OtherComment -> (holder as OtherCommentViewHolder).bind(item.comment) - is CommentViewType.DateSeparator -> (holder as DateSeparatorViewHolder).bind(item.comment) + when (holder) { + is MyCommentViewHolder -> holder.bind(getItem(position)) + is OtherCommentViewHolder -> holder.bind(getItem(position)) + is DateSeparatorViewHolder -> holder.bind(getItem(position)) } } override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is CommentViewType.MyComment -> VIEW_TYPE_MY_COMMENT - is CommentViewType.OtherComment -> VIEW_TYPE_OTHER_COMMENT - is CommentViewType.DateSeparator -> VIEW_TYPE_DATE_SEPARATOR + return when (getItem(position).commentViewType) { + CommentViewType.MyComment -> VIEW_TYPE_MY_COMMENT + CommentViewType.OtherComment -> VIEW_TYPE_OTHER_COMMENT + CommentViewType.DateSeparator -> VIEW_TYPE_DATE_SEPARATOR } } @@ -82,28 +71,18 @@ class CommentAdapter : ListAdapter(DIF private const val VIEW_TYPE_DATE_SEPARATOR = 3 private val DIFF_CALLBACK = - object : DiffUtil.ItemCallback() { + object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: CommentViewType, - newItem: CommentViewType, + oldItem: CommentUiModel, + newItem: CommentUiModel, ): Boolean { - return when { - oldItem is CommentViewType.MyComment && newItem is CommentViewType.MyComment -> - oldItem.comment == newItem.comment - - oldItem is CommentViewType.OtherComment && newItem is CommentViewType.OtherComment -> - oldItem.comment == newItem.comment - - oldItem is CommentViewType.DateSeparator && newItem is CommentViewType.DateSeparator -> - oldItem.comment == newItem.comment - - else -> false - } + return oldItem.commentViewType == newItem.commentViewType && + oldItem.date == newItem.date && oldItem.time == newItem.time } override fun areContentsTheSame( - oldItem: CommentViewType, - newItem: CommentViewType, + oldItem: CommentUiModel, + newItem: CommentUiModel, ): Boolean { return oldItem == newItem } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentViewType.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentViewType.kt index 8b8f30ec8..7a6ee7cd8 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentViewType.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/CommentViewType.kt @@ -1,21 +1,9 @@ package com.zzang.chongdae.presentation.view.commentdetail.adapter.comment -import com.zzang.chongdae.domain.model.Comment - sealed class CommentViewType { - data class MyComment(val comment: Comment) : CommentViewType() - - data class OtherComment(val comment: Comment) : CommentViewType() + data object MyComment : CommentViewType() - data class DateSeparator(val comment: Comment) : CommentViewType() + data object OtherComment : CommentViewType() - companion object { - fun fromComment(comment: Comment): CommentViewType { - return if (comment.isMine) { - MyComment(comment) - } else { - OtherComment(comment) - } - } - } + data object DateSeparator : CommentViewType() } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/DateSeparatorViewHolder.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/DateSeparatorViewHolder.kt index 332edbbdd..eea8f62a0 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/DateSeparatorViewHolder.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/DateSeparatorViewHolder.kt @@ -2,12 +2,12 @@ package com.zzang.chongdae.presentation.view.commentdetail.adapter.comment import androidx.recyclerview.widget.RecyclerView import com.zzang.chongdae.databinding.ItemDateSeparatorBinding -import com.zzang.chongdae.domain.model.Comment +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel class DateSeparatorViewHolder( private val binding: ItemDateSeparatorBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(comment: Comment) { + fun bind(comment: CommentUiModel) { binding.comment = comment } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/MyCommentViewHolder.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/MyCommentViewHolder.kt index e6b48a8e6..0655c02b2 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/MyCommentViewHolder.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/MyCommentViewHolder.kt @@ -2,12 +2,12 @@ package com.zzang.chongdae.presentation.view.commentdetail.adapter.comment import androidx.recyclerview.widget.RecyclerView import com.zzang.chongdae.databinding.ItemMyCommentBinding -import com.zzang.chongdae.domain.model.Comment +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel class MyCommentViewHolder( private val binding: ItemMyCommentBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(comment: Comment) { + fun bind(comment: CommentUiModel) { binding.comment = comment } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/OtherCommentViewHolder.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/OtherCommentViewHolder.kt index f14fd231d..56400dad7 100644 --- a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/OtherCommentViewHolder.kt +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/adapter/comment/OtherCommentViewHolder.kt @@ -2,12 +2,12 @@ package com.zzang.chongdae.presentation.view.commentdetail.adapter.comment import androidx.recyclerview.widget.RecyclerView import com.zzang.chongdae.databinding.ItemOtherCommentBinding -import com.zzang.chongdae.domain.model.Comment +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel class OtherCommentViewHolder( private val binding: ItemOtherCommentBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(comment: Comment) { + fun bind(comment: CommentUiModel) { binding.comment = comment } } diff --git a/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/model/comment/CommentUiModel.kt b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/model/comment/CommentUiModel.kt new file mode 100644 index 000000000..ac11a01cc --- /dev/null +++ b/android/app/src/main/java/com/zzang/chongdae/presentation/view/commentdetail/model/comment/CommentUiModel.kt @@ -0,0 +1,59 @@ +package com.zzang.chongdae.presentation.view.commentdetail.model.comment + +import com.zzang.chongdae.domain.model.Comment +import com.zzang.chongdae.presentation.util.toFormattedDate +import com.zzang.chongdae.presentation.util.toFormattedTime +import com.zzang.chongdae.presentation.view.commentdetail.adapter.comment.CommentViewType + +data class CommentUiModel( + val content: String, + val date: String, + val time: String, + val isMine: Boolean, + val isProposer: Boolean, + val nickname: String, + val commentViewType: CommentViewType, +) { + companion object { + fun Comment.toUiModel(): CommentUiModel { + val viewType = if (this.isMine) CommentViewType.MyComment else CommentViewType.OtherComment + return CommentUiModel( + content = this.content, + date = this.commentCreatedAt.date.toFormattedDate(), + time = this.commentCreatedAt.time.toFormattedTime(), + isMine = this.isMine, + isProposer = this.isProposer, + nickname = this.nickname, + commentViewType = viewType, + ) + } + + fun createDateSeparator(date: String): CommentUiModel { + return CommentUiModel( + content = "", + date = date, + time = "", + isMine = false, + isProposer = false, + nickname = "", + commentViewType = CommentViewType.DateSeparator, + ) + } + + fun List.toUiModelListWithSeparators(): List { + val uiModels = mutableListOf() + var currentDate: String? = null + + for (comment in this) { + val commentDate = comment.commentCreatedAt.date.toFormattedDate() + if (commentDate != currentDate) { + uiModels.add(CommentUiModel.createDateSeparator(commentDate)) + currentDate = commentDate + } + uiModels.add(comment.toUiModel()) + } + + return uiModels + } + } +} diff --git a/android/app/src/main/res/layout/item_date_separator.xml b/android/app/src/main/res/layout/item_date_separator.xml index 3ee39ea44..8f24d4225 100644 --- a/android/app/src/main/res/layout/item_date_separator.xml +++ b/android/app/src/main/res/layout/item_date_separator.xml @@ -7,7 +7,7 @@ + type="com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel" /> + type="com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel" /> diff --git a/android/app/src/main/res/layout/item_other_comment.xml b/android/app/src/main/res/layout/item_other_comment.xml index fce240006..6f3be7f3a 100644 --- a/android/app/src/main/res/layout/item_other_comment.xml +++ b/android/app/src/main/res/layout/item_other_comment.xml @@ -7,7 +7,7 @@ + type="com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel" /> diff --git a/android/app/src/test/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModelTest.kt b/android/app/src/test/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModelTest.kt index c5b6090b1..1b3917db4 100644 --- a/android/app/src/test/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModelTest.kt +++ b/android/app/src/test/java/com/zzang/chongdae/presentation/view/commentdetail/CommentDetailViewModelTest.kt @@ -70,7 +70,7 @@ class CommentDetailViewModelTest { // when // then val result = viewModel.comments.getOrAwaitValue() - assertThat(result).isEqualTo(TestFixture.comments) + assertThat(result).isEqualTo(TestFixture.commentsUiModels) } @DisplayName("댓글을 작성하면 댓글 목록에 추가된다") @@ -78,7 +78,6 @@ class CommentDetailViewModelTest { fun addComment() { // given val before = viewModel.comments.getOrAwaitValue() - assertThat(before.size).isEqualTo(1) // when viewModel.commentContent.value = "new comment" @@ -87,7 +86,7 @@ class CommentDetailViewModelTest { // then val result = viewModel.comments.getOrAwaitValue() - assertThat(result.size).isEqualTo(2) + assertThat(result.size).isEqualTo(before.size + 1) } @DisplayName("약속 장소(도로명주소)를 불러온다") diff --git a/android/app/src/test/java/com/zzang/chongdae/repository/FakeCommentDetailRepository.kt b/android/app/src/test/java/com/zzang/chongdae/repository/FakeCommentDetailRepository.kt index b177a9a22..c63560308 100644 --- a/android/app/src/test/java/com/zzang/chongdae/repository/FakeCommentDetailRepository.kt +++ b/android/app/src/test/java/com/zzang/chongdae/repository/FakeCommentDetailRepository.kt @@ -11,6 +11,11 @@ class FakeCommentDetailRepository : CommentDetailRepository { private val comments: MutableList = TestFixture.comments private var commentOfferingInfo: CommentOfferingInfo = TestFixture.commentOfferingInfo + init { + comments.clear() + comments.add(TestFixture.comment) + } + override suspend fun saveComment( offeringId: Long, comment: String, diff --git a/android/app/src/test/java/com/zzang/chongdae/util/TestFixture.kt b/android/app/src/test/java/com/zzang/chongdae/util/TestFixture.kt index 8755f6c83..f3cda7fa4 100644 --- a/android/app/src/test/java/com/zzang/chongdae/util/TestFixture.kt +++ b/android/app/src/test/java/com/zzang/chongdae/util/TestFixture.kt @@ -20,6 +20,9 @@ import com.zzang.chongdae.domain.model.participant.Participant import com.zzang.chongdae.domain.model.participant.ParticipantCount import com.zzang.chongdae.domain.model.participant.Participants import com.zzang.chongdae.domain.model.participant.Proposer +import com.zzang.chongdae.presentation.view.commentdetail.adapter.comment.CommentViewType +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel +import com.zzang.chongdae.presentation.view.commentdetail.model.comment.CommentUiModel.Companion.toUiModelListWithSeparators import okhttp3.MultipartBody import java.time.LocalDateTime @@ -50,6 +53,22 @@ object TestFixture { comment, ) + val commentUiModel: CommentUiModel = + CommentUiModel( + content = "content1", + date = "2021-10-10", + time = "10:10:10", + isMine = true, + isProposer = true, + nickname = "nickname", + commentViewType = CommentViewType.MyComment, + ) + + val commentsUiModels: List = + mutableListOf( + comment, + ).toUiModelListWithSeparators() + val commentOfferingInfo: CommentOfferingInfo = CommentOfferingInfo( status = "status",