feat(character-comment): 캐릭터 댓글 리스트 BottomSheet UI 및 페이징 스텁 구현

- CharacterDetail 댓글 섹션 터치 시 BottomSheet 표시
- 헤더/입력폼/Divider/리스트/더보기 BottomSheet 구성
- RecyclerView 하단 도달 시 더미 데이터 추가 로드(Stub)
- 상대시간 표기(분/시간/일/년 전)
- API 연동은 이후 작업 예정 (스텁)
This commit is contained in:
2025-08-20 00:42:15 +09:00
parent df1746976c
commit a9742a07c0
10 changed files with 694 additions and 7 deletions

View File

@@ -16,15 +16,15 @@ data class CreateCharacterCommentRequest(
// - 댓글 쓴 Member 닉네임 // - 댓글 쓴 Member 닉네임
// - 댓글 쓴 시간 timestamp(long) // - 댓글 쓴 시간 timestamp(long)
// - 답글 수 // - 답글 수
@Keep @Keep
data class CharacterCommentResponse( data class CharacterCommentResponse(
@SerializedName("commentId") val commentId: Long, @SerializedName("commentId") val commentId: Long,
@SerializedName("memberId") val memberId: Long,
@SerializedName("memberProfileImage") val memberProfileImage: String, @SerializedName("memberProfileImage") val memberProfileImage: String,
@SerializedName("memberNickname") val memberNickname: String, @SerializedName("memberNickname") val memberNickname: String,
@SerializedName("createdAt") val createdAt: Long, @SerializedName("createdAt") val createdAt: Long,
@SerializedName("replyCount") val replyCount: Int, @SerializedName("replyCount") val replyCount: Int,
@SerializedName("comment") val comment: String? @SerializedName("comment") val comment: String
) )
// 답글 Response 단건(목록 원소) // 답글 Response 단건(목록 원소)
@@ -32,21 +32,38 @@ data class CharacterCommentResponse(
// - 답글 쓴 Member 프로필 이미지 // - 답글 쓴 Member 프로필 이미지
// - 답글 쓴 Member 닉네임 // - 답글 쓴 Member 닉네임
// - 답글 쓴 시간 timestamp(long) // - 답글 쓴 시간 timestamp(long)
@Keep @Keep
data class CharacterReplyResponse( data class CharacterReplyResponse(
@SerializedName("replyId") val replyId: Long, @SerializedName("replyId") val replyId: Long,
@SerializedName("memberId") val memberId: Long,
@SerializedName("memberProfileImage") val memberProfileImage: String, @SerializedName("memberProfileImage") val memberProfileImage: String,
@SerializedName("memberNickname") val memberNickname: String, @SerializedName("memberNickname") val memberNickname: String,
@SerializedName("createdAt") val createdAt: Long @SerializedName("createdAt") val createdAt: Long,
@SerializedName("comment") val comment: String
) )
// 댓글의 답글 조회 Response 컨테이너 // 댓글의 답글 조회 Response 컨테이너
// - 원본 댓글 Response // - 원본 댓글 Response
// - 답글 목록(위 사양의 필드 포함) // - 답글 목록(위 사양의 필드 포함)
@Keep @Keep
data class CharacterCommentRepliesResponse( data class CharacterCommentRepliesResponse(
@SerializedName("original") val original: CharacterCommentResponse, @SerializedName("original") val original: CharacterCommentResponse,
@SerializedName("replies") val replies: List<CharacterReplyResponse> @SerializedName("replies") val replies: List<CharacterReplyResponse>,
@SerializedName("cursor") val cursor: Long?
)
// 댓글 리스트 조회 Response 컨테이너
// - 전체 댓글 개수(totalCount)
// - 댓글 목록(comments)
@Keep
data class CharacterCommentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("comments") val comments: List<CharacterCommentResponse>,
@SerializedName("cursor") val cursor: Long?
)
// 신고 Request
@Keep
data class ReportCharacterCommentRequest(
@SerializedName("content") val content: String
) )

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogCharacterCommentBinding
/**
* 캐릭터 댓글 리스트 BottomSheet 컨테이너
* 내부에 CharacterCommentListFragment를 호스팅합니다.
*/
class CharacterCommentListBottomSheet(
private val characterId: Long
) : BottomSheetDialogFragment() {
private lateinit var binding: DialogCharacterCommentBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
bottomSheet?.let { bs ->
BottomSheetBehavior.from(bs).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogCharacterCommentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val tag = "CHARACTER_COMMENT_LIST"
val fragment: Fragment = CharacterCommentListFragment.newInstance(characterId)
childFragmentManager.beginTransaction()
.replace(R.id.fl_container, fragment, tag)
.commit()
}
}

View File

@@ -0,0 +1,205 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class CharacterCommentListFragment : BaseFragment<FragmentCharacterCommentListBinding>(
FragmentCharacterCommentListBinding::inflate
) {
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CharacterCommentsAdapter
private var characterId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
characterId = arguments?.getLong(EXTRA_CHARACTER_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext()
.getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
setupView()
bindData()
// 초기 로드 (스텁)
loadMore()
}
private fun hideDialog() {
(parentFragment as? BottomSheetDialogFragment)?.dismiss()
}
private fun setupView() {
binding.ivClose.setOnClickListener { hideDialog() }
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
if (comment.isBlank()) return@setOnClickListener
// 스텁: 로컬에 즉시 추가 (CharacterCommentDto 기반)
val me = CharacterCommentResponse(
commentId = System.currentTimeMillis(),
memberId = SharedPreferenceManager.userId,
memberProfileImage = SharedPreferenceManager.profileImage,
memberNickname = SharedPreferenceManager.nickname,
createdAt = System.currentTimeMillis(),
replyCount = 0,
comment = comment
)
adapter.items.add(me)
adapter.notifyItemInserted(adapter.items.size - 1)
binding.rvComment.scrollToPosition(adapter.items.size - 1)
binding.etComment.setText("")
Toast.makeText(requireContext(), "등록되었습니다 (stub)", Toast.LENGTH_SHORT).show()
}
adapter = CharacterCommentsAdapter(
currentUserId = SharedPreferenceManager.userId,
onClickMore = { item, isOwner, anchor ->
CharacterCommentMoreBottomSheet.newInstance(isOwner).apply {
onReport = {
Toast.makeText(requireContext(), "신고되었습니다 (stub)", Toast.LENGTH_SHORT).show()
}
onDelete = {
val index = adapter.items.indexOfFirst { it.commentId == item.commentId }
if (index >= 0) {
adapter.items.removeAt(index)
adapter.notifyItemRemoved(index)
}
}
}.show(childFragmentManager, "comment_more")
},
onClickItem = { /* 답글 보기로 이동 예정 (stub) */ }
)
val recyclerView = binding.rvComment
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(
activity,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisible =
(recyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
val total = recyclerView.adapter?.itemCount ?: 0
if (!recyclerView.canScrollVertically(1) && lastVisible == total - 1) {
loadMore()
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
// total count 스텁: 어댑터 크기 사용
// 필요 시 ViewModel 도입 가능
}
private fun hideKeyboard() {
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
private var page = 1
private fun loadMore() {
// API 스텁: 더미 데이터 생성
val newItems = (1..10).map { idx ->
val id = page * 1000L + idx
val writerId = if (idx % 5 == 0) SharedPreferenceManager.userId else -idx.toLong()
CharacterCommentResponse(
commentId = id,
memberId = writerId,
memberProfileImage = "",
memberNickname = "게스트$id",
createdAt = System.currentTimeMillis() - (idx * 60_000L * page),
replyCount = (idx % 3),
comment = "캐릭터 댓글 예시 텍스트 $id"
)
}
if (page == 1) adapter.items.clear()
val start = adapter.items.size
adapter.items.addAll(newItems)
adapter.notifyItemRangeInserted(start, newItems.size)
binding.tvCommentCount.text = "${adapter.items.size}"
page++
}
companion object {
private const val EXTRA_CHARACTER_ID = "extra_character_id"
fun newInstance(characterId: Long): CharacterCommentListFragment {
val args = Bundle().apply { putLong(EXTRA_CHARACTER_ID, characterId) }
val f = CharacterCommentListFragment()
f.arguments = args
return f
}
}
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
class CharacterCommentMoreBottomSheet : BottomSheetDialogFragment() {
var onReport: (() -> Unit)? = null
var onDelete: (() -> Unit)? = null
private var isOwner: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isOwner = arguments?.getBoolean(ARG_IS_OWNER) ?: false
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.dialog_character_comment_more, container, false)
val tvReport = view.findViewById<TextView>(R.id.tv_report)
val tvDelete = view.findViewById<TextView>(R.id.tv_delete)
tvReport.setOnClickListener {
dismiss()
onReport?.invoke()
}
if (isOwner) {
tvDelete.visibility = View.VISIBLE
tvDelete.setOnClickListener {
dismiss()
onDelete?.invoke()
}
} else {
tvDelete.visibility = View.GONE
}
return view
}
companion object {
private const val ARG_IS_OWNER = "arg_is_owner"
fun newInstance(isOwner: Boolean): CharacterCommentMoreBottomSheet {
val f = CharacterCommentMoreBottomSheet()
f.arguments = Bundle().apply { putBoolean(ARG_IS_OWNER, isOwner) }
return f
}
}
}

View File

@@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCharacterCommentBinding
class CharacterCommentsAdapter(
private val currentUserId: Long,
private val onClickMore: (item: CharacterCommentResponse, isOwner: Boolean, anchor: View) -> Unit,
private val onClickItem: (CharacterCommentResponse) -> Unit
) : RecyclerView.Adapter<CharacterCommentsAdapter.VH>() {
val items = mutableListOf<CharacterCommentResponse>()
inner class VH(private val binding: ItemCharacterCommentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: CharacterCommentResponse) {
if (item.memberProfileImage.isNotBlank()) {
binding.ivCommentProfile.load(item.memberProfileImage) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
} else {
binding.ivCommentProfile.setImageResource(R.drawable.ic_placeholder_profile)
}
binding.tvCommentNickname.text = item.memberNickname
binding.tvCommentDate.text = timeAgo(item.createdAt)
binding.tvComment.text = item.comment
binding.tvWriteReply.text = if (item.replyCount > 0) {
"답글 ${item.replyCount}"
} else {
"답글 쓰기"
}
val isOwner = item.memberId == currentUserId
binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener { onClickMore(item, isOwner, it) }
// 전체영역 터치 시: 답글 보기로 이동(콜백)
binding.root.setOnClickListener { onClickItem(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding =
ItemCharacterCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
private fun timeAgo(createdAtMillis: Long): String {
val now = System.currentTimeMillis()
val diff = (now - createdAtMillis).coerceAtLeast(0)
val minutes = diff / 60_000
if (minutes < 1) return "방금전"
if (minutes < 60) return "${minutes}분전"
val hours = minutes / 60
if (hours < 24) return "${hours}시간전"
val days = hours / 24
if (days < 365) return "${days}일전"
val years = days / 365
return "${years}년전"
}

View File

@@ -222,10 +222,18 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
// 댓글 섹션 바인딩 // 댓글 섹션 바인딩
binding.tvCommentsCount.text = "${detail.totalComments}" binding.tvCommentsCount.text = "${detail.totalComments}"
// 댓글 섹션 터치 시 리스트 BottomSheet 열기 (댓글 1개 이상일 때)
binding.llCommentsSection.setOnClickListener(null)
if (detail.totalComments > 0) {
binding.llCommentsSection.setOnClickListener {
val sheet = kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListBottomSheet(detail.characterId)
sheet.show(supportFragmentManager, "character_comments")
}
}
if ( if (
detail.totalComments > 0 && detail.totalComments > 0 &&
detail.latestComment != null && detail.latestComment != null &&
detail.latestComment.comment.isNullOrBlank() !detail.latestComment.comment.isNullOrBlank()
) { ) {
binding.llLatestComment.visibility = View.VISIBLE binding.llLatestComment.visibility = View.VISIBLE
binding.llNoComment.visibility = View.GONE binding.llNoComment.visibility = View.GONE

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/color_131313">
<TextView
android:id="@+id/tv_report"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:text="신고"
android:textColor="@color/white"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#78909C" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:text="삭제"
android:textColor="#EF5350"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_top_round_corner_16_7_222222">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rl_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="댓글"
android:textColor="@color/white"
android:textSize="14.7sp"
app:layout_constraintBottom_toBottomOf="@+id/iv_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_close" />
<TextView
android:id="@+id/tv_comment_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6.7dp"
android:layout_toEndOf="@+id/tv_title"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_909090"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/iv_close"
app:layout_constraintStart_toEndOf="@+id/tv_title"
app:layout_constraintTop_toTopOf="@+id/iv_close"
tools:text="1,204" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="12dp"
android:contentDescription="@null"
android:paddingHorizontal="13.3dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:src="@drawable/ic_circle_x_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/rl_toolbar"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="12dp"
android:background="#78909C" />
<!-- 캐릭터 상세의 댓글 입력 폼과 동일하게 표시 (간단화 버전) -->
<LinearLayout
android:id="@+id/ll_comment_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/divider"
android:background="@drawable/bg_top_round_corner_8_222222"
android:elevation="13.3dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="13.3dp">
<ImageView
android:id="@+id/iv_comment_profile"
android:layout_width="33.3dp"
android:layout_height="33.3dp"
android:contentDescription="@null"
tools:src="@drawable/ic_placeholder_profile" />
<RelativeLayout
android:id="@+id/rl_input_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp">
<EditText
android:id="@+id/et_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_10_232323_3bb9f1"
android:hint="댓글을 입력해 보세요"
android:importantForAutofill="no"
android:inputType="text|textMultiLine"
android:paddingVertical="13.3dp"
android:paddingStart="13.3dp"
android:paddingEnd="50.7dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
tools:ignore="LabelFor" />
<ImageView
android:id="@+id/iv_comment_send"
android:layout_width="33.3dp"
android:layout_height="33.3dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="6dp"
android:contentDescription="@null"
android:src="@drawable/btn_message_send" />
</RelativeLayout>
</LinearLayout>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/ll_comment_input"
android:layout_marginHorizontal="13.3dp"
android:background="#78909C" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_comment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_below="@+id/divider2"
android:layout_alignParentBottom="true"
android:clipToPadding="false"
android:padding="13.3dp" />
</RelativeLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_comment_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_placeholder_profile" />
<LinearLayout
android:id="@+id/ll_comment_nickname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@+id/iv_menu"
app:layout_constraintStart_toEndOf="@+id/iv_comment_profile"
app:layout_constraintTop_toTopOf="@+id/iv_comment_profile">
<TextView
android:id="@+id/tv_comment_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="닉네임" />
</LinearLayout>
<ImageView
android:id="@+id/iv_menu"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_seemore_vertical_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_comment_profile" />
<TextView
android:id="@+id/tv_comment_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="@font/pretendard_regular"
android:textColor="@color/color_b0bec5"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/ll_comment_nickname"
app:layout_constraintTop_toBottomOf="@+id/ll_comment_nickname"
tools:text="2시간전" />
<TextView
android:id="@+id/tv_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_regular"
android:lineSpacingExtra="4dp"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_comment_date"
app:layout_constraintTop_toBottomOf="@+id/tv_comment_date"
tools:text="댓글 내용이 표시됩니다." />
<TextView
android:id="@+id/tv_write_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/pretendard_bold"
android:text="답글 쓰기"
android:textColor="@color/color_3bb9f1"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@+id/tv_comment"
app:layout_constraintTop_toBottomOf="@+id/tv_comment" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="#78909C"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_write_reply" />
</androidx.constraintlayout.widget.ConstraintLayout>