feat(character-detail): 캐릭터 상세 댓글 섹션 추가 및 데이터 바인딩

- 댓글 입력 필드 stroke(흰색 1dp stroke와 radius 5dp) 추가
- 입력 박스 내부 우측에 전송 아이콘(ic_message_send) 추가
- 배경 드로어블(#263238, radius 10dp) 추가
- CharacterCommentResponse에 comment(nullable) 필드 추가
- CharacterDetailActivity에서 latestComment/totalComments 바인딩 및 UI 분기 처리
This commit is contained in:
2025-08-19 18:35:04 +09:00
parent 61cfbe249c
commit df1746976c
6 changed files with 272 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.chat.character.comment
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
// Request DTOs
@Keep
data class CreateCharacterCommentRequest(
@SerializedName("comment") val comment: String
)
// Response DTOs
// 댓글 Response
// - 댓글 ID
// - 댓글 쓴 Member 프로필 이미지
// - 댓글 쓴 Member 닉네임
// - 댓글 쓴 시간 timestamp(long)
// - 답글 수
@Keep
data class CharacterCommentResponse(
@SerializedName("commentId") val commentId: Long,
@SerializedName("memberProfileImage") val memberProfileImage: String,
@SerializedName("memberNickname") val memberNickname: String,
@SerializedName("createdAt") val createdAt: Long,
@SerializedName("replyCount") val replyCount: Int,
@SerializedName("comment") val comment: String?
)
// 답글 Response 단건(목록 원소)
// - 답글 ID
// - 답글 쓴 Member 프로필 이미지
// - 답글 쓴 Member 닉네임
// - 답글 쓴 시간 timestamp(long)
@Keep
data class CharacterReplyResponse(
@SerializedName("replyId") val replyId: Long,
@SerializedName("memberProfileImage") val memberProfileImage: String,
@SerializedName("memberNickname") val memberNickname: String,
@SerializedName("createdAt") val createdAt: Long
)
// 댓글의 답글 조회 Response 컨테이너
// - 원본 댓글 Response
// - 답글 목록(위 사양의 필드 포함)
@Keep
data class CharacterCommentRepliesResponse(
@SerializedName("original") val original: CharacterCommentResponse,
@SerializedName("replies") val replies: List<CharacterReplyResponse>
)

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.chat.character.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
@@ -9,10 +10,12 @@ import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCharacterDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -119,6 +122,7 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
}
}
@SuppressLint("SetTextI18n")
private fun bindObservers() {
viewModel.uiState.observe(this) { state ->
// 1) 로딩 상태 처리
@@ -169,7 +173,8 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
binding.tvWorldviewContent.text = worldviewText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvWorldviewContent.post {
val totalLines = binding.tvWorldviewContent.layout?.lineCount ?: binding.tvWorldviewContent.lineCount
val totalLines = binding.tvWorldviewContent.layout?.lineCount
?: binding.tvWorldviewContent.lineCount
val needExpand = totalLines > 3
binding.llWorldviewExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
// 표시 상태는 항상 접힘 상태로 시작
@@ -184,7 +189,8 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
binding.tvPersonalityContent.text = personalityText
// 먼저 전체 줄 수를 측정한 뒤 접힘 레이아웃 적용
binding.tvPersonalityContent.post {
val totalLines = binding.tvPersonalityContent.layout?.lineCount ?: binding.tvPersonalityContent.lineCount
val totalLines = binding.tvPersonalityContent.layout?.lineCount
?: binding.tvPersonalityContent.lineCount
val needExpand = totalLines > 3
binding.llPersonalityExpand.visibility = if (needExpand) View.VISIBLE else View.GONE
applyPersonalityCollapsedLayout()
@@ -213,6 +219,66 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
binding.llOtherCharactersSection.visibility = View.VISIBLE
adapter.submitList(detail.others)
}
// 댓글 섹션 바인딩
binding.tvCommentsCount.text = "${detail.totalComments}"
if (
detail.totalComments > 0 &&
detail.latestComment != null &&
detail.latestComment.comment.isNullOrBlank()
) {
binding.llLatestComment.visibility = View.VISIBLE
binding.llNoComment.visibility = View.GONE
val latest = detail.latestComment
val profileUrl = latest.memberProfileImage
if (profileUrl.isNotBlank()) {
binding.ivCommentProfile.load(profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
val commentText = latest.comment
binding.tvLatestComment.text = if (!commentText.isNullOrBlank()) {
commentText
} else {
latest.memberNickname
}
} else {
binding.llLatestComment.visibility = View.GONE
binding.llNoComment.visibility = View.VISIBLE
// 내 프로필 이미지는 SharedPreference의 profileImage 사용 (fallback: placeholder)
val myProfileUrl = SharedPreferenceManager.profileImage
if (myProfileUrl.isNotBlank()) {
binding.ivMyProfile.load(myProfileUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivMyProfile.load(R.drawable.ic_placeholder_profile) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
}
binding.ivSendComment.setOnClickListener {
}
}
}
}

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.character.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
@Keep
data class CharacterDetailResponse(
@@ -16,7 +17,9 @@ data class CharacterDetailResponse(
@SerializedName("originalTitle") val originalTitle: String?,
@SerializedName("originalLink") val originalLink: String?,
@SerializedName("characterType") val characterType: CharacterType,
@SerializedName("others") val others: List<OtherCharacter>
@SerializedName("others") val others: List<OtherCharacter>,
@SerializedName("latestComment") val latestComment: CharacterCommentResponse?,
@SerializedName("totalComments") val totalComments: Int
)
@Keep

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="#263238" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<stroke
android:width="1dp"
android:color="@android:color/white" />
<solid android:color="@android:color/transparent" />
</shape>

View File

@@ -327,6 +327,139 @@
android:textSize="14sp" />
</LinearLayout>
<!-- 댓글 섹션 -->
<LinearLayout
android:id="@+id/ll_comments_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_round_corner_10_263238"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<!-- 헤더: 댓글 (댓글 수) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_comments_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="댓글"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_comments_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/color_b0bec5"
android:textSize="16sp"
tools:text="0" />
</LinearLayout>
<!-- 내용 컨테이너 -->
<LinearLayout
android:id="@+id/ll_comments_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<!-- 댓글 있을 때 -->
<LinearLayout
android:id="@+id/ll_latest_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/iv_comment_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<TextView
android:id="@+id/tv_latest_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="가장 최근 댓글 내용이 여기에 표시됩니다." />
</LinearLayout>
<!-- 댓글 없을 때 -->
<LinearLayout
android:id="@+id/ll_no_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_my_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
android:src="@drawable/ic_placeholder_profile" />
<LinearLayout
android:id="@+id/ll_comment_input_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/bg_round_corner_5_stroke_white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<EditText
android:id="@+id/et_comment_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:fontFamily="@font/pretendard_regular"
android:hint="댓글을 입력해보세요"
android:imeOptions="actionSend"
android:importantForAutofill="no"
android:inputType="textCapSentences|textMultiLine"
android:maxLines="3"
android:padding="0dp"
android:textColor="@color/white"
android:textColorHint="@color/color_7c7c80"
android:textSize="14sp"
tools:ignore="NestedWeights" />
<ImageView
android:id="@+id/iv_send_comment"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_message_send" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 장르의 다른 캐릭터 섹션 -->
<LinearLayout
android:id="@+id/ll_other_characters_section"