feat(profile): 크리에이터 상세정보를 노출한다

This commit is contained in:
2026-02-25 15:02:50 +09:00
parent c74d27f4ab
commit 5b83ae69dd
17 changed files with 601 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
@@ -43,6 +44,12 @@ interface ExplorerApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorProfileResponse>> ): Single<ApiResponse<GetCreatorProfileResponse>>
@GET("/explorer/profile/{id}/detail")
fun getCreatorDetail(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorDetailResponse>>
@GET("/explorer/profile/{id}/donation-rank") @GET("/explorer/profile/{id}/donation-rank")
fun getCreatorProfileDonationRanking( fun getCreatorProfileDonationRanking(
@Path("id") id: Long, @Path("id") id: Long,

View File

@@ -27,6 +27,11 @@ class ExplorerRepository(
authHeader = token authHeader = token
) )
fun getCreatorDetail(id: Long, token: String) = api.getCreatorDetail(
id = id,
authHeader = token
)
fun getCreatorProfileCheers( fun getCreatorProfileCheers(
creatorId: Long, creatorId: Long,
page: Int, page: Int,

View File

@@ -51,6 +51,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityP
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.relativeTimeText import kr.co.vividnext.sodalive.explorer.profile.creator_community.relativeTimeText
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.explorer.profile.detail.CreatorDetailDialog
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
@@ -711,6 +712,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
if (creator.creatorId == SharedPreferenceManager.userId) { if (creator.creatorId == SharedPreferenceManager.userId) {
binding.ivNotification.visibility = View.GONE binding.ivNotification.visibility = View.GONE
binding.tvNotificationCount.visibility = View.GONE binding.tvNotificationCount.visibility = View.GONE
binding.tvNotificationCount.setOnClickListener(null)
binding.tvFollowerList.visibility = View.VISIBLE binding.tvFollowerList.visibility = View.VISIBLE
binding.tvFollowerList.setOnClickListener { binding.tvFollowerList.setOnClickListener {
@@ -728,6 +730,17 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
R.string.screen_user_profile_follower_count, R.string.screen_user_profile_follower_count,
creator.notificationRecipientCount.moneyFormat() creator.notificationRecipientCount.moneyFormat()
) )
binding.tvNotificationCount.setOnClickListener {
viewModel.getCreatorDetail(creator.creatorId) { detail ->
CreatorDetailDialog(
activity = this@UserProfileActivity,
layoutInflater = layoutInflater,
screenWidth = screenWidth,
detail = detail
).show()
}
}
} }
if (creator.isFollow) { if (creator.isFollow) {

View File

@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.Utils import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.explorer.ExplorerRepository import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
import kr.co.vividnext.sodalive.report.ReportRepository import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType import kr.co.vividnext.sodalive.report.ReportType
@@ -156,6 +157,43 @@ class UserProfileViewModel(
) )
} }
fun getCreatorDetail(userId: Long, onSuccess: (GetCreatorDetailResponse) -> Unit) {
_isLoading.value = true
compositeDisposable.add(
repository.getCreatorDetail(
id = userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
onSuccess(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
fun follow(creatorId: Long, follow: Boolean = true, notify: Boolean = true) { fun follow(creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(

View File

@@ -0,0 +1,157 @@
package kr.co.vividnext.sodalive.explorer.profile.detail
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.webkit.URLUtil
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogCreatorDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class CreatorDetailDialog(
private val activity: Activity,
layoutInflater: LayoutInflater,
private val screenWidth: Int,
private val detail: GetCreatorDetailResponse
) {
private data class SnsItem(
val url: String,
val iconResId: Int
)
private val alertDialog: AlertDialog
private val dialogView = DialogCreatorDetailBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
setupView()
bindData()
}
private fun setupView() {
dialogView.ivClose.setOnClickListener { dismiss() }
}
private fun bindData() {
dialogView.ivProfile.load(detail.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(16f.dpToPx()))
}
dialogView.tvNickname.text = detail.nickname
dialogView.tvDebutValue.text = getDebutValue()
dialogView.tvLiveCountValue.text = detail.activitySummary.liveCount.moneyFormat()
dialogView.tvLiveTimeValue.text = detail.activitySummary.liveTime.moneyFormat()
dialogView.tvLiveContributorCountValue.text = detail.activitySummary.liveContributorCount.moneyFormat()
dialogView.tvContentCountValue.text = detail.activitySummary.contentCount.moneyFormat()
bindSnsItems()
}
private fun getDebutValue(): String {
val debutDate = detail.debutDate.trim()
val dDay = detail.dDay.trim()
if (debutDate.isBlank() && dDay.isBlank()) {
return activity.getString(R.string.screen_creator_detail_debut_before)
}
return "$debutDate ($dDay)"
}
private fun bindSnsItems() {
val snsItems = listOf(
SnsItem(
url = detail.youtubeUrl.trim(),
iconResId = R.drawable.ic_sns_youtube
),
SnsItem(
url = detail.instagramUrl.trim(),
iconResId = R.drawable.ic_sns_instagram
),
SnsItem(
url = detail.kakaoOpenChatUrl.trim(),
iconResId = R.drawable.ic_sns_kakao
),
SnsItem(
url = detail.fancimmUrl.trim(),
iconResId = R.drawable.ic_sns_fancimm
),
SnsItem(
url = detail.xUrl.trim(),
iconResId = R.drawable.ic_sns_x
)
).filter { item ->
item.url.isNotBlank() && URLUtil.isValidUrl(item.url)
}
if (snsItems.isEmpty()) {
dialogView.llSectionSns.visibility = View.GONE
return
}
dialogView.llSectionSns.visibility = View.VISIBLE
dialogView.llSnsIcons.removeAllViews()
snsItems.forEachIndexed { index, item ->
val imageView = ImageView(activity).apply {
setImageResource(item.iconResId)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
if (index > 0) {
marginStart = 12.dpToPx().toInt()
}
}
setOnClickListener {
openUrl(item.url)
}
}
dialogView.llSnsIcons.addView(imageView)
}
}
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
if (intent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(intent)
}
}
private fun dismiss() {
alertDialog.dismiss()
}
fun show() {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = screenWidth - (48.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.explorer.profile.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorActivitySummary
@Keep
data class GetCreatorDetailResponse(
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String,
@SerializedName("debutDate") val debutDate: String,
@SerializedName("dday") val dDay: String,
@SerializedName("activitySummary") val activitySummary: GetCreatorActivitySummary,
@SerializedName("instagramUrl") val instagramUrl: String,
@SerializedName("fancimmUrl") val fancimmUrl: String,
@SerializedName("xurl") val xUrl: String,
@SerializedName("youtubeUrl") val youtubeUrl: String,
@SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_close_white" />

View File

@@ -0,0 +1,225 @@
<?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_round_corner_8_222222">
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_x_white" />
<ScrollView
android:id="@+id/sv_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/iv_close"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="12dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_place_holder" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="36sp"
tools:text="크리에이터 닉네임" />
<LinearLayout
android:id="@+id/ll_section_debut"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_debut_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_debut"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_debut_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="D-17" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_section_live_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_live_count_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_live_count"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_live_count_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="1,234" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_section_live_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_live_time_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_live_time"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_live_time_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="2,345" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_section_live_contributor_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_live_contributor_count_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_live_contributor_count"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_live_contributor_count_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="12,345" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_section_content_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content_count_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_content_count"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_content_count_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/bold"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="567" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_section_sns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="24dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_sns_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_creator_detail_sns"
android:textColor="@color/color_b0bec5"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/ll_sns_icons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@@ -774,7 +774,14 @@
<string name="error_invalid_request">Invalid request.</string> <string name="error_invalid_request">Invalid request.</string>
<string name="screen_user_profile_share_channel">Share channel</string> <string name="screen_user_profile_share_channel">Share channel</string>
<string name="screen_user_profile_follower_list">Follower list</string> <string name="screen_user_profile_follower_list">Follower list</string>
<string name="screen_user_profile_follower_count">Followers %1$s</string> <string name="screen_user_profile_follower_count">Followers %1$s · Details &gt;</string>
<string name="screen_creator_detail_debut">Debut</string>
<string name="screen_creator_detail_debut_before">Before debut</string>
<string name="screen_creator_detail_live_count">Total live sessions</string>
<string name="screen_creator_detail_live_time">Cumulative live time</string>
<string name="screen_creator_detail_live_contributor_count">Cumulative live participants</string>
<string name="screen_creator_detail_content_count">Registered content count</string>
<string name="screen_creator_detail_sns">SNS</string>
<string name="screen_user_profile_latest_content_scheduled">Scheduled</string> <string name="screen_user_profile_latest_content_scheduled">Scheduled</string>
<string name="screen_user_profile_latest_content_point">Points</string> <string name="screen_user_profile_latest_content_point">Points</string>
<string name="screen_user_profile_live_title">Live</string> <string name="screen_user_profile_live_title">Live</string>

View File

@@ -774,7 +774,14 @@
<string name="error_invalid_request">無効なリクエストです。</string> <string name="error_invalid_request">無効なリクエストです。</string>
<string name="screen_user_profile_share_channel">チャンネル共有</string> <string name="screen_user_profile_share_channel">チャンネル共有</string>
<string name="screen_user_profile_follower_list">フォロワーリスト</string> <string name="screen_user_profile_follower_list">フォロワーリスト</string>
<string name="screen_user_profile_follower_count">フォロワー %1$s人</string> <string name="screen_user_profile_follower_count">フォロワー %1$s人 · 詳細情報 &gt;</string>
<string name="screen_creator_detail_debut">デビュー</string>
<string name="screen_creator_detail_debut_before">デビュー前</string>
<string name="screen_creator_detail_live_count">ライブ総回数</string>
<string name="screen_creator_detail_live_time">ライブ累積時間</string>
<string name="screen_creator_detail_live_contributor_count">ライブ累積参加者</string>
<string name="screen_creator_detail_content_count">登録コンテンツ数</string>
<string name="screen_creator_detail_sns">SNS</string>
<string name="screen_user_profile_latest_content_scheduled">公開予定</string> <string name="screen_user_profile_latest_content_scheduled">公開予定</string>
<string name="screen_user_profile_latest_content_point">ポイント</string> <string name="screen_user_profile_latest_content_point">ポイント</string>
<string name="screen_user_profile_live_title">ライブ</string> <string name="screen_user_profile_live_title">ライブ</string>

View File

@@ -773,7 +773,14 @@
<string name="error_invalid_request">잘못된 요청입니다.</string> <string name="error_invalid_request">잘못된 요청입니다.</string>
<string name="screen_user_profile_share_channel">채널 공유</string> <string name="screen_user_profile_share_channel">채널 공유</string>
<string name="screen_user_profile_follower_list">팔로워 리스트</string> <string name="screen_user_profile_follower_list">팔로워 리스트</string>
<string name="screen_user_profile_follower_count">팔로워 %1$s명</string> <string name="screen_user_profile_follower_count">팔로워 %1$s명 · 상세정보 &gt;</string>
<string name="screen_creator_detail_debut">데뷔</string>
<string name="screen_creator_detail_debut_before">데뷔전</string>
<string name="screen_creator_detail_live_count">라이브 총 횟수</string>
<string name="screen_creator_detail_live_time">라이브 누적 시간</string>
<string name="screen_creator_detail_live_contributor_count">라이브 누적 참여자</string>
<string name="screen_creator_detail_content_count">등록 콘텐츠 수</string>
<string name="screen_creator_detail_sns">SNS</string>
<string name="screen_user_profile_latest_content_scheduled">오픈예정</string> <string name="screen_user_profile_latest_content_scheduled">오픈예정</string>
<string name="screen_user_profile_latest_content_point">포인트</string> <string name="screen_user_profile_latest_content_point">포인트</string>
<string name="screen_user_profile_live_title">라이브</string> <string name="screen_user_profile_live_title">라이브</string>

View File

@@ -0,0 +1,110 @@
# 크리에이터 상세정보 다이얼로그 구현 계획
## 구현 체크리스트
- [x] 1단계: 크리에이터 상세정보 다이얼로그 요구사항 및 기존 구현 패턴을 확인한다.
- 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt`, `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/detail/GetCreatorDetailResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/dialog/MemberProfileDialog.kt`, `app/src/main/res/values/strings.xml`
- [x] 2단계: `kr.co.vividnext.sodalive.explorer.profile.detail` 패키지에 크리에이터 상세정보 Custom Dialog UI/로직을 구현한다.
- 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/detail/*.kt`, `app/src/main/res/layout/*.xml`, `app/src/main/res/drawable/*.xml`
- [x] 3단계: `UserProfileActivity`에서 `tvNotificationCount` 클릭 시 상세정보 다이얼로그가 표시되도록 연결한다.
- 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt`, `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileViewModel.kt`, `app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerApi.kt`, `app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt`
- [x] 4단계: 팔로워 문구를 `팔로워 OO명 · 상세정보 >` 형태로 국제화 문자열에 반영한다.
- 대상 파일: `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`
- [x] 5단계: 진단/테스트/빌드 검증을 수행하고 문서 하단 검증 기록에 누적한다.
- 대상 명령: `./gradlew :app:testDebugUnitTest`, `./gradlew :app:assembleDebug`
## 작업 메모
- `@docs/20260224_AGENTS문서정비.md`의 제목/섹션 구조를 기준 포맷으로 채택한다.
- 구현 중 범위 변경 시 체크리스트를 먼저 갱신한 뒤 코드 변경을 진행한다.
- SNS 아이콘/정렬 정책 추가 반영: `ic_sns_*` 아이콘 사용, 노출 순서 `유튜브 -> 인스타그램 -> 오픈채팅 -> fancimm -> x` 고정.
## 검증 기록
### 1) 계획 문서 포맷 정렬
- 무엇: 계획 문서를 `제목 -> 구현 체크리스트 -> 작업 메모 -> 검증 기록` 구조로 정렬.
- 왜: 초기 기준 문서(`docs/20260224_AGENTS문서정비.md`)와 동일한 작성 규칙을 유지하기 위해.
- 어떻게:
- `docs/20260224_AGENTS문서정비.md`의 헤더 구조를 확인.
- `docs/20260225_크리에이터상세정보다이얼로그구현.md`에 제목/체크리스트/메모 섹션을 추가 및 정리.
- 결과: 계획 문서 형식이 기준 문서와 동일한 섹션 구조로 정렬됨.
### 2) 상세정보 다이얼로그 및 문자열 반영
- 무엇: 크리에이터 상세정보 다이얼로그 구현 및 `tvNotificationCount` 연결, 팔로워 문구 국제화 반영.
- 왜: 팔로워 수 클릭 시 상세정보 노출 UX와 다국어 문구 요구사항을 충족하기 위해.
- 어떻게:
- `CreatorDetailDialog`/`dialog_creator_detail.xml`/`ic_x_white.xml` 생성.
- `ExplorerApi`/`ExplorerRepository`/`UserProfileViewModel`에 크리에이터 상세 API 호출 흐름 추가.
- `UserProfileActivity`에서 `tvNotificationCount` 클릭 시 상세 다이얼로그 표시 연결.
- `values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`의 팔로워 문구를 `... · ... >` 형태로 변경하고 상세정보 타이틀 문자열 추가.
- 결과: 요구한 다이얼로그 표시 흐름과 국제화 문자열 반영 완료.
### 3) 진단/테스트/빌드 검증
- 무엇: 변경 파일 진단 및 단위 테스트/디버그 빌드 수행.
- 왜: 구현 반영 후 컴파일/리소스/테스트 안정성을 확인하기 위해.
- 어떻게:
- `lsp_diagnostics`를 수정한 Kotlin/XML 파일에 실행 시도.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과:
- LSP: 현재 환경에서 Kotlin/XML LSP 미설정(`No LSP server configured for extension: .kt/.xml`).
- Gradle: `BUILD SUCCESSFUL` (unit test + debug assemble 통과).
### 4) 추가 수정 후 재검증
- 무엇: SNS 항목 확장 반영(`fancimmUrl`, `websiteUrl`, `blogUrl`) 이후 재검증.
- 왜: 마지막 코드 변경 이후에도 빌드/테스트가 정상인지 확인하기 위해.
- 어떻게:
- `lsp_diagnostics``CreatorDetailDialog.kt` 진단 실행 시도.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 재실행.
- 결과:
- LSP: Kotlin LSP 미설정(`No LSP server configured for extension: .kt`).
- Gradle: `BUILD SUCCESSFUL`.
### 5) 최종 변경 반영 후 검증
- 무엇: SNS 아이템 노출 필드(오픈채팅/인스타그램/유튜브/팬심/X/웹사이트/블로그) 최종 반영 후 재검증.
- 왜: 마지막 수정 이후에도 테스트/빌드 통과 상태를 보장하기 위해.
- 어떻게:
- `lsp_diagnostics``CreatorDetailDialog.kt` 진단 실행 시도.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과:
- LSP: Kotlin LSP 미설정(`No LSP server configured for extension: .kt`).
- Gradle: `BUILD SUCCESSFUL`.
### 6) SNS 순서/아이콘 정책 반영 후 재검증
- 무엇: SNS 노출 순서를 `유튜브 -> 인스타그램 -> 오픈채팅 -> fancimm -> x`로 고정하고 `ic_sns_*` 아이콘 사용으로 통일.
- 왜: 추가 요청된 UI 정책을 정확히 반영하기 위해.
- 어떻게:
- `CreatorDetailDialog.kt`의 SNS 리스트 순서와 아이콘 리소스를 `ic_sns_youtube`, `ic_sns_instagram`, `ic_sns_kakao`, `ic_sns_fancimm`, `ic_sns_x`로 수정.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과: `BUILD SUCCESSFUL`.
### 7) 프로필 이미지 1:1 전체 폭 반영 후 재검증
- 무엇: 상세 다이얼로그 프로필 이미지를 다이얼로그 가로 전체 폭으로 확장하고 1:1 비율로 고정.
- 왜: 추가 요청된 UI 규격(가로 full + 정사각형 비율)을 충족하기 위해.
- 어떻게:
- `dialog_creator_detail.xml`에서 이미지 블록을 상단 full-width ConstraintLayout으로 분리.
- `ImageView``0dp x 0dp` + `app:layout_constraintDimensionRatio="1:1"`로 설정해 가로 기준 정사각형을 강제.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과:
- LSP: XML LSP 미설정(`No LSP server configured for extension: .xml`).
- Gradle: `BUILD SUCCESSFUL`.
### 8) 최근 2회 수정 롤백 및 검증
- 무엇: 직전 2회 수정(라운드/패딩/스크롤 구조 변경, 긴급 표시 수정)을 롤백.
- 왜: 요청에 따라 최근 2회 수정 내용을 원복하기 위해.
- 어떻게:
- `dialog_creator_detail.xml`을 롤백 전 구조(루트 `wrap_content`, 스크롤 `wrap_content`, 이미지 `0dp/0dp + ratio 1:1`)로 복원.
- `CreatorDetailDialog.kt`를 롤백 전 상태(`CircleCropTransformation`, 다이얼로그 높이 `WRAP_CONTENT`, `ivProfile.post` 제거)로 복원.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과:
- LSP: Kotlin/XML LSP 미설정(`No LSP server configured for extension: .kt/.xml`).
- Gradle: `BUILD SUCCESSFUL`.
### 9) 라이브 누적 시간 섹션 추가
- 무엇: `라이브 총 횟수` 아래에 동일 UI 패턴의 `라이브 누적 시간` 섹션을 추가하고 `liveTime` 값을 표시.
- 왜: 추가 요청된 정보 항목을 동일한 상세정보 구조로 노출하기 위해.
- 어떻게:
- `dialog_creator_detail.xml``ll_section_live_time`, `tv_live_time_title`, `tv_live_time_value` 추가.
- `CreatorDetailDialog.kt`에서 `detail.activitySummary.liveTime``moneyFormat()`으로 바인딩.
- `values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml``screen_creator_detail_live_time` 문자열 추가.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행.
- 결과:
- LSP: Kotlin/XML LSP 미설정(`No LSP server configured for extension: .kt/.xml`).
- Gradle: `BUILD SUCCESSFUL`.