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.explorer.profile.GetCheersResponse
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.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
@@ -43,6 +44,12 @@ interface ExplorerApi {
@Header("Authorization") authHeader: String
): 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")
fun getCreatorProfileDonationRanking(
@Path("id") id: Long,

View File

@@ -27,6 +27,11 @@ class ExplorerRepository(
authHeader = token
)
fun getCreatorDetail(id: Long, token: String) = api.getCreatorDetail(
id = id,
authHeader = token
)
fun getCreatorProfileCheers(
creatorId: Long,
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.relativeTimeText
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.UserProfileDonationAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
@@ -711,6 +712,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
if (creator.creatorId == SharedPreferenceManager.userId) {
binding.ivNotification.visibility = View.GONE
binding.tvNotificationCount.visibility = View.GONE
binding.tvNotificationCount.setOnClickListener(null)
binding.tvFollowerList.visibility = View.VISIBLE
binding.tvFollowerList.setOnClickListener {
@@ -728,6 +730,17 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
R.string.screen_user_profile_follower_count,
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) {

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.explorer.ExplorerRepository
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.ReportRequest
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) {
_isLoading.value = true
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="screen_user_profile_share_channel">Share channel</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_point">Points</string>
<string name="screen_user_profile_live_title">Live</string>

View File

@@ -774,7 +774,14 @@
<string name="error_invalid_request">無効なリクエストです。</string>
<string name="screen_user_profile_share_channel">チャンネル共有</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_point">ポイント</string>
<string name="screen_user_profile_live_title">ライブ</string>

View File

@@ -773,7 +773,14 @@
<string name="error_invalid_request">잘못된 요청입니다.</string>
<string name="screen_user_profile_share_channel">채널 공유</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_point">포인트</string>
<string name="screen_user_profile_live_title">라이브</string>