15 Commits
Author SHA1 Message Date
klaus 1f8d0beb2f fix(my): 배너 이미지 리소스를 정리한다 2026-08-26 17:13:11 +09:00
klaus 10886153b2 docs(creator): 채널 표시 정리 문서를 완료한다 2026-08-26 16:30:16 +09:00
klaus 1f88d721d7 feat(creator): 채널 표시와 상대 시간을 정리한다 2026-08-26 16:30:03 +09:00
klaus 9939910c71 chore(app): versionCode를 250으로 올린다 2026-08-26 16:29:55 +09:00
klaus 4f4ae62026 fix(creator): 후원 item locale 표시를 적용한다 2026-08-26 13:27:20 +09:00
klaus 3560d7bf89 chore(app): versionCode를 249로 올린다 2026-08-26 13:24:45 +09:00
klaus 5cfc42774c test(ui): 문구 변경 검증을 정리한다 2026-08-26 00:56:18 +09:00
klaus 4130ab296e fix(home): 모두 팔로우 확인 문구를 정리한다 2026-08-25 16:38:43 +09:00
klaus 87d9a1d377 fix(fanletter-write): 오른쪽 하단에 있는 글자수 단위(자) 제거 2026-08-25 16:04:43 +09:00
klaus 70f4266ed8 fix(ui): 텍스트 탭 가로 스크롤을 적용한다 2026-08-25 15:57:10 +09:00
klaus 7a9c2512cf fix(brand): 보이스온 표기를 소다라이브로 교체한다 2026-08-25 13:59:55 +09:00
klaus a04704db40 fix(content): 콘텐츠, 시리즈 오리지널 아이콘 리소스를 교체한다 2026-08-25 12:26:30 +09:00
klaus ec808cfd0c fix(home): 텍스트 로고 리소스를 교체한다 2026-08-25 12:18:51 +09:00
klaus 817234a80b fix(app): 런처 아이콘 리소스를 교체한다 2026-08-25 12:09:55 +09:00
klaus 88cb581231 fix(splash): 로고와 문구 배치를 정리한다 2026-08-25 11:49:19 +09:00
111 changed files with 1756 additions and 345 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 35 targetSdk 35
versionCode 246 versionCode 250
versionName "1.56.1" versionName "1.56.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">VoiceOn-Test</string> <string name="app_name">SodaLive-Test</string>
</resources> </resources>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">ボイスオン-テスト</string> <string name="app_name">ソーダライブ-テスト</string>
</resources> </resources>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">보이스온-테스트</string> <string name="app_name">소다라이브-테스트</string>
</resources> </resources>
@@ -60,7 +60,7 @@ class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) { if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) {
getString(R.string.screen_series_main_completed_title) getString(R.string.screen_series_main_completed_title)
} else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) { } else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) {
getString(R.string.voiceon_original_only) getString(R.string.sodalive_original_only)
} else { } else {
getString(R.string.screen_series_main_title) getString(R.string.screen_series_main_title)
} }
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 캐릭터 정보 모델 * 소다라이브 - 채팅방 캐릭터 정보 모델
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 모델 (서버 응답 기반) * 소다라이브 - 채팅 메시지 모델 (서버 응답 기반)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 어댑터 (4.2 ViewHolder 구현) * 소다라이브 - 채팅방 메시지 어댑터 (4.2 ViewHolder 구현)
* - 4가지 ViewType 지원 * - 4가지 ViewType 지원
* - ViewHolder 바인딩 로직 구현: 시간 포맷팅, 상태 표시, 이미지 로딩, 그룹화 처리, 타이핑 애니메이션 * - ViewHolder 바인딩 로직 구현: 시간 포맷팅, 상태 표시, 이미지 로딩, 그룹화 처리, 타이핑 애니메이션
*/ */
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Entity/도메인 매퍼 * 소다라이브 - 채팅 메시지 Entity/도메인 매퍼
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 매핑 유틸리티 * 소다라이브 - 채팅 메시지 매핑 유틸리티
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 점진적 메시지 로딩 응답 DTO * 소다라이브 - 점진적 메시지 로딩 응답 DTO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 ChatRepository (Repository 패턴) * 소다라이브 - 채팅방 ChatRepository (Repository 패턴)
* - 로컬 DB(Room) + 네트워크(TalkApi) 통합 관리 * - 로컬 DB(Room) + 네트워크(TalkApi) 통합 관리
* - 통합 채팅방 입장 API 호출 및 로컬 동기화 * - 통합 채팅방 입장 API 호출 및 로컬 동기화
* - 점진적 메시지 로딩 * - 점진적 메시지 로딩
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 통합 채팅방 입장 응답 DTO * 소다라이브 - 통합 채팅방 입장 응답 DTO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 전송 상태 Enum * 소다라이브 - 채팅방 메시지 전송 상태 Enum
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 메시지 타입 Enum * 소다라이브 - 채팅방 메시지 타입 Enum
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 모델 (서버 응답 전용 DTO) * 소다라이브 - 채팅 메시지 모델 (서버 응답 전용 DTO)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room package kr.co.vividnext.sodalive.chat.talk.room
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Room TypeConverters * 소다라이브 - 채팅 메시지 Room TypeConverters
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 DAO * 소다라이브 - 채팅 메시지 DAO
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅방 Room Database (ChatMessage 테이블 포함) * 소다라이브 - 채팅방 Room Database (ChatMessage 테이블 포함)
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -1,5 +1,5 @@
/* /*
* 보이스온 - 채팅 메시지 Room Entity * 소다라이브 - 채팅 메시지 Room Entity
*/ */
package kr.co.vividnext.sodalive.chat.talk.room.db package kr.co.vividnext.sodalive.chat.talk.room.db
@@ -463,13 +463,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorChannelLiveViewModel(get()) } viewModel { CreatorChannelLiveViewModel(get()) }
viewModel { CreatorChannelAudioViewModel(get()) } viewModel { CreatorChannelAudioViewModel(get()) }
viewModel { CreatorChannelSeriesViewModel(get()) } viewModel { CreatorChannelSeriesViewModel(get()) }
viewModel { CreatorChannelCommunityViewModel(get(), get()) } viewModel { CreatorChannelCommunityViewModel(get()) }
viewModel { CreatorChannelCommunityDetailViewModel(get(), get(), get()) } viewModel { CreatorChannelCommunityDetailViewModel(get(), get()) }
viewModel { CreatorChannelCommunityReplyViewModel(get(), get(), get()) } viewModel { CreatorChannelCommunityReplyViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkViewModel(get(), get()) } viewModel { CreatorChannelFanTalkViewModel(get()) }
viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) } viewModel { CreatorChannelFanTalkDetailViewModel(get()) }
viewModel { CreatorChannelFanTalkWriteViewModel(get()) } viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
viewModel { CreatorChannelDonationViewModel(get(), get()) } viewModel { CreatorChannelDonationViewModel(get()) }
viewModel { PushNotificationListViewModel(get()) } viewModel { PushNotificationListViewModel(get()) }
viewModel { CharacterTabViewModel(get()) } viewModel { CharacterTabViewModel(get()) }
viewModel { CharacterDetailViewModel(get()) } viewModel { CharacterDetailViewModel(get()) }
@@ -359,7 +359,7 @@ class UserProfileViewModel(
"deep_link_sub5" to "$userId" "deep_link_sub5" to "$userId"
) )
val shareUrl = Utils.createOneLinkUrl(params = params) val shareUrl = Utils.createOneLinkUrl(params = params)
onSuccess("보이스온 ${nickname}님의 채널입니다.\n$shareUrl") onSuccess("소다라이브 ${nickname}님의 채널입니다.\n$shareUrl")
} }
fun getChannelDonationList(creatorId: Long) { fun getChannelDonationList(creatorId: Long) {
@@ -604,7 +604,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
recyclerView.adapter = originalSeriesAdapter recyclerView.adapter = originalSeriesAdapter
binding.tvSeriesOriginalAll.setOnClickListener { binding.tvSeriesOriginalAll.setOnClickListener {
// ‘오직 보이스온에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입 // ‘오직 소다라이브에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent( Intent(
@@ -436,12 +436,12 @@ class LiveRoomViewModel(
val message = if (isPrivateRoom) { val message = if (isPrivateRoom) {
"${SharedPreferenceManager.nickname}님이 귀하를 " + "${SharedPreferenceManager.nickname}님이 귀하를 " +
"보이스온의 비공개라이브에 초대하였습니다.\n" + "소다라이브의 비공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $shareUrl\n" + "※ 라이브 참여: $shareUrl\n" +
"(입장 비밀번호 : $password)" "(입장 비밀번호 : $password)"
} else { } else {
"${SharedPreferenceManager.nickname}님이 귀하를 " + "${SharedPreferenceManager.nickname}님이 귀하를 " +
"보이스온의 공개라이브에 초대하였습니다.\n" + "소다라이브의 공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $shareUrl" "※ 라이브 참여: $shareUrl"
} }
@@ -183,7 +183,7 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
} }
} }
binding.ivIntroduceVoiceon.setOnClickListener { binding.ivIntroduceSodalive.setOnClickListener {
val url = "https://blog.naver.com/sodalive_official" val url = "https://blog.naver.com/sodalive_official"
if (URLUtil.isValidUrl(url)) { if (URLUtil.isValidUrl(url)) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
@@ -7,15 +7,13 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.toCommunityPostUiModels import kr.co.vividnext.sodalive.v2.creator.channel.community.model.toCommunityPostUiModels
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
class CreatorChannelCommunityViewModel( class CreatorChannelCommunityViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _communityStateLiveData = MutableLiveData<CreatorChannelCommunityUiState>() private val _communityStateLiveData = MutableLiveData<CreatorChannelCommunityUiState>()
@@ -158,7 +156,6 @@ class CreatorChannelCommunityViewModel(
private fun CreatorChannelCommunityTabResponse.toCommunityPostUiModels(): List<CreatorChannelCommunityPostUiModel> = private fun CreatorChannelCommunityTabResponse.toCommunityPostUiModels(): List<CreatorChannelCommunityPostUiModel> =
communityPosts.toCommunityPostUiModels( communityPosts.toCommunityPostUiModels(
relativeTimeTextFormatter = relativeTimeTextFormatter,
isOwner = isOwner, isOwner = isOwner,
currentUserId = SharedPreferenceManager.userId currentUserId = SharedPreferenceManager.userId
) )
@@ -24,6 +24,7 @@ import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityReportDialog
@@ -171,7 +172,8 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
tvCreatorChannelCommunityDetailTime.text = post.createdAtText tvCreatorChannelCommunityDetailTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityDetailTime.context, post.createdAtUtc)
btnCreatorChannelCommunityDetailMore.isVisible = post.showMore btnCreatorChannelCommunityDetailMore.isVisible = post.showMore
tvCreatorChannelCommunityDetailBody.text = post.content tvCreatorChannelCommunityDetailBody.text = post.content
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
@@ -11,7 +11,6 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.ModifyCommunityPostRequest
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
@@ -25,8 +24,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
class CreatorChannelCommunityDetailViewModel( class CreatorChannelCommunityDetailViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository,
private val legacyRepository: CreatorCommunityRepository, private val legacyRepository: CreatorCommunityRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _detailStateLiveData = MutableLiveData<CreatorChannelCommunityDetailUiState>() private val _detailStateLiveData = MutableLiveData<CreatorChannelCommunityDetailUiState>()
@@ -536,7 +534,7 @@ class CreatorChannelCommunityDetailViewModel(
creatorId = creatorId, creatorId = creatorId,
creatorNickname = creatorNickname, creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfileUrl, creatorProfileUrl = creatorProfileUrl,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
content = content, content = content,
imageUrl = imageUrl.takeUnless { isLocked() }, imageUrl = imageUrl.takeUnless { isLocked() },
audioUrl = audioUrl.takeUnless { isLocked() }, audioUrl = audioUrl.takeUnless { isLocked() },
@@ -566,7 +564,6 @@ class CreatorChannelCommunityDetailViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
replyCount = if (latestReply != null) 1 else 0, replyCount = if (latestReply != null) 1 else 0,
@@ -586,7 +583,6 @@ class CreatorChannelCommunityDetailViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
isLiked = false, isLiked = false,
@@ -625,7 +621,7 @@ data class CreatorChannelCommunityPostDetailUiModel(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileUrl: String?, val creatorProfileUrl: String?,
val createdAtText: String, val createdAtUtc: String,
val content: String, val content: String,
val imageUrl: String?, val imageUrl: String?,
val audioUrl: String?, val audioUrl: String?,
@@ -648,7 +644,6 @@ data class CreatorChannelCommunityCommentUiModel(
val memberNickname: String, val memberNickname: String,
val memberProfileUrl: String?, val memberProfileUrl: String?,
val createdAtUtc: String, val createdAtUtc: String,
val createdAtText: String,
val comment: String, val comment: String,
val likeCount: Int, val likeCount: Int,
val replyCount: Int, val replyCount: Int,
@@ -665,7 +660,6 @@ data class CreatorChannelCommunityReplyUiModel(
val memberNickname: String, val memberNickname: String,
val memberProfileUrl: String?, val memberProfileUrl: String?,
val createdAtUtc: String, val createdAtUtc: String,
val createdAtText: String,
val comment: String, val comment: String,
val likeCount: Int, val likeCount: Int,
val isLiked: Boolean, val isLiked: Boolean,
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityReplyBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -110,6 +111,11 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
onModifyClick = { comment -> viewModel.startParentCommentEdit(comment.comment) }, onModifyClick = { comment -> viewModel.startParentCommentEdit(comment.comment) },
onDeleteClick = { showDeleteParentCommentDialog() } onDeleteClick = { showDeleteParentCommentDialog() }
) )
layoutCreatorChannelCommunityReplyParent.tvCreatorChannelCommunityCommentTime.text =
formatUtcRelativeTimeText(
layoutCreatorChannelCommunityReplyParent.tvCreatorChannelCommunityCommentTime.context,
content.parentComment.createdAtUtc
)
layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false
replyAdapter.submitItems(content.replies) replyAdapter.submitItems(content.replies)
if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) { if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) {
@@ -183,7 +189,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(), memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(),
memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL), memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL),
createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(), createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(),
createdAtText = intent.getStringExtra(EXTRA_CREATED_AT_TEXT).orEmpty(),
comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(), comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(),
likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0), likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0),
replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0), replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0),
@@ -201,7 +206,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname" private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname"
private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url" private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url"
private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc" private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc"
private const val EXTRA_CREATED_AT_TEXT = "extra_created_at_text"
private const val EXTRA_COMMENT = "extra_comment" private const val EXTRA_COMMENT = "extra_comment"
private const val EXTRA_LIKE_COUNT = "extra_like_count" private const val EXTRA_LIKE_COUNT = "extra_like_count"
private const val EXTRA_REPLY_COUNT = "extra_reply_count" private const val EXTRA_REPLY_COUNT = "extra_reply_count"
@@ -217,7 +221,6 @@ class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannel
.putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname) .putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname)
.putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl) .putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl)
.putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc) .putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc)
.putExtra(EXTRA_CREATED_AT_TEXT, parentComment.createdAtText)
.putExtra(EXTRA_COMMENT, parentComment.comment) .putExtra(EXTRA_COMMENT, parentComment.comment)
.putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount) .putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount)
.putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount) .putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount)
@@ -10,7 +10,6 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -21,8 +20,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
class CreatorChannelCommunityReplyViewModel( class CreatorChannelCommunityReplyViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository,
private val legacyRepository: CreatorCommunityRepository, private val legacyRepository: CreatorCommunityRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>() private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>()
@@ -57,20 +55,17 @@ class CreatorChannelCommunityReplyViewModel(
private var isModifyingReply = false private var isModifyingReply = false
fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) { fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) {
val formattedParentComment = parentComment.copy(
createdAtText = relativeTimeTextFormatter.format(parentComment.createdAtUtc)
)
this.postId = postId this.postId = postId
this.parentComment = formattedParentComment this.parentComment = parentComment
replyRequestGeneration++ replyRequestGeneration++
isLoadingReplies = false isLoadingReplies = false
_replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content( _replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content(
parentComment = formattedParentComment, parentComment = parentComment,
replies = emptyList(), replies = emptyList(),
replyPage = FIRST_PAGE, replyPage = FIRST_PAGE,
hasNextReply = false hasNextReply = false
) )
if (formattedParentComment.commentId > 0) { if (parentComment.commentId > 0) {
loadReplyPage(page = FIRST_PAGE, append = false) loadReplyPage(page = FIRST_PAGE, append = false)
} }
} }
@@ -486,7 +481,6 @@ class CreatorChannelCommunityReplyViewModel(
memberNickname = writerNickname, memberNickname = writerNickname,
memberProfileUrl = writerProfileImageUrl, memberProfileUrl = writerProfileImageUrl,
createdAtUtc = createdAtUtc, createdAtUtc = createdAtUtc,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
comment = content, comment = content,
likeCount = 0, likeCount = 0,
isLiked = false, isLiked = false,
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
@@ -51,7 +52,8 @@ class CreatorChannelCommunityReplyAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname
tvCreatorChannelCommunityReplyItemTime.text = item.createdAtText tvCreatorChannelCommunityReplyItemTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityReplyItemTime.context, item.createdAtUtc)
tvCreatorChannelCommunityReplyItemBody.text = item.comment tvCreatorChannelCommunityReplyItemBody.text = item.comment
ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore
ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor -> ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor ->
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityCommentBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityCommentBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
@@ -70,7 +71,8 @@ class CreatorChannelCommunityCommentAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityCommentNickname.text = item.memberNickname tvCreatorChannelCommunityCommentNickname.text = item.memberNickname
tvCreatorChannelCommunityCommentTime.text = item.createdAtText tvCreatorChannelCommunityCommentTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityCommentTime.context, item.createdAtUtc)
tvCreatorChannelCommunityCommentBody.text = item.comment tvCreatorChannelCommunityCommentBody.text = item.comment
ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null
ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor -> ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
@@ -1,20 +1,17 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.model package kr.co.vividnext.sodalive.v2.creator.channel.community.model
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse
private const val GRID_PREVIEW_MAX_LENGTH = 24 private const val GRID_PREVIEW_MAX_LENGTH = 24
fun List<CreatorChannelCommunityPostResponse>.toCommunityPostUiModels( fun List<CreatorChannelCommunityPostResponse>.toCommunityPostUiModels(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): List<CreatorChannelCommunityPostUiModel> = map { ): List<CreatorChannelCommunityPostUiModel> = map {
it.toCommunityPostUiModel(relativeTimeTextFormatter, isOwner, currentUserId) it.toCommunityPostUiModel(isOwner, currentUserId)
} }
private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel( private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): CreatorChannelCommunityPostUiModel { ): CreatorChannelCommunityPostUiModel {
@@ -26,7 +23,7 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
creatorId = creatorId, creatorId = creatorId,
creatorNickname = creatorNickname, creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfileUrl, creatorProfileUrl = creatorProfileUrl,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
content = content, content = content,
imageUrl = visibleImageUrl, imageUrl = visibleImageUrl,
price = price, price = price,
@@ -29,7 +29,7 @@ data class CreatorChannelCommunityPostUiModel(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String, val creatorNickname: String,
val creatorProfileUrl: String, val creatorProfileUrl: String,
val createdAtText: String, val createdAtUtc: String,
val content: String, val content: String,
val imageUrl: String?, val imageUrl: String?,
val price: Int, val price: Int,
@@ -15,6 +15,7 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -68,7 +69,8 @@ class CreatorChannelCommunityListAdapter(
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
tvCreatorChannelCommunityListNickname.text = item.creatorNickname tvCreatorChannelCommunityListNickname.text = item.creatorNickname
tvCreatorChannelCommunityListTime.text = item.createdAtText tvCreatorChannelCommunityListTime.text =
formatUtcRelativeTimeText(tvCreatorChannelCommunityListTime.context, item.createdAtUtc)
layoutCreatorChannelCommunityListNotice.isVisible = item.showNotice layoutCreatorChannelCommunityListNotice.isVisible = item.showNotice
tvCreatorChannelCommunityListBody.text = item.content tvCreatorChannelCommunityListBody.text = item.content
layoutCreatorChannelCommunityListReaction.isVisible = item.showReaction || item.showComment layoutCreatorChannelCommunityListReaction.isVisible = item.showReaction || item.showComment
@@ -194,7 +194,7 @@ private data class CreatorChannelDonationItemLayoutKey(
val nickname: String, val nickname: String,
val can: Int, val can: Int,
val message: String, val message: String,
val createdAtText: String val createdAtUtc: String
) )
private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorChannelDonationContentLayoutKey { private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorChannelDonationContentLayoutKey {
@@ -206,7 +206,7 @@ private fun CreatorChannelDonationUiState.Content.toContentLayoutKey(): CreatorC
nickname = donation.nickname, nickname = donation.nickname,
can = donation.can, can = donation.can,
message = donation.message, message = donation.message,
createdAtText = donation.createdAtText createdAtUtc = donation.createdAtUtc
) )
} }
) )
@@ -7,8 +7,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationRankingUiModel import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationRankingUiModel
@@ -17,15 +15,13 @@ import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationRank
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels
class CreatorChannelDonationViewModel( class CreatorChannelDonationViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _donationStateLiveData = MutableLiveData<CreatorChannelDonationUiState>() private val _donationStateLiveData = MutableLiveData<CreatorChannelDonationUiState>()
val donationStateLiveData: LiveData<CreatorChannelDonationUiState> val donationStateLiveData: LiveData<CreatorChannelDonationUiState>
get() = _donationStateLiveData get() = _donationStateLiveData
private val context = SodaLiveApplicationHolder.get()
private var creatorId: Long = 0L private var creatorId: Long = 0L
private var isOwner: Boolean = false private var isOwner: Boolean = false
private var requestGeneration: Int = 0 private var requestGeneration: Int = 0
@@ -222,7 +218,7 @@ class CreatorChannelDonationViewModel(
rankings.toDonationRankingUiModels() rankings.toDonationRankingUiModels()
private fun CreatorChannelDonationTabResponse.toDonationUiModels(): List<CreatorChannelDonationUiModel> = private fun CreatorChannelDonationTabResponse.toDonationUiModels(): List<CreatorChannelDonationUiModel> =
donations.toDonationUiModels(context, relativeTimeTextFormatter) donations.toDonationUiModels()
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}" private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
@@ -1,19 +1,15 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.model package kr.co.vividnext.sodalive.v2.creator.channel.donation.model
import android.content.Context
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse
fun List<MemberDonationRankingResponse>.toDonationRankingUiModels(): List<CreatorChannelDonationRankingUiModel> = fun List<MemberDonationRankingResponse>.toDonationRankingUiModels(): List<CreatorChannelDonationRankingUiModel> =
mapIndexed { index, response -> response.toDonationRankingUiModel(rank = index + 1) } mapIndexed { index, response -> response.toDonationRankingUiModel(rank = index + 1) }
fun List<CreatorChannelDonationResponse>.toDonationUiModels( fun List<CreatorChannelDonationResponse>.toDonationUiModels(): List<CreatorChannelDonationUiModel> =
context: Context, map { it.toDonationUiModel() }
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
): List<CreatorChannelDonationUiModel> = map { it.toDonationUiModel(context, relativeTimeTextFormatter) }
private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) = CreatorChannelDonationRankingUiModel( private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) = CreatorChannelDonationRankingUiModel(
rank = rank, rank = rank,
@@ -23,15 +19,12 @@ private fun MemberDonationRankingResponse.toDonationRankingUiModel(rank: Int) =
donationCan = donationCan donationCan = donationCan
) )
private fun CreatorChannelDonationResponse.toDonationUiModel( private fun CreatorChannelDonationResponse.toDonationUiModel() = CreatorChannelDonationUiModel(
context: Context,
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelDonationUiModel(
nickname = nickname, nickname = nickname,
profileImageUrl = profileImageUrl, profileImageUrl = profileImageUrl,
can = can, can = can,
message = message.takeUnless { it.isBlank() } ?: context.getString(R.string.creator_channel_donation_fallback_message, can), message = message,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
headerColorResId = calculateDonationHeaderColorRes(can) headerColorResId = calculateDonationHeaderColorRes(can)
) )
@@ -15,6 +15,6 @@ data class CreatorChannelDonationUiModel(
val profileImageUrl: String, val profileImageUrl: String,
val can: Int, val can: Int,
val message: String, val message: String,
val createdAtText: String, val createdAtUtc: String,
@param:ColorRes val headerColorResId: Int @param:ColorRes val headerColorResId: Int
) )
@@ -8,6 +8,8 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding
@@ -115,6 +117,7 @@ class CreatorChannelDonationAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CreatorChannelDonationUiModel) = with(binding) { fun bind(item: CreatorChannelDonationUiModel) = with(binding) {
val context = root.context
bindCreatorChannelDonationCard( bindCreatorChannelDonationCard(
headerView = layoutCreatorChannelDonationHeader, headerView = layoutCreatorChannelDonationHeader,
profileView = ivCreatorChannelDonationProfile, profileView = ivCreatorChannelDonationProfile,
@@ -125,9 +128,10 @@ class CreatorChannelDonationAdapter(
headerColorResId = item.headerColorResId, headerColorResId = item.headerColorResId,
profileImageUrl = item.profileImageUrl, profileImageUrl = item.profileImageUrl,
nickname = item.nickname, nickname = item.nickname,
createdAtText = item.createdAtText, createdAtText = formatUtcRelativeTimeText(context, item.createdAtUtc),
can = item.can, can = item.can,
message = item.message message = item.message.takeUnless { it.isBlank() }
?: context.getString(R.string.creator_channel_donation_fallback_message, item.can)
) )
} }
} }
@@ -9,15 +9,13 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkUiModels import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkUiModels
class CreatorChannelFanTalkViewModel( class CreatorChannelFanTalkViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _fanTalkStateLiveData = MutableLiveData<CreatorChannelFanTalkUiState>() private val _fanTalkStateLiveData = MutableLiveData<CreatorChannelFanTalkUiState>()
@@ -198,7 +196,6 @@ class CreatorChannelFanTalkViewModel(
private fun CreatorChannelFanTalkTabResponse.toFanTalkUiModels(): List<CreatorChannelFanTalkUiModel> = private fun CreatorChannelFanTalkTabResponse.toFanTalkUiModels(): List<CreatorChannelFanTalkUiModel> =
fanTalks.toFanTalkUiModels( fanTalks.toFanTalkUiModels(
relativeTimeTextFormatter = relativeTimeTextFormatter,
isOwner = isOwner, isOwner = isOwner,
currentUserId = SharedPreferenceManager.userId currentUserId = SharedPreferenceManager.userId
) )
@@ -19,6 +19,7 @@ import androidx.core.widget.doAfterTextChanged
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding
import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -107,7 +108,8 @@ class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelF
private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) { private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) {
ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl) ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl)
tvCreatorChannelFantalkDetailNickname.text = item.writerNickname tvCreatorChannelFantalkDetailNickname.text = item.writerNickname
tvCreatorChannelFantalkDetailTime.text = item.createdAtText tvCreatorChannelFantalkDetailTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkDetailTime.context, item.createdAtUtc)
tvCreatorChannelFantalkDetailContent.text = item.content tvCreatorChannelFantalkDetailContent.text = item.content
ivCreatorChannelFantalkDetailMore.isVisible = false ivCreatorChannelFantalkDetailMore.isVisible = false
ivCreatorChannelFantalkDetailMore.setOnClickListener(null) ivCreatorChannelFantalkDetailMore.setOnClickListener(null)
@@ -123,7 +125,8 @@ class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelF
ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl) ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl)
tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname
tvCreatorChannelFantalkDetailReplyTime.text = reply.createdAtText tvCreatorChannelFantalkDetailReplyTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkDetailReplyTime.context, reply.createdAtUtc)
tvCreatorChannelFantalkDetailReplyContent.text = reply.content tvCreatorChannelFantalkDetailReplyContent.text = reply.content
ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L
ivCreatorChannelFantalkDetailReplyMore.setOnClickListener( ivCreatorChannelFantalkDetailReplyMore.setOnClickListener(
@@ -15,7 +15,7 @@ data class CreatorChannelFanTalkDetailPayload(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String, val createdAtUtc: String,
val reply: CreatorChannelFanTalkDetailReplyPayload?, val reply: CreatorChannelFanTalkDetailReplyPayload?,
val showEdit: Boolean, val showEdit: Boolean,
val showDelete: Boolean val showDelete: Boolean
@@ -26,7 +26,7 @@ data class CreatorChannelFanTalkDetailPayload(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText, createdAtUtc = createdAtUtc,
reply = reply?.toUiModel(), reply = reply?.toUiModel(),
rightAction = CreatorChannelFanTalkRightAction.OwnerMore(showEdit = showEdit, showDelete = showDelete) rightAction = CreatorChannelFanTalkRightAction.OwnerMore(showEdit = showEdit, showDelete = showDelete)
) )
@@ -39,7 +39,7 @@ data class CreatorChannelFanTalkDetailReplyPayload(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String val createdAtUtc: String
) : Parcelable { ) : Parcelable {
fun toUiModel(): CreatorChannelFanTalkReplyUiModel = CreatorChannelFanTalkReplyUiModel( fun toUiModel(): CreatorChannelFanTalkReplyUiModel = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
@@ -47,7 +47,7 @@ data class CreatorChannelFanTalkDetailReplyPayload(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText createdAtUtc = createdAtUtc
) )
} }
@@ -59,7 +59,7 @@ fun CreatorChannelFanTalkUiModel.toFanTalkDetailPayload(): CreatorChannelFanTalk
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText, createdAtUtc = createdAtUtc,
reply = reply?.toFanTalkDetailReplyPayload(), reply = reply?.toFanTalkDetailReplyPayload(),
showEdit = ownerMore?.showEdit == true, showEdit = ownerMore?.showEdit == true,
showDelete = ownerMore?.showDelete == true showDelete = ownerMore?.showDelete == true
@@ -73,7 +73,7 @@ fun CreatorChannelFanTalkReplyUiModel.toFanTalkDetailReplyPayload(): CreatorChan
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = createdAtText createdAtUtc = createdAtUtc
) )
} }
@@ -11,15 +11,13 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel
class CreatorChannelFanTalkDetailViewModel( class CreatorChannelFanTalkDetailViewModel(
private val repository: CreatorChannelRepository, private val repository: CreatorChannelRepository
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) : BaseViewModel() { ) : BaseViewModel() {
private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>() private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>()
@@ -191,7 +189,7 @@ class CreatorChannelFanTalkDetailViewModel(
val current = currentContent() ?: return val current = currentContent() ?: return
if (response.success) { if (response.success) {
_detailStateLiveData.value = current.copy( _detailStateLiveData.value = current.copy(
reply = response.data?.toFanTalkReplyUiModel(relativeTimeTextFormatter) ?: current.reply, reply = response.data?.toFanTalkReplyUiModel() ?: current.reply,
replyInput = "", replyInput = "",
isSubmitting = false, isSubmitting = false,
editingTarget = null editingTarget = null
@@ -1,19 +1,16 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse
fun List<CreatorChannelFanTalkResponse>.toFanTalkUiModels( fun List<CreatorChannelFanTalkResponse>.toFanTalkUiModels(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
): List<CreatorChannelFanTalkUiModel> = map { ): List<CreatorChannelFanTalkUiModel> = map {
it.toFanTalkUiModel(relativeTimeTextFormatter, isOwner, currentUserId) it.toFanTalkUiModel(isOwner, currentUserId)
} }
private fun CreatorChannelFanTalkResponse.toFanTalkUiModel( private fun CreatorChannelFanTalkResponse.toFanTalkUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter,
isOwner: Boolean, isOwner: Boolean,
currentUserId: Long currentUserId: Long
) = CreatorChannelFanTalkUiModel( ) = CreatorChannelFanTalkUiModel(
@@ -22,31 +19,27 @@ private fun CreatorChannelFanTalkResponse.toFanTalkUiModel(
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc), createdAtUtc = createdAtUtc,
reply = creatorReplies.orEmpty().firstOrNull()?.toFanTalkReplyUiModel(relativeTimeTextFormatter), reply = creatorReplies.orEmpty().firstOrNull()?.toFanTalkReplyUiModel(),
rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId) rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId)
) )
fun CreatorChannelFanTalkReplyResponse.toFanTalkReplyUiModel( fun CreatorChannelFanTalkReplyResponse.toFanTalkReplyUiModel() = CreatorChannelFanTalkReplyUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc) createdAtUtc = createdAtUtc
) )
fun CreatorChannelFanTalkResponse.toFanTalkReplyUiModel( fun CreatorChannelFanTalkResponse.toFanTalkReplyUiModel() = CreatorChannelFanTalkReplyUiModel(
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
) = CreatorChannelFanTalkReplyUiModel(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = writerNickname, writerNickname = writerNickname,
writerProfileImageUrl = writerProfileImageUrl, writerProfileImageUrl = writerProfileImageUrl,
content = content, content = content,
createdAtText = relativeTimeTextFormatter.format(createdAtUtc) createdAtUtc = createdAtUtc
) )
private fun CreatorChannelFanTalkResponse.toRightAction( private fun CreatorChannelFanTalkResponse.toRightAction(
@@ -6,7 +6,7 @@ data class CreatorChannelFanTalkUiModel(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String, val createdAtUtc: String,
val reply: CreatorChannelFanTalkReplyUiModel?, val reply: CreatorChannelFanTalkReplyUiModel?,
val rightAction: CreatorChannelFanTalkRightAction val rightAction: CreatorChannelFanTalkRightAction
) )
@@ -17,7 +17,7 @@ data class CreatorChannelFanTalkReplyUiModel(
val writerNickname: String, val writerNickname: String,
val writerProfileImageUrl: String, val writerProfileImageUrl: String,
val content: String, val content: String,
val createdAtText: String val createdAtUtc: String
) )
sealed interface CreatorChannelFanTalkRightAction { sealed interface CreatorChannelFanTalkRightAction {
@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelFantalkBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelFantalkBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
@@ -51,7 +52,8 @@ class CreatorChannelFanTalkAdapter(
fun bind(item: CreatorChannelFanTalkUiModel) = with(binding) { fun bind(item: CreatorChannelFanTalkUiModel) = with(binding) {
ivCreatorChannelFantalkProfile.loadProfile(item.writerProfileImageUrl) ivCreatorChannelFantalkProfile.loadProfile(item.writerProfileImageUrl)
tvCreatorChannelFantalkNickname.text = item.writerNickname tvCreatorChannelFantalkNickname.text = item.writerNickname
tvCreatorChannelFantalkTime.text = item.createdAtText tvCreatorChannelFantalkTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkTime.context, item.createdAtUtc)
tvCreatorChannelFantalkContent.text = item.content tvCreatorChannelFantalkContent.text = item.content
root.setOnClickListener { onItemClick(item) } root.setOnClickListener { onItemClick(item) }
tvCreatorChannelFantalkContent.setOnClickListener { onItemClick(item) } tvCreatorChannelFantalkContent.setOnClickListener { onItemClick(item) }
@@ -63,6 +65,7 @@ class CreatorChannelFanTalkAdapter(
when (item.rightAction) { when (item.rightAction) {
CreatorChannelFanTalkRightAction.Report -> { CreatorChannelFanTalkRightAction.Report -> {
tvCreatorChannelFantalkReport.isVisible = true tvCreatorChannelFantalkReport.isVisible = true
tvCreatorChannelFantalkReport.text = root.context.getString(R.string.creator_channel_fantalk_report)
tvCreatorChannelFantalkReport.setOnClickListener { onReportClick(item) } tvCreatorChannelFantalkReport.setOnClickListener { onReportClick(item) }
ivCreatorChannelFantalkMore.isVisible = false ivCreatorChannelFantalkMore.isVisible = false
ivCreatorChannelFantalkMore.setOnClickListener(null) ivCreatorChannelFantalkMore.setOnClickListener(null)
@@ -84,7 +87,8 @@ class CreatorChannelFanTalkAdapter(
ivCreatorChannelFantalkReplyProfile.loadProfile(reply.writerProfileImageUrl) ivCreatorChannelFantalkReplyProfile.loadProfile(reply.writerProfileImageUrl)
tvCreatorChannelFantalkReplyNickname.text = reply.writerNickname tvCreatorChannelFantalkReplyNickname.text = reply.writerNickname
tvCreatorChannelFantalkReplyTime.text = reply.createdAtText tvCreatorChannelFantalkReplyTime.text =
formatUtcRelativeTimeText(tvCreatorChannelFantalkReplyTime.context, reply.createdAtUtc)
tvCreatorChannelFantalkReplyContent.text = reply.content tvCreatorChannelFantalkReplyContent.text = reply.content
} }
@@ -33,7 +33,7 @@ internal fun bindCreatorChannelDonationCard(
} }
nicknameView.text = nickname nicknameView.text = nickname
createdAtView.text = createdAtText createdAtView.text = createdAtText
canView.text = context.getString(R.string.creator_channel_donation_can_format, can.moneyFormat()) canView.text = can.moneyFormat()
messageView.text = message messageView.text = message
if (headerColorResId == R.color.red_400) { if (headerColorResId == R.color.red_400) {
nicknameView.setTextColor(context.getColor(R.color.white)) nicknameView.setTextColor(context.getColor(R.color.white))
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -27,7 +27,7 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -88,7 +88,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:includeFontPadding="false" android:includeFontPadding="false"
android:text="/500자" android:text="/500"
android:textColor="@color/gray_500" /> android:textColor="@color/gray_500" />
</LinearLayout> </LinearLayout>
@@ -26,7 +26,7 @@
android:fontFamily="@font/medium" android:fontFamily="@font/medium"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" android:textSize="14.7sp"
tools:text="보이스온 App 오픈 관련 공지사항입니다." /> tools:text="소다라이브 App 오픈 관련 공지사항입니다." />
<TextView <TextView
android:id="@+id/tv_date" android:id="@+id/tv_date"
@@ -31,7 +31,7 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
</RelativeLayout> </RelativeLayout>
<ScrollView <ScrollView
+9 -7
View File
@@ -15,17 +15,19 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView
style="@style/Typography.Heading3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/screen_splash_tagline"
android:textColor="@color/white" />
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/splash_text_logo" /> android:src="@drawable/splash_text_logo" />
<TextView
style="@style/Typography.Heading4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_8"
android:text="@string/screen_splash_tagline"
android:textColor="@color/white" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -21,6 +21,6 @@
android:textSize="18.3sp" android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back" app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" tools:ignore="RelativeOverlap"
tools:text="보이스온" /> tools:text="소다라이브" />
</RelativeLayout> </RelativeLayout>
+1 -1
View File
@@ -222,7 +222,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:fontFamily="@font/bold" android:fontFamily="@font/bold"
android:text="@string/voiceon_original_only" android:text="@string/sodalive_original_only"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="24sp" /> android:textSize="24sp" />
+5 -3
View File
@@ -291,18 +291,20 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:layout_marginBottom="32dp" android:adjustViewBounds="true"
android:layout_marginBottom="@dimen/spacing_32"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/img_apply_creator" /> android:src="@drawable/img_apply_creator" />
<ImageView <ImageView
android:id="@+id/iv_introduce_voiceon" android:id="@+id/iv_introduce_sodalive"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:adjustViewBounds="true"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/img_introduce_voiceon" /> android:src="@drawable/img_introduce_sodalive" />
<!-- Recent Content Section --> <!-- Recent Content Section -->
<LinearLayout <LinearLayout
@@ -77,7 +77,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:textColor="@color/white" android:textColor="@color/white"
tools:text="20캔" /> tools:text="20" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
+1 -1
View File
@@ -19,7 +19,7 @@
android:fontFamily="@font/medium" android:fontFamily="@font/medium"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" android:textSize="14.7sp"
tools:text="보이스온 App 오픈 관련 공지사항입니다." /> tools:text="소다라이브 App 오픈 관련 공지사항입니다." />
<TextView <TextView
android:id="@+id/tv_date" android:id="@+id/tv_date"
+15 -1
View File
@@ -5,9 +5,21 @@
android:layout_height="52dp" android:layout_height="52dp"
android:background="@color/black" android:background="@color/black"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:paddingHorizontal="@dimen/spacing_20"
android:orientation="horizontal"> android:orientation="horizontal">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="start|center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20">
<TextView <TextView
android:id="@+id/tv_text_tab_first" android:id="@+id/tv_text_tab_first"
style="@style/Typography.Heading3" style="@style/Typography.Heading3"
@@ -40,4 +52,6 @@
android:visibility="gone" android:visibility="gone"
tools:text="팔로잉" tools:text="팔로잉"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout>
</HorizontalScrollView>
</kr.co.vividnext.sodalive.v2.widget.TextTabBarView> </kr.co.vividnext.sodalive.v2.widget.TextTabBarView>
@@ -12,7 +12,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/img_text_logo_v2" /> android:src="@drawable/img_text_logo" />
<View <View
android:id="@+id/view_title_bar_spacer" android:id="@+id/view_title_bar_spacer"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

+14 -15
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">VoiceOn</string> <string name="app_name">SodaLive</string>
<string name="picker_gallery">Gallery</string> <string name="picker_gallery">Gallery</string>
<string name="picker_camera">Camera</string> <string name="picker_camera">Camera</string>
<string name="picker_select">Select</string> <string name="picker_select">Select</string>
@@ -36,7 +36,7 @@
<!-- Chat Room UI strings (English) --> <!-- Chat Room UI strings (English) -->
<string name="chat_notice_clone">AI Clone converses based on the creator\'s information, but it may not perfectly reflect all details or match real conversations.</string> <string name="chat_notice_clone">AI Clone converses based on the creator\'s information, but it may not perfectly reflect all details or match real conversations.</string>
<string name="chat_notice_character">VoiceOn AI Character Talk allows a high degree of freedom. You can be anyone in the conversation. Talk as a character in a universe or create your own story with a new persona together with the character.\nNote: AI Character Talk is in open beta; conversations may be awkward or incomplete.</string> <string name="chat_notice_character">SodaLive AI Character Talk allows a high degree of freedom. You can be anyone in the conversation. Talk as a character in a universe or create your own story with a new persona together with the character.\nNote: AI Character Talk is in open beta; conversations may be awkward or incomplete.</string>
<string name="chat_character_type_clone">Clone</string> <string name="chat_character_type_clone">Clone</string>
<string name="chat_character_type_character">Character</string> <string name="chat_character_type_character">Character</string>
<string name="chat_freeze_status_creator">\"🧊 Freeze, everyone!\" The chat has been frozen.</string> <string name="chat_freeze_status_creator">\"🧊 Freeze, everyone!\" The chat has been frozen.</string>
@@ -121,7 +121,7 @@
<string name="hint_email">Email</string> <string name="hint_email">Email</string>
<string name="hint_password">Password</string> <string name="hint_password">Password</string>
<string name="forgot_password">Forgot your password?</string> <string name="forgot_password">Forgot your password?</string>
<string name="signup_prompt">Not a VoiceOn member yet? Sign up now.</string> <string name="signup_prompt">Not a SodaLive member yet? Sign up now.</string>
<string name="terms_of_service">Terms of Service</string> <string name="terms_of_service">Terms of Service</string>
<string name="privacy_policy">Privacy Policy</string> <string name="privacy_policy">Privacy Policy</string>
<string name="required_label">(Required)</string> <string name="required_label">(Required)</string>
@@ -172,7 +172,7 @@
<string name="famous_creator">Top creators</string> <string name="famous_creator">Top creators</string>
<string name="latest_content">New</string> <string name="latest_content">New</string>
<string name="view_all">View all</string> <string name="view_all">View all</string>
<string name="voiceon_original_only">Only on VoiceOn</string> <string name="sodalive_original_only">Only on SodaLive</string>
<string name="series_by_weekday">By weekday</string> <string name="series_by_weekday">By weekday</string>
<string name="popular_character_chat">Top character</string> <string name="popular_character_chat">Top character</string>
<string name="weekly_chart">Weekly chart</string> <string name="weekly_chart">Weekly chart</string>
@@ -211,7 +211,7 @@
<string name="creator_channel_series_progress_count">%1$s/%2$s episodes</string> <string name="creator_channel_series_progress_count">%1$s/%2$s episodes</string>
<string name="creator_channel_series_progress_percent">%1$s%%</string> <string name="creator_channel_series_progress_percent">%1$s%%</string>
<string name="auth_title">Identity verification</string> <string name="auth_title">Identity verification</string>
<string name="auth_desc">VoiceOn’s Open World Character Chat\nis available only to adults who have completed identity verification to protect minors.\nTo use the character chat service, please complete identity verification.</string> <string name="auth_desc">SodaLive’s Open World Character Chat\nis available only to adults who have completed identity verification to protect minors.\nTo use the character chat service, please complete identity verification.</string>
<string name="auth_desc_live">To protect minors,\nonly adults who have completed identity verification\ncan enter live rooms.\nPlease complete identity verification\nto enter the live room.</string> <string name="auth_desc_live">To protect minors,\nonly adults who have completed identity verification\ncan enter live rooms.\nPlease complete identity verification\nto enter the live room.</string>
<string name="auth_go">Verify now</string> <string name="auth_go">Verify now</string>
<string name="deeplink_not_found">Failed to find deep link.</string> <string name="deeplink_not_found">Failed to find deep link.</string>
@@ -295,7 +295,7 @@
<string name="screen_content_ranking_type_sales_count">Sales</string> <string name="screen_content_ranking_type_sales_count">Sales</string>
<string name="screen_content_ranking_type_comment_count">Comments</string> <string name="screen_content_ranking_type_comment_count">Comments</string>
<string name="screen_content_ranking_type_like_count">Likes</string> <string name="screen_content_ranking_type_like_count">Likes</string>
<string name="content_recommendation_section_original_series">Only on Voice On!</string> <string name="content_recommendation_section_original_series">Only on SodaLive!</string>
<string name="content_recommendation_section_latest_audio">New audio</string> <string name="content_recommendation_section_latest_audio">New audio</string>
<string name="content_recommendation_section_new_and_hot">New&amp;Hot</string> <string name="content_recommendation_section_new_and_hot">New&amp;Hot</string>
<string name="content_recommendation_section_free_audio">Free audio</string> <string name="content_recommendation_section_free_audio">Free audio</string>
@@ -324,7 +324,7 @@
<string name="home_recommendation_follow_all">Follow all</string> <string name="home_recommendation_follow_all">Follow all</string>
<string name="home_recommendation_follow_all_done">Following all</string> <string name="home_recommendation_follow_all_done">Following all</string>
<string name="home_recommendation_follow_all_confirm_title">Follow all</string> <string name="home_recommendation_follow_all_confirm_title">Follow all</string>
<string name="home_recommendation_follow_all_confirm_description">Would you like to follow all creators in this list?</string> <string name="home_recommendation_follow_all_confirm_description">Follow all recommended creators\nand get their updates!</string>
<string name="home_recommendation_follow_all_confirm_button">Follow</string> <string name="home_recommendation_follow_all_confirm_button">Follow</string>
<string name="home_recommendation_more">More</string> <string name="home_recommendation_more">More</string>
<string name="home_recommendation_collapse">Collapse</string> <string name="home_recommendation_collapse">Collapse</string>
@@ -411,7 +411,6 @@
<string name="creator_channel_home_donation_empty_title">Be the first to\nsupport this creator!</string> <string name="creator_channel_home_donation_empty_title">Be the first to\nsupport this creator!</string>
<string name="creator_channel_donation_empty_title">No support yet.\nBe the first to support this creator!</string> <string name="creator_channel_donation_empty_title">No support yet.\nBe the first to support this creator!</string>
<string name="creator_channel_donation_empty_owner_title">No donation history yet.</string> <string name="creator_channel_donation_empty_owner_title">No donation history yet.</string>
<string name="creator_channel_donation_can_format">%1$s cans</string>
<string name="creator_channel_donation_fallback_message">Supported with %1$d cans.</string> <string name="creator_channel_donation_fallback_message">Supported with %1$d cans.</string>
<string name="creator_channel_owner_fab_open">Create content</string> <string name="creator_channel_owner_fab_open">Create content</string>
<string name="creator_channel_owner_fab_community">Post community</string> <string name="creator_channel_owner_fab_community">Post community</string>
@@ -442,7 +441,7 @@
<string name="character_detail_original_link">View the original</string> <string name="character_detail_original_link">View the original</string>
<string name="character_detail_personality_title">[Personality &amp; Traits]</string> <string name="character_detail_personality_title">[Personality &amp; Traits]</string>
<string name="character_detail_chat_guide_title">⚠️ Character Chat Guidelines</string> <string name="character_detail_chat_guide_title">⚠️ Character Chat Guidelines</string>
<string name="character_detail_chat_guide_desc">VoiceOn AI Character Chat offers a high level of conversational freedom, allowing you to be whoever you want in the conversation.\nChat as a character within the world, or create your own story with the character as someone entirely new.</string> <string name="character_detail_chat_guide_desc">SodaLive AI Character Chat offers a high level of conversational freedom, allowing you to be whoever you want in the conversation.\nChat as a character within the world, or create your own story with the character as someone entirely new.</string>
<string name="character_detail_chat_guide_notice">※ Character AI Chat is in open beta.\nResponses may sometimes feel awkward or incomplete.\nIf the character feels out of character early in the conversation, please reset the chat and try again.</string> <string name="character_detail_chat_guide_notice">※ Character AI Chat is in open beta.\nResponses may sometimes feel awkward or incomplete.\nIf the character feels out of character early in the conversation, please reset the chat and try again.</string>
<string name="character_detail_comments_label">Comments</string> <string name="character_detail_comments_label">Comments</string>
<string name="character_detail_comment_input_hint">Enter a comment</string> <string name="character_detail_comment_input_hint">Enter a comment</string>
@@ -713,8 +712,8 @@
<string name="screen_live_room_unknown_error">An unknown error occurred. try again.</string> <string name="screen_live_room_unknown_error">An unknown error occurred. try again.</string>
<string name="screen_live_room_detail_date_format">yyyy.MM.dd EEE hh:mm a</string> <string name="screen_live_room_detail_date_format">yyyy.MM.dd EEE hh:mm a</string>
<string name="screen_live_room_reserver_label">Reserved users</string> <string name="screen_live_room_reserver_label">Reserved users</string>
<string name="screen_live_room_share_message_private">%1$s invited you to a private live on VoiceOn.\n\u203b Join: %2$s\n(Password: %3$s)</string> <string name="screen_live_room_share_message_private">%1$s invited you to a private live on SodaLive.\n\u203b Join: %2$s\n(Password: %3$s)</string>
<string name="screen_live_room_share_message_public">%1$s invited you to a public live on VoiceOn.\n\u203b Join: %2$s</string> <string name="screen_live_room_share_message_public">%1$s invited you to a public live on SodaLive.\n\u203b Join: %2$s</string>
<string name="screen_live_room_dialog_title_no_chat">Chat mute</string> <string name="screen_live_room_dialog_title_no_chat">Chat mute</string>
<string name="screen_live_room_dialog_no_chat_desc">Mute chat for 3 minutes?</string> <string name="screen_live_room_dialog_no_chat_desc">Mute chat for 3 minutes?</string>
<string name="screen_live_room_dialog_skip">Skip</string> <string name="screen_live_room_dialog_skip">Skip</string>
@@ -953,7 +952,7 @@
<string name="screen_can_payment_method_unified">Unified payment</string> <string name="screen_can_payment_method_unified">Unified payment</string>
<string name="screen_can_payment_method_phone">Mobile payment</string> <string name="screen_can_payment_method_phone">Mobile payment</string>
<string name="screen_can_payment_agree">I agree to the purchase terms and payment</string> <string name="screen_can_payment_agree">I agree to the purchase terms and payment</string>
<string name="screen_can_payment_notice">- Charged cans expire 5 years after charging.\n- Payments can be cancelled within 7 days; cancellations are not allowed once any can is used.\n- Promotional cans provided for free are non-refundable.\n- See the VoiceOn Terms of Service for details.</string> <string name="screen_can_payment_notice">- Charged cans expire 5 years after charging.\n- Payments can be cancelled within 7 days; cancellations are not allowed once any can is used.\n- Promotional cans provided for free are non-refundable.\n- See the SodaLive Terms of Service for details.</string>
<string name="screen_can_payment_amount_label">Payment amount</string> <string name="screen_can_payment_amount_label">Payment amount</string>
<string name="screen_can_payment_pay">Pay</string> <string name="screen_can_payment_pay">Pay</string>
<string name="screen_can_payment_currency_unit">KRW</string> <string name="screen_can_payment_currency_unit">KRW</string>
@@ -975,7 +974,7 @@
<string name="msg_can_coupon_unknown_error">An unknown error occurred. try again.</string> <string name="msg_can_coupon_unknown_error">An unknown error occurred. try again.</string>
<!-- Service Center --> <!-- Service Center -->
<string name="screen_service_center_title">Customer service</string> <string name="screen_service_center_title">Customer service</string>
<string name="screen_service_center_logo_label">VoiceOn Customer Service</string> <string name="screen_service_center_logo_label">SodaLive Customer Service</string>
<string name="screen_service_center_faq_title">FAQ</string> <string name="screen_service_center_faq_title">FAQ</string>
<string name="screen_service_center_faq_category_all">All</string> <string name="screen_service_center_faq_category_all">All</string>
<string name="screen_service_center_kakao">TALK inquiry</string> <string name="screen_service_center_kakao">TALK inquiry</string>
@@ -1130,7 +1129,7 @@
<string name="screen_audition_on">ON</string> <string name="screen_audition_on">ON</string>
<string name="screen_audition_off">OFF</string> <string name="screen_audition_off">OFF</string>
<string name="screen_audition_total_count">Total %1$d</string> <string name="screen_audition_total_count">Total %1$d</string>
<string name="screen_audition_how_to_use">How to use VoiceOn audition</string> <string name="screen_audition_how_to_use">How to use SodaLive audition</string>
<string name="screen_audition_how_to_use_more">See details&gt;</string> <string name="screen_audition_how_to_use_more">See details&gt;</string>
<string name="screen_audition_error_invalid_request">Invalid request.\ntry again.</string> <string name="screen_audition_error_invalid_request">Invalid request.\ntry again.</string>
<string name="screen_audition_detail_info_title">Audition info</string> <string name="screen_audition_detail_info_title">Audition info</string>
@@ -1149,7 +1148,7 @@
<string name="dialog_audition_apply_audio_file_title">Recording file</string> <string name="dialog_audition_apply_audio_file_title">Recording file</string>
<string name="dialog_audition_apply_phone_title">Contact</string> <string name="dialog_audition_apply_phone_title">Contact</string>
<string name="dialog_audition_apply_phone_hint">Leave a contact number for results</string> <string name="dialog_audition_apply_phone_hint">Leave a contact number for results</string>
<string name="dialog_audition_apply_agreement">I agree to the collection and use of my personal contact information for notification if I pass the VoiceOn audio drama audition.\nYou may refuse to consent, but the audition application will be canceled if you do not agree.</string> <string name="dialog_audition_apply_agreement">I agree to the collection and use of my personal contact information for notification if I pass the SodaLive audio drama audition.\nYou may refuse to consent, but the audition application will be canceled if you do not agree.</string>
<string name="dialog_audition_apply_submit">Submit audition application</string> <string name="dialog_audition_apply_submit">Submit audition application</string>
<string name="dialog_audition_apply_error_invalid_phone">Invalid contact number.\ntry again.</string> <string name="dialog_audition_apply_error_invalid_phone">Invalid contact number.\ntry again.</string>
<string name="dialog_audition_apply_error_need_agreement">You must agree to the collection and use of contact information to apply for the audition.</string> <string name="dialog_audition_apply_error_need_agreement">You must agree to the collection and use of contact information to apply for the audition.</string>
+14 -15
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">ボイスオン</string> <string name="app_name">ソーダライブ</string>
<string name="picker_gallery">ギャラリー</string> <string name="picker_gallery">ギャラリー</string>
<string name="picker_camera">カメラ</string> <string name="picker_camera">カメラ</string>
<string name="picker_select">選択</string> <string name="picker_select">選択</string>
@@ -36,7 +36,7 @@
<!-- Chat Room UI strings --> <!-- Chat Room UI strings -->
<string name="chat_notice_clone">AIクローンはクリエイターのデータを基に会話しますが、完全に再現されない場合があります。</string> <string name="chat_notice_clone">AIクローンはクリエイターのデータを基に会話しますが、完全に再現されない場合があります。</string>
<string name="chat_notice_character">ボイスオンのAIキャラトークは自由度が高く、あなたは誰にでもなれます。\n世界観のキャラになりきったり、新しい人物としてあなただけのストーリーを作ってみましょう。\n※オープンベータ中のため、会話がぎこちない場合があります。</string> <string name="chat_notice_character">ソーダライブのAIキャラトークは自由度が高く、あなたは誰にでもなれます。\n世界観のキャラになりきったり、新しい人物としてあなただけのストーリーを作ってみましょう。\n※オープンベータ中のため、会話がぎこちない場合があります。</string>
<string name="chat_character_type_clone">クローン</string> <string name="chat_character_type_clone">クローン</string>
<string name="chat_character_type_character">キャラクター</string> <string name="chat_character_type_character">キャラクター</string>
<string name="chat_freeze_status_creator">「🧊 みんなフリーズ!」チャットを凍結しました。</string> <string name="chat_freeze_status_creator">「🧊 みんなフリーズ!」チャットを凍結しました。</string>
@@ -121,7 +121,7 @@
<string name="hint_email">メールアドレス</string> <string name="hint_email">メールアドレス</string>
<string name="hint_password">パスワード</string> <string name="hint_password">パスワード</string>
<string name="forgot_password">パスワードをお忘れですか?</string> <string name="forgot_password">パスワードをお忘れですか?</string>
<string name="signup_prompt">ボイスオンは初めてですか?今すぐ登録しましょう。</string> <string name="signup_prompt">ソーダライブは初めてですか?今すぐ登録しましょう。</string>
<string name="terms_of_service">利用規約</string> <string name="terms_of_service">利用規約</string>
<string name="privacy_policy">個人情報の収集・利用への同意</string> <string name="privacy_policy">個人情報の収集・利用への同意</string>
<string name="required_label">(必須)</string> <string name="required_label">(必須)</string>
@@ -172,10 +172,10 @@
<string name="famous_creator">人気クリエイター</string> <string name="famous_creator">人気クリエイター</string>
<string name="latest_content">最新コンテンツ</string> <string name="latest_content">最新コンテンツ</string>
<string name="view_all">すべて見る</string> <string name="view_all">すべて見る</string>
<string name="voiceon_original_only">ボイスオン限定</string> <string name="sodalive_original_only">ソーダライブ限定</string>
<string name="series_by_weekday">曜日別シリーズ</string> <string name="series_by_weekday">曜日別シリーズ</string>
<string name="popular_character_chat">人気キャラチャット</string> <string name="popular_character_chat">人気キャラチャット</string>
<string name="weekly_chart">ボイスオン週間チャート</string> <string name="weekly_chart">ソーダライブ週間チャート</string>
<string name="recommend_channel">おすすめチャンネル</string> <string name="recommend_channel">おすすめチャンネル</string>
<string name="free_content">無料コンテンツ</string> <string name="free_content">無料コンテンツ</string>
<string name="point_rental_content">ポイントレンタル・コンテンツ</string> <string name="point_rental_content">ポイントレンタル・コンテンツ</string>
@@ -211,7 +211,7 @@
<string name="creator_channel_series_progress_count">%1$s/%2$s話</string> <string name="creator_channel_series_progress_count">%1$s/%2$s話</string>
<string name="creator_channel_series_progress_percent">%1$s%%</string> <string name="creator_channel_series_progress_percent">%1$s%%</string>
<string name="auth_title">本人確認</string> <string name="auth_title">本人確認</string>
<string name="auth_desc">ボイスオンのオープンワールド・キャラトークは、\n青少年保護のため本人確認済みの\n成人の方のみご利用いただけます。\nサービスを利用するには本人確認をお願いします。</string> <string name="auth_desc">ソーダライブのオープンワールド・キャラトークは、\n青少年保護のため本人確認済みの\n成人の方のみご利用いただけます。\nサービスを利用するには本人確認をお願いします。</string>
<string name="auth_desc_live">青少年保護のため、\n本人確認を完了した成人の方のみ\nライブ入場が可能です。\nライブ入場のために\n本人確認を行ってください。</string> <string name="auth_desc_live">青少年保護のため、\n本人確認を完了した成人の方のみ\nライブ入場が可能です。\nライブ入場のために\n本人確認を行ってください。</string>
<string name="auth_go">本人確認へ進む</string> <string name="auth_go">本人確認へ進む</string>
<string name="deeplink_not_found">ディープリンクが見つかりませんでした。</string> <string name="deeplink_not_found">ディープリンクが見つかりませんでした。</string>
@@ -295,7 +295,7 @@
<string name="screen_content_ranking_type_sales_count">販売数</string> <string name="screen_content_ranking_type_sales_count">販売数</string>
<string name="screen_content_ranking_type_comment_count">コメント数</string> <string name="screen_content_ranking_type_comment_count">コメント数</string>
<string name="screen_content_ranking_type_like_count">いいね</string> <string name="screen_content_ranking_type_like_count">いいね</string>
<string name="content_recommendation_section_original_series">VOICE ON限定</string> <string name="content_recommendation_section_original_series">SODALIVE限定</string>
<string name="content_recommendation_section_latest_audio">新着オーディオ</string> <string name="content_recommendation_section_latest_audio">新着オーディオ</string>
<string name="content_recommendation_section_new_and_hot">New&amp;Hot</string> <string name="content_recommendation_section_new_and_hot">New&amp;Hot</string>
<string name="content_recommendation_section_free_audio">無料オーディオ</string> <string name="content_recommendation_section_free_audio">無料オーディオ</string>
@@ -324,7 +324,7 @@
<string name="home_recommendation_follow_all">すべてフォロー</string> <string name="home_recommendation_follow_all">すべてフォロー</string>
<string name="home_recommendation_follow_all_done">すべてフォロー中</string> <string name="home_recommendation_follow_all_done">すべてフォロー中</string>
<string name="home_recommendation_follow_all_confirm_title">すべてフォロー</string> <string name="home_recommendation_follow_all_confirm_title">すべてフォロー</string>
<string name="home_recommendation_follow_all_confirm_description">このリストのクリエイターをすべてフォローしますか?</string> <string name="home_recommendation_follow_all_confirm_description">おすすめのクリエイターをすべてフォローして、\n最新情報を受け取りましょう!</string>
<string name="home_recommendation_follow_all_confirm_button">フォローする</string> <string name="home_recommendation_follow_all_confirm_button">フォローする</string>
<string name="home_recommendation_more">もっと見る</string> <string name="home_recommendation_more">もっと見る</string>
<string name="home_recommendation_collapse">折りたたむ</string> <string name="home_recommendation_collapse">折りたたむ</string>
@@ -411,7 +411,6 @@
<string name="creator_channel_home_donation_empty_title">最初のサポートをしてみましょう!</string> <string name="creator_channel_home_donation_empty_title">最初のサポートをしてみましょう!</string>
<string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string> <string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string>
<string name="creator_channel_donation_empty_owner_title">応援履歴がありません</string> <string name="creator_channel_donation_empty_owner_title">応援履歴がありません</string>
<string name="creator_channel_donation_can_format">%1$sCAN</string>
<string name="creator_channel_donation_fallback_message">%1$dCANを応援しました。</string> <string name="creator_channel_donation_fallback_message">%1$dCANを応援しました。</string>
<string name="creator_channel_owner_fab_open">コンテンツ作成</string> <string name="creator_channel_owner_fab_open">コンテンツ作成</string>
<string name="creator_channel_owner_fab_community">コミュニティ投稿</string> <string name="creator_channel_owner_fab_community">コミュニティ投稿</string>
@@ -442,7 +441,7 @@
<string name="character_detail_original_link">原作を見に行く</string> <string name="character_detail_original_link">原作を見に行く</string>
<string name="character_detail_personality_title">[性格・特徴]</string> <string name="character_detail_personality_title">[性格・特徴]</string>
<string name="character_detail_chat_guide_title">⚠️ キャラクタートークガイド</string> <string name="character_detail_chat_guide_title">⚠️ キャラクタートークガイド</string>
<string name="character_detail_chat_guide_desc">ボイスオンのAIキャラトークは自由度が高く、あなたは誰にでもなれます。\n世界観のキャラになりきったり、新しい人物としてあなただけのストーリーを作ってみましょう。</string> <string name="character_detail_chat_guide_desc">ソーダライブのAIキャラトークは自由度が高く、あなたは誰にでもなれます。\n世界観のキャラになりきったり、新しい人物としてあなただけのストーリーを作ってみましょう。</string>
<string name="character_detail_chat_guide_notice">※オープンベータ中のため、会話がぎこちない場合があります。キャラ崩壊を感じたら、リセットして再試行してください。</string> <string name="character_detail_chat_guide_notice">※オープンベータ中のため、会話がぎこちない場合があります。キャラ崩壊を感じたら、リセットして再試行してください。</string>
<string name="character_detail_comments_label">コメント</string> <string name="character_detail_comments_label">コメント</string>
<string name="character_detail_comment_input_hint">コメントを入力してください</string> <string name="character_detail_comment_input_hint">コメントを入力してください</string>
@@ -713,8 +712,8 @@
<string name="screen_live_room_unknown_error">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string> <string name="screen_live_room_unknown_error">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string>
<string name="screen_live_room_detail_date_format">yyyy年 MM月 dd日 (E) a hh:mm</string> <string name="screen_live_room_detail_date_format">yyyy年 MM月 dd日 (E) a hh:mm</string>
<string name="screen_live_room_reserver_label">予約者</string> <string name="screen_live_room_reserver_label">予約者</string>
<string name="screen_live_room_share_message_private">%1$sさんがボイスオンの非公開ライブに招待しました。\n※ ライブ参加: %2$s\n(入室パスワード: %3$s)</string> <string name="screen_live_room_share_message_private">%1$sさんがソーダライブの非公開ライブに招待しました。\n※ ライブ参加: %2$s\n(入室パスワード: %3$s)</string>
<string name="screen_live_room_share_message_public">%1$sさんがボイスオンの公開ライブに招待しました。\n※ ライブ参加: %2$s</string> <string name="screen_live_room_share_message_public">%1$sさんがソーダライブの公開ライブに招待しました。\n※ ライブ参加: %2$s</string>
<string name="screen_live_room_dialog_title_no_chat">チャット禁止</string> <string name="screen_live_room_dialog_title_no_chat">チャット禁止</string>
<string name="screen_live_room_dialog_no_chat_desc">3分間チャットを禁止しますか?</string> <string name="screen_live_room_dialog_no_chat_desc">3分間チャットを禁止しますか?</string>
<string name="screen_live_room_dialog_skip">スキップ</string> <string name="screen_live_room_dialog_skip">スキップ</string>
@@ -975,7 +974,7 @@
<string name="msg_can_coupon_unknown_error">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string> <string name="msg_can_coupon_unknown_error">不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。</string>
<!-- Service Center --> <!-- Service Center -->
<string name="screen_service_center_title">サポート</string> <string name="screen_service_center_title">サポート</string>
<string name="screen_service_center_logo_label">ボイスオンサポート</string> <string name="screen_service_center_logo_label">ソーダライブサポート</string>
<string name="screen_service_center_faq_title">よくある質問</string> <string name="screen_service_center_faq_title">よくある質問</string>
<string name="screen_service_center_faq_category_all">すべて</string> <string name="screen_service_center_faq_category_all">すべて</string>
<string name="screen_service_center_kakao">TALK問い合わせ</string> <string name="screen_service_center_kakao">TALK問い合わせ</string>
@@ -1130,7 +1129,7 @@
<string name="screen_audition_on">ON</string> <string name="screen_audition_on">ON</string>
<string name="screen_audition_off">OFF</string> <string name="screen_audition_off">OFF</string>
<string name="screen_audition_total_count">すべて %1$d個</string> <string name="screen_audition_total_count">すべて %1$d個</string>
<string name="screen_audition_how_to_use">ボイスオンオーディション利用方法</string> <string name="screen_audition_how_to_use">ソーダライブオーディション利用方法</string>
<string name="screen_audition_how_to_use_more">詳しく&gt;</string> <string name="screen_audition_how_to_use_more">詳しく&gt;</string>
<string name="screen_audition_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string> <string name="screen_audition_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
<string name="screen_audition_detail_info_title">オーディション情報</string> <string name="screen_audition_detail_info_title">オーディション情報</string>
@@ -1149,7 +1148,7 @@
<string name="dialog_audition_apply_audio_file_title">録音ファイル</string> <string name="dialog_audition_apply_audio_file_title">録音ファイル</string>
<string name="dialog_audition_apply_phone_title">連絡先</string> <string name="dialog_audition_apply_phone_title">連絡先</string>
<string name="dialog_audition_apply_phone_hint">合格時に連絡可能な連絡先を残してください</string> <string name="dialog_audition_apply_phone_hint">合格時に連絡可能な連絡先を残してください</string>
<string name="dialog_audition_apply_agreement">ボイスオンオーディオドラマオーディション合格時の連絡のため、個人情報(連絡先)の収集・利用に同意します。\n同意を拒否する権利がありますが、その場合応募はキャンセルされます。</string> <string name="dialog_audition_apply_agreement">ソーダライブオーディオドラマオーディション合格時の連絡のため、個人情報(連絡先)の収集・利用に同意します。\n同意を拒否する権利がありますが、その場合応募はキャンセルされます。</string>
<string name="dialog_audition_apply_submit">オーディションに応募する</string> <string name="dialog_audition_apply_submit">オーディションに応募する</string>
<string name="dialog_audition_apply_error_invalid_phone">無効な連絡先です。\nもう一度入力してください。</string> <string name="dialog_audition_apply_error_invalid_phone">無効な連絡先です。\nもう一度入力してください。</string>
<string name="dialog_audition_apply_error_need_agreement">連絡先の収集・利用に同意すると応募可能です。</string> <string name="dialog_audition_apply_error_need_agreement">連絡先の収集・利用に同意すると応募可能です。</string>
+15 -16
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">보이스온</string> <string name="app_name">소다라이브</string>
<string name="picker_gallery">갤러리</string> <string name="picker_gallery">갤러리</string>
<string name="picker_camera">카메라</string> <string name="picker_camera">카메라</string>
<string name="picker_select">선택</string> <string name="picker_select">선택</string>
@@ -36,7 +36,7 @@
<!-- Chat Room UI strings (tasks 2.3) --> <!-- Chat Room UI strings (tasks 2.3) -->
<string name="chat_notice_clone">AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다.</string> <string name="chat_notice_clone">AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다.</string>
<string name="chat_notice_character">보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다.</string> <string name="chat_notice_character">소다라이브 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다.</string>
<string name="chat_character_type_clone">Clone</string> <string name="chat_character_type_clone">Clone</string>
<string name="chat_character_type_character">Character</string> <string name="chat_character_type_character">Character</string>
<string name="chat_freeze_status_creator">“🧊 모두들 얼음!” 채팅창을 얼렸습니다.</string> <string name="chat_freeze_status_creator">“🧊 모두들 얼음!” 채팅창을 얼렸습니다.</string>
@@ -120,7 +120,7 @@
<string name="hint_email">이메일</string> <string name="hint_email">이메일</string>
<string name="hint_password">비밀번호</string> <string name="hint_password">비밀번호</string>
<string name="forgot_password">비밀번호를 잊으셨나요?</string> <string name="forgot_password">비밀번호를 잊으셨나요?</string>
<string name="signup_prompt">보이스온 회원이 아닌가요? 지금 가입하세요.</string> <string name="signup_prompt">소다라이브 회원이 아닌가요? 지금 가입하세요.</string>
<string name="terms_of_service">이용약관</string> <string name="terms_of_service">이용약관</string>
<string name="privacy_policy">개인정보수집 및 이용동의</string> <string name="privacy_policy">개인정보수집 및 이용동의</string>
<string name="required_label">(필수)</string> <string name="required_label">(필수)</string>
@@ -171,7 +171,7 @@
<string name="famous_creator">인기 크리에이터</string> <string name="famous_creator">인기 크리에이터</string>
<string name="latest_content">최신 콘텐츠</string> <string name="latest_content">최신 콘텐츠</string>
<string name="view_all">전체보기</string> <string name="view_all">전체보기</string>
<string name="voiceon_original_only">오직 보이스온에서만</string> <string name="sodalive_original_only">오직 소다라이브에서만</string>
<string name="series_by_weekday">요일별 시리즈</string> <string name="series_by_weekday">요일별 시리즈</string>
<string name="popular_character_chat">인기 캐릭터 채팅</string> <string name="popular_character_chat">인기 캐릭터 채팅</string>
<string name="weekly_chart">보온 주간 차트</string> <string name="weekly_chart">보온 주간 차트</string>
@@ -210,7 +210,7 @@
<string name="creator_channel_series_progress_count">%1$s/%2$s화</string> <string name="creator_channel_series_progress_count">%1$s/%2$s화</string>
<string name="creator_channel_series_progress_percent">%1$s%%</string> <string name="creator_channel_series_progress_percent">%1$s%%</string>
<string name="auth_title">본인인증</string> <string name="auth_title">본인인증</string>
<string name="auth_desc">보이스온의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.</string> <string name="auth_desc">소다라이브의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.</string>
<string name="auth_desc_live">청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n라이브 입장을 위해\n본인인증을 진행해 주세요.</string> <string name="auth_desc_live">청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n라이브 입장을 위해\n본인인증을 진행해 주세요.</string>
<string name="auth_go">본인인증 하러가기</string> <string name="auth_go">본인인증 하러가기</string>
<string name="deeplink_not_found">딥링크를 찾을 수 없습니다.</string> <string name="deeplink_not_found">딥링크를 찾을 수 없습니다.</string>
@@ -294,7 +294,7 @@
<string name="screen_content_ranking_type_sales_count">판매량</string> <string name="screen_content_ranking_type_sales_count">판매량</string>
<string name="screen_content_ranking_type_comment_count">댓글수</string> <string name="screen_content_ranking_type_comment_count">댓글수</string>
<string name="screen_content_ranking_type_like_count">좋아요</string> <string name="screen_content_ranking_type_like_count">좋아요</string>
<string name="content_recommendation_section_original_series">오직 보이스온에서만!</string> <string name="content_recommendation_section_original_series">오직 소다라이브에서만!</string>
<string name="content_recommendation_section_latest_audio">새로 올라온 오디오</string> <string name="content_recommendation_section_latest_audio">새로 올라온 오디오</string>
<string name="content_recommendation_section_new_and_hot">New&amp;Hot</string> <string name="content_recommendation_section_new_and_hot">New&amp;Hot</string>
<string name="content_recommendation_section_free_audio">무료 오디오</string> <string name="content_recommendation_section_free_audio">무료 오디오</string>
@@ -322,8 +322,8 @@
<string name="home_recommendation_activity_community">커뮤니티</string> <string name="home_recommendation_activity_community">커뮤니티</string>
<string name="home_recommendation_follow_all">모두 팔로우 하기</string> <string name="home_recommendation_follow_all">모두 팔로우 하기</string>
<string name="home_recommendation_follow_all_done">모두 팔로우 완료</string> <string name="home_recommendation_follow_all_done">모두 팔로우 완료</string>
<string name="home_recommendation_follow_all_confirm_title">모두 팔로우</string> <string name="home_recommendation_follow_all_confirm_title">모두 팔로우 하기</string>
<string name="home_recommendation_follow_all_confirm_description">목록의 크리에이터를 모두 팔로우하시겠어요?</string> <string name="home_recommendation_follow_all_confirm_description">추천 크리에이터를 모두 팔로우하고,\n소식을 받아보세요!</string>
<string name="home_recommendation_follow_all_confirm_button">팔로우 하기</string> <string name="home_recommendation_follow_all_confirm_button">팔로우 하기</string>
<string name="home_recommendation_more">더보기</string> <string name="home_recommendation_more">더보기</string>
<string name="home_recommendation_collapse">접기</string> <string name="home_recommendation_collapse">접기</string>
@@ -410,7 +410,6 @@
<string name="creator_channel_home_donation_empty_title">처음으로 크리에이터를\n후원해 보세요!</string> <string name="creator_channel_home_donation_empty_title">처음으로 크리에이터를\n후원해 보세요!</string>
<string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string> <string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string>
<string name="creator_channel_donation_empty_owner_title">후원 내역이 없습니다</string> <string name="creator_channel_donation_empty_owner_title">후원 내역이 없습니다</string>
<string name="creator_channel_donation_can_format">%1$s캔</string>
<string name="creator_channel_donation_fallback_message">%1$d캔을 후원하였습니다.</string> <string name="creator_channel_donation_fallback_message">%1$d캔을 후원하였습니다.</string>
<string name="creator_channel_owner_fab_open">콘텐츠 만들기</string> <string name="creator_channel_owner_fab_open">콘텐츠 만들기</string>
<string name="creator_channel_owner_fab_community">커뮤니티 글 올리기</string> <string name="creator_channel_owner_fab_community">커뮤니티 글 올리기</string>
@@ -441,7 +440,7 @@
<string name="character_detail_original_link">원작 보러가기</string> <string name="character_detail_original_link">원작 보러가기</string>
<string name="character_detail_personality_title">[성격 및 특징]</string> <string name="character_detail_personality_title">[성격 및 특징]</string>
<string name="character_detail_chat_guide_title">⚠️ 캐릭터톡 대화 가이드</string> <string name="character_detail_chat_guide_title">⚠️ 캐릭터톡 대화 가이드</string>
<string name="character_detail_chat_guide_desc">보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.</string> <string name="character_detail_chat_guide_desc">소다라이브 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.</string>
<string name="character_detail_chat_guide_notice">※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다. 대화 초반에 캐릭터 붕괴가 느껴진다면 대화를 리셋하고 다시 시도해보세요.</string> <string name="character_detail_chat_guide_notice">※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다. 대화 초반에 캐릭터 붕괴가 느껴진다면 대화를 리셋하고 다시 시도해보세요.</string>
<string name="character_detail_comments_label">댓글</string> <string name="character_detail_comments_label">댓글</string>
<string name="character_detail_comment_input_hint">댓글을 입력해보세요</string> <string name="character_detail_comment_input_hint">댓글을 입력해보세요</string>
@@ -712,8 +711,8 @@
<string name="screen_live_room_unknown_error">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string> <string name="screen_live_room_unknown_error">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string>
<string name="screen_live_room_detail_date_format">yyyy년 MM월 dd일 (E) a hh시 mm분</string> <string name="screen_live_room_detail_date_format">yyyy년 MM월 dd일 (E) a hh시 mm분</string>
<string name="screen_live_room_reserver_label">예약자</string> <string name="screen_live_room_reserver_label">예약자</string>
<string name="screen_live_room_share_message_private">%1$s님이 귀하를 보이스온의 비공개라이브에 초대하였습니다.\n※ 라이브 참여: %2$s\n(입장 비밀번호 : %3$s)</string> <string name="screen_live_room_share_message_private">%1$s님이 귀하를 소다라이브의 비공개라이브에 초대하였습니다.\n※ 라이브 참여: %2$s\n(입장 비밀번호 : %3$s)</string>
<string name="screen_live_room_share_message_public">%1$s님이 귀하를 보이스온의 공개라이브에 초대하였습니다.\n※ 라이브 참여: %2$s</string> <string name="screen_live_room_share_message_public">%1$s님이 귀하를 소다라이브의 공개라이브에 초대하였습니다.\n※ 라이브 참여: %2$s</string>
<string name="screen_live_room_dialog_title_no_chat">채팅금지</string> <string name="screen_live_room_dialog_title_no_chat">채팅금지</string>
<string name="screen_live_room_dialog_no_chat_desc">3분간 채팅금지를 하겠습니까?</string> <string name="screen_live_room_dialog_no_chat_desc">3분간 채팅금지를 하겠습니까?</string>
<string name="screen_live_room_dialog_skip">건너뛰기</string> <string name="screen_live_room_dialog_skip">건너뛰기</string>
@@ -952,7 +951,7 @@
<string name="screen_can_payment_method_unified">통합 결제</string> <string name="screen_can_payment_method_unified">통합 결제</string>
<string name="screen_can_payment_method_phone">휴대폰 결제</string> <string name="screen_can_payment_method_phone">휴대폰 결제</string>
<string name="screen_can_payment_agree">구매조건 확인 및 결제 진행 동의</string> <string name="screen_can_payment_agree">구매조건 확인 및 결제 진행 동의</string>
<string name="screen_can_payment_notice">- 충전된 캔의 유효기간은 충전 후 5년 입니다.\n- 결제 취소는 결제 후 7일 이내에만 할 수 있습니다.\n 단, 캔의 일부를 사용하면 결제 취소할 수 없습니다.\n- 광고성 이벤트 등 회사가 무료로 지급한 \n포인트는 환불되지 않습니다.\n- 자세한 내용은 보이스온 이용약관에서 확인할 수 있습니다.</string> <string name="screen_can_payment_notice">- 충전된 캔의 유효기간은 충전 후 5년 입니다.\n- 결제 취소는 결제 후 7일 이내에만 할 수 있습니다.\n 단, 캔의 일부를 사용하면 결제 취소할 수 없습니다.\n- 광고성 이벤트 등 회사가 무료로 지급한 \n포인트는 환불되지 않습니다.\n- 자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.</string>
<string name="screen_can_payment_amount_label">결제금액</string> <string name="screen_can_payment_amount_label">결제금액</string>
<string name="screen_can_payment_pay">결제하기</string> <string name="screen_can_payment_pay">결제하기</string>
<string name="screen_can_payment_currency_unit">원</string> <string name="screen_can_payment_currency_unit">원</string>
@@ -974,7 +973,7 @@
<string name="msg_can_coupon_unknown_error">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string> <string name="msg_can_coupon_unknown_error">알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.</string>
<!-- Service Center --> <!-- Service Center -->
<string name="screen_service_center_title">고객센터</string> <string name="screen_service_center_title">고객센터</string>
<string name="screen_service_center_logo_label">보이스온 고객센터</string> <string name="screen_service_center_logo_label">소다라이브 고객센터</string>
<string name="screen_service_center_faq_title">자주 묻는 질문</string> <string name="screen_service_center_faq_title">자주 묻는 질문</string>
<string name="screen_service_center_faq_category_all">전체</string> <string name="screen_service_center_faq_category_all">전체</string>
<string name="screen_service_center_kakao">TALK 문의</string> <string name="screen_service_center_kakao">TALK 문의</string>
@@ -1129,7 +1128,7 @@
<string name="screen_audition_on">ON</string> <string name="screen_audition_on">ON</string>
<string name="screen_audition_off">OFF</string> <string name="screen_audition_off">OFF</string>
<string name="screen_audition_total_count">전체 %1$d개</string> <string name="screen_audition_total_count">전체 %1$d개</string>
<string name="screen_audition_how_to_use">보이스온 오디션 이용방법</string> <string name="screen_audition_how_to_use">소다라이브 오디션 이용방법</string>
<string name="screen_audition_how_to_use_more">자세히&gt;</string> <string name="screen_audition_how_to_use_more">자세히&gt;</string>
<string name="screen_audition_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string> <string name="screen_audition_error_invalid_request">잘못된 요청입니다.\n다시 시도해 주세요.</string>
<string name="screen_audition_detail_info_title">오디션 정보</string> <string name="screen_audition_detail_info_title">오디션 정보</string>
@@ -1148,7 +1147,7 @@
<string name="dialog_audition_apply_audio_file_title">녹음파일</string> <string name="dialog_audition_apply_audio_file_title">녹음파일</string>
<string name="dialog_audition_apply_phone_title">연락처</string> <string name="dialog_audition_apply_phone_title">연락처</string>
<string name="dialog_audition_apply_phone_hint">합격 시 연락받을 연락처를 남겨주세요</string> <string name="dialog_audition_apply_phone_hint">합격 시 연락받을 연락처를 남겨주세요</string>
<string name="dialog_audition_apply_agreement">보이스온 오디오 드라마 오디션 합격 시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의 시 오디션 지원은 취소됩니다.</string> <string name="dialog_audition_apply_agreement">소다라이브 오디오 드라마 오디션 합격 시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의 시 오디션 지원은 취소됩니다.</string>
<string name="dialog_audition_apply_submit">오디션 지원하기</string> <string name="dialog_audition_apply_submit">오디션 지원하기</string>
<string name="dialog_audition_apply_error_invalid_phone">잘못된 연락처 입니다.\n다시 입력해 주세요.</string> <string name="dialog_audition_apply_error_invalid_phone">잘못된 연락처 입니다.\n다시 입력해 주세요.</string>
<string name="dialog_audition_apply_error_need_agreement">연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.</string> <string name="dialog_audition_apply_error_need_agreement">연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.</string>
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.common
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@@ -57,6 +58,21 @@ class RelativeTimeFormatterTest {
assertEquals(context.getString(R.string.character_comment_time_just_now), text) assertEquals(context.getString(R.string.character_comment_time_just_now), text)
} }
@Test
fun `epoch millis는 한국어 영어 일본어 context에서 locale별 경과 분 resource를 반환한다`() {
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val localizedContext = context.createConfigurationContext(
Configuration(context.resources.configuration).apply { setLocale(locale) }
)
val twoMinutesAgoMillis = System.currentTimeMillis() - 2 * MINUTE
assertEquals(
localizedContext.getString(R.string.character_comment_time_minutes, 2),
formatUtcRelativeTimeText(localizedContext, twoMinutesAgoMillis.toString())
)
}
}
private fun utcIso(millis: Long): String { private fun utcIso(millis: Long): String {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
@@ -13,7 +13,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailUiState import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailUiState
@@ -58,7 +57,7 @@ class CreatorChannelCommunityDetailViewModelTest {
SharedPreferenceManager.userId = 10L SharedPreferenceManager.userId = 10L
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
legacyRepository = org.mockito.kotlin.mock() legacyRepository = org.mockito.kotlin.mock()
viewModel = CreatorChannelCommunityDetailViewModel(repository, legacyRepository, testFormatter) viewModel = CreatorChannelCommunityDetailViewModel(repository, legacyRepository)
} }
@After @After
@@ -84,6 +83,7 @@ class CreatorChannelCommunityDetailViewModelTest {
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
assertEquals(POST_ID, state.post.postId) assertEquals(POST_ID, state.post.postId)
assertEquals("2026-07-08T00:00:00Z", state.post.createdAtUtc)
assertEquals("body", state.post.content) assertEquals("body", state.post.content)
assertEquals("image.png", state.post.imageUrl) assertEquals("image.png", state.post.imageUrl)
assertEquals("audio.mp3", state.post.audioUrl) assertEquals("audio.mp3", state.post.audioUrl)
@@ -729,6 +729,5 @@ class CreatorChannelCommunityDetailViewModelTest {
const val POST_ID = 200L const val POST_ID = 200L
const val COMMENT_ID = 11L const val COMMENT_ID = 11L
const val AUTH_TOKEN = "Bearer test-token" const val AUTH_TOKEN = "Bearer test-token"
val testFormatter = UtcRelativeTimeTextFormatter { "방금 전" }
} }
} }
@@ -1,10 +1,6 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community package kr.co.vividnext.sodalive.v2.creator.channel.community
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityImageMode import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityImageMode
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
@@ -14,17 +10,9 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelCommunityMapperTest { class CreatorChannelCommunityMapperTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val relativeTimeTextFormatter = AndroidUtcRelativeTimeTextFormatter(context)
@Test @Test
fun `보기 방식은 label과 icon resource를 가진다`() { fun `보기 방식은 label과 icon resource를 가진다`() {
assertEquals(R.string.creator_channel_community_view_mode_list, CreatorChannelCommunityViewMode.List.labelResId) assertEquals(R.string.creator_channel_community_view_mode_list, CreatorChannelCommunityViewMode.List.labelResId)
@@ -34,21 +22,22 @@ class CreatorChannelCommunityMapperTest {
} }
@Test @Test
fun `게시글 기본 필드와 상대 시간 notice 댓글 표시 상태를 매핑한다`() { fun `게시글 기본 필드와 raw UTC notice 댓글 표시 상태를 매핑한다`() {
val createdAtUtc = "2026-06-21T00:00:00Z"
val item = listOf( val item = listOf(
communityPost( communityPost(
creatorProfileUrl = "profile.png", creatorProfileUrl = "profile.png",
createdAtUtc = System.currentTimeMillis().toString(), createdAtUtc = createdAtUtc,
isPinned = true, isPinned = true,
isCommentAvailable = true isCommentAvailable = true
) )
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() ).toCommunityPostUiModels(isOwner = false, currentUserId = 99L).single()
assertEquals(1L, item.postId) assertEquals(1L, item.postId)
assertEquals(10L, item.creatorId) assertEquals(10L, item.creatorId)
assertEquals("creator", item.creatorNickname) assertEquals("creator", item.creatorNickname)
assertEquals("profile.png", item.creatorProfileUrl) assertEquals("profile.png", item.creatorProfileUrl)
assertEquals(context.getString(R.string.character_comment_time_just_now), item.createdAtText) assertEquals(createdAtUtc, item.createdAtUtc)
assertEquals("hello community", item.content) assertEquals("hello community", item.content)
assertEquals(3, item.likeCount) assertEquals(3, item.likeCount)
assertEquals(4, item.commentCount) assertEquals(4, item.commentCount)
@@ -59,7 +48,7 @@ class CreatorChannelCommunityMapperTest {
@Test @Test
fun `댓글 불가 게시글은 댓글 표시 상태가 false다`() { fun `댓글 불가 게시글은 댓글 표시 상태가 false다`() {
val item = listOf(communityPost(isCommentAvailable = false)) val item = listOf(communityPost(isCommentAvailable = false))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toCommunityPostUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertFalse(item.showComment) assertFalse(item.showComment)
@@ -69,7 +58,7 @@ class CreatorChannelCommunityMapperTest {
fun `유료 미구매 타인 게시글은 잠금 상태이고 이미지를 숨긴다`() { fun `유료 미구매 타인 게시글은 잠금 상태이고 이미지를 숨긴다`() {
val item = listOf( val item = listOf(
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3") communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3")
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() ).toCommunityPostUiModels(isOwner = false, currentUserId = 99L).single()
assertTrue(item.isLocked) assertTrue(item.isLocked)
assertNull(item.imageUrl) assertNull(item.imageUrl)
@@ -84,7 +73,7 @@ class CreatorChannelCommunityMapperTest {
existOrdered = false, existOrdered = false,
isCommentAvailable = true isCommentAvailable = true
) )
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() ).toCommunityPostUiModels(isOwner = false, currentUserId = 99L).single()
assertTrue(item.isLocked) assertTrue(item.isLocked)
assertFalse(item.showReaction) assertFalse(item.showReaction)
@@ -94,10 +83,10 @@ class CreatorChannelCommunityMapperTest {
@Test @Test
fun `고정 게시글 여부는 UI 모델에 보존한다`() { fun `고정 게시글 여부는 UI 모델에 보존한다`() {
val pinnedItem = listOf(communityPost(isPinned = true)) val pinnedItem = listOf(communityPost(isPinned = true))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
val normalItem = listOf(communityPost(isPinned = false)) val normalItem = listOf(communityPost(isPinned = false))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
assertTrue(pinnedItem.isPinned) assertTrue(pinnedItem.isPinned)
@@ -109,16 +98,16 @@ class CreatorChannelCommunityMapperTest {
@Test @Test
fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() { fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() {
val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true)) val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .toCommunityPostUiModels(isOwner = false, currentUserId = 10L)
.single() .single()
val visitorFree = listOf(communityPost(price = 0, existOrdered = false)) val visitorFree = listOf(communityPost(price = 0, existOrdered = false))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toCommunityPostUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
val visitorPaidOrdered = listOf(communityPost(price = 100, existOrdered = true)) val visitorPaidOrdered = listOf(communityPost(price = 100, existOrdered = true))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toCommunityPostUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
val visitorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false)) val visitorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toCommunityPostUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertTrue(authorPaidNotOrdered.menuItem.isOwner) assertTrue(authorPaidNotOrdered.menuItem.isOwner)
@@ -134,16 +123,16 @@ class CreatorChannelCommunityMapperTest {
@Test @Test
fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() { fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() {
val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L)) val ownerPaid = listOf(communityPost(price = 100, creatorId = 10L))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
val ownerFree = listOf(communityPost(price = 0, creatorId = 10L)) val ownerFree = listOf(communityPost(price = 0, creatorId = 10L))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
val otherCreator = listOf(communityPost(price = 100, creatorId = 11L)) val otherCreator = listOf(communityPost(price = 100, creatorId = 11L))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toCommunityPostUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
val otherChannel = listOf(communityPost(price = 100, creatorId = 10L)) val otherChannel = listOf(communityPost(price = 100, creatorId = 10L))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .toCommunityPostUiModels(isOwner = false, currentUserId = 10L)
.single() .single()
assertTrue(ownerPaid.showOwnerTopPrice) assertTrue(ownerPaid.showOwnerTopPrice)
@@ -155,7 +144,7 @@ class CreatorChannelCommunityMapperTest {
@Test @Test
fun `grid text-only preview는 줄바꿈을 공백으로 바꾸고 trim 후 24자까지만 사용한다`() { fun `grid text-only preview는 줄바꿈을 공백으로 바꾸고 trim 후 24자까지만 사용한다`() {
val item = listOf(communityPost(content = "\n123456789012345678901234567890\n")) val item = listOf(communityPost(content = "\n123456789012345678901234567890\n"))
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toCommunityPostUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertEquals("123456789012345678901234", item.gridPreviewText) assertEquals("123456789012345678901234", item.gridPreviewText)
@@ -12,7 +12,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.SingleSubject import io.reactivex.rxjava3.subjects.SingleSubject
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
@@ -45,7 +44,7 @@ class CreatorChannelCommunityPaginationTest {
SharedPreferenceManager.init(context) SharedPreferenceManager.init(context)
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelCommunityViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelCommunityViewModel(repository)
} }
@After @After
@@ -0,0 +1,179 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.ui.CreatorChannelCommunityReplyAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityImageMode
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityListAdapter
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.io.File
import java.util.Locale
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelCommunityRelativeTimeLocaleTest {
@Before
fun setUp() {
if (!ImageLoaderProvider.isInitialized) {
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
}
}
@Test
fun `영어와 일본어 목록 댓글 답글 bind는 raw UTC를 현재 View locale 상대 시간으로 표시한다`() {
listOf(Locale.ENGLISH, Locale.JAPANESE).forEach(::assertAdapterRelativeTime)
}
@Test
fun `영어와 일본어 상세와 답글 화면은 raw UTC를 현재 View locale 상대 시간으로 표시한다`() {
val detailSource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
"CreatorChannelCommunityDetailActivity.kt"
).readText()
val replySource = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/" +
"CreatorChannelCommunityReplyActivity.kt"
).readText()
assertTrue(
Regex(
"tvCreatorChannelCommunityDetailTime\\.text\\s*=\\s*" +
"formatUtcRelativeTimeText\\(tvCreatorChannelCommunityDetailTime\\.context, post\\.createdAtUtc\\)"
).containsMatchIn(detailSource)
)
assertTrue(
Regex(
"layoutCreatorChannelCommunityReplyParent\\.tvCreatorChannelCommunityCommentTime\\.text\\s*=\\s*" +
"formatUtcRelativeTimeText\\(\\s*" +
"layoutCreatorChannelCommunityReplyParent\\.tvCreatorChannelCommunityCommentTime\\.context,\\s*" +
"content\\.parentComment\\.createdAtUtc\\s*\\)"
).containsMatchIn(replySource)
)
listOf(Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val context = localizedContext(locale)
assertEquals(
context.getString(R.string.character_comment_time_just_now),
formatUtcRelativeTimeText(context, System.currentTimeMillis().toString())
)
}
}
private fun assertAdapterRelativeTime(locale: Locale) {
val context = localizedContext(locale)
val nowUtc = System.currentTimeMillis().toString()
val expected = context.getString(R.string.character_comment_time_just_now)
val listAdapter = CreatorChannelCommunityListAdapter()
val listHolder = listAdapter.onCreateViewHolder(FrameLayout(context), 0)
listAdapter.submitItems(listOf(post(nowUtc)))
listAdapter.onBindViewHolder(listHolder, 0)
assertEquals(
expected,
listHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_community_list_time).text.toString()
)
val commentAdapter = CreatorChannelCommunityCommentAdapter(onReplyClick = {})
val commentHolder = commentAdapter.onCreateViewHolder(FrameLayout(context), 0)
commentAdapter.submitItems(listOf(comment(nowUtc)))
commentAdapter.onBindViewHolder(commentHolder, 0)
assertEquals(
expected,
commentHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_community_comment_time).text.toString()
)
val replyAdapter = CreatorChannelCommunityReplyAdapter(onModifyClick = {}, onDeleteClick = {})
val replyHolder = replyAdapter.onCreateViewHolder(FrameLayout(context), 0)
replyAdapter.submitItems(listOf(reply(nowUtc)))
replyAdapter.onBindViewHolder(replyHolder, 0)
assertEquals(
expected,
replyHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_community_reply_item_time).text.toString()
)
}
private fun post(createdAtUtc: String) = CreatorChannelCommunityPostUiModel(
postId = 1L,
creatorId = 2L,
creatorNickname = "creator",
creatorProfileUrl = "",
createdAtUtc = createdAtUtc,
content = "post",
imageUrl = null,
price = 0,
existOrdered = true,
likeCount = 0,
commentCount = 0,
showReaction = true,
showComment = true,
showNotice = false,
isPinned = false,
isLocked = false,
showOwnerTopPrice = false,
gridPreviewText = "post",
imageMode = CreatorChannelCommunityImageMode.TextPreview,
menuItem = CreatorChannelCommunityMenuItem(1L, 2L, 3L, 0, true, false)
)
private fun comment(createdAtUtc: String) = CreatorChannelCommunityCommentUiModel(
commentId = 1L,
memberId = 2L,
memberNickname = "member",
memberProfileUrl = "",
createdAtUtc = createdAtUtc,
comment = "comment",
likeCount = 0,
replyCount = 0,
isLiked = false,
isMine = false,
isCreatorOwner = false,
latestReply = null
)
private fun reply(createdAtUtc: String) = CreatorChannelCommunityReplyUiModel(
replyId = 1L,
commentId = 2L,
memberId = 3L,
memberNickname = "member",
memberProfileUrl = "",
createdAtUtc = createdAtUtc,
comment = "reply",
likeCount = 0,
isLiked = false,
isMine = false,
isCreatorOwner = false
)
private fun localizedContext(locale: Locale): Context {
val context = ApplicationProvider.getApplicationContext<Context>()
val configuration = Configuration(context.resources.configuration)
configuration.setLocale(locale)
return context.createConfigurationContext(configuration)
}
private fun projectFile(path: String): File = File(projectRoot(), path)
private fun projectRoot(): File {
return generateSequence(File(System.getProperty("user.dir") ?: ".").absoluteFile) { it.parentFile }
.first { File(it, "settings.gradle").exists() }
}
}
@@ -14,7 +14,6 @@ import io.reactivex.rxjava3.subjects.PublishSubject
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyUiState import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyUiState
@@ -46,7 +45,6 @@ class CreatorChannelCommunityReplyViewModelTest {
private lateinit var repository: CreatorChannelRepository private lateinit var repository: CreatorChannelRepository
private lateinit var legacyRepository: CreatorCommunityRepository private lateinit var legacyRepository: CreatorCommunityRepository
private lateinit var viewModel: CreatorChannelCommunityReplyViewModel private lateinit var viewModel: CreatorChannelCommunityReplyViewModel
private lateinit var formattedInputs: MutableList<String?>
@Before @Before
fun setUp() { fun setUp() {
@@ -57,15 +55,7 @@ class CreatorChannelCommunityReplyViewModelTest {
SharedPreferenceManager.userId = 10L SharedPreferenceManager.userId = 10L
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
legacyRepository = org.mockito.kotlin.mock() legacyRepository = org.mockito.kotlin.mock()
formattedInputs = mutableListOf() viewModel = CreatorChannelCommunityReplyViewModel(repository, legacyRepository)
viewModel = CreatorChannelCommunityReplyViewModel(
repository,
legacyRepository,
UtcRelativeTimeTextFormatter { value ->
formattedInputs.add(value)
"relative:$value"
}
)
} }
@After @After
@@ -84,9 +74,9 @@ class CreatorChannelCommunityReplyViewModelTest {
val state = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content val state = viewModel.replyStateLiveData.requireValue() as CreatorChannelCommunityReplyUiState.Content
assertEquals(COMMENT_ID, state.parentComment.commentId) assertEquals(COMMENT_ID, state.parentComment.commentId)
assertEquals("부모 댓글", state.parentComment.comment) assertEquals("부모 댓글", state.parentComment.comment)
assertEquals("relative:2026-07-08T00:00:00Z", state.parentComment.createdAtText) assertEquals("2026-07-08T00:00:00Z", state.parentComment.createdAtUtc)
assertEquals(listOf(101L, 102L), state.replies.map { it.replyId }) assertEquals(listOf(101L, 102L), state.replies.map { it.replyId })
assertTrue(formattedInputs.contains("2026-07-08T00:00:00Z")) assertEquals(listOf("2026-07-08T00:00:00Z", "2026-07-08T00:00:00Z"), state.replies.map { it.createdAtUtc })
assertEquals(0, state.replyPage) assertEquals(0, state.replyPage)
assertTrue(state.hasNextReply) assertTrue(state.hasNextReply)
verify(repository).getCommunityCommentReplies( verify(repository).getCommunityCommentReplies(
@@ -499,7 +489,6 @@ class CreatorChannelCommunityReplyViewModelTest {
memberId = 10L, memberId = 10L,
memberNickname = "member", memberNickname = "member",
memberProfileUrl = "member.png", memberProfileUrl = "member.png",
createdAtText = "이미 포맷된 시간",
createdAtUtc = "2026-07-08T00:00:00Z", createdAtUtc = "2026-07-08T00:00:00Z",
comment = "부모 댓글", comment = "부모 댓글",
likeCount = 1, likeCount = 1,
@@ -11,7 +11,6 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityPostResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
@@ -45,7 +44,7 @@ class CreatorChannelCommunityViewModelTest {
SharedPreferenceManager.init(context) SharedPreferenceManager.init(context)
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelCommunityViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelCommunityViewModel(repository)
} }
@After @After
@@ -66,6 +65,7 @@ class CreatorChannelCommunityViewModelTest {
assertEquals(0, state.page) assertEquals(0, state.page)
assertEquals(CreatorChannelCommunityViewModel.DEFAULT_PAGE_SIZE, state.size) assertEquals(CreatorChannelCommunityViewModel.DEFAULT_PAGE_SIZE, state.size)
assertEquals(listOf(1L), state.communityPosts.map { it.postId }) assertEquals(listOf(1L), state.communityPosts.map { it.postId })
assertEquals("2026-06-21T00:00:00Z", state.communityPosts.single().createdAtUtc)
verifyGetCommunity() verifyGetCommunity()
} }
@@ -94,8 +94,8 @@ class CreatorChannelDonationActionTest {
assertTrue(adapter.contains("if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null")) assertTrue(adapter.contains("if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null"))
assertTrue(adapter.contains("bindCreatorChannelDonationCard(")) assertTrue(adapter.contains("bindCreatorChannelDonationCard("))
assertTrue(adapter.contains("headerColorResId = item.headerColorResId")) assertTrue(adapter.contains("headerColorResId = item.headerColorResId"))
assertTrue(adapter.contains("createdAtText = item.createdAtText")) assertTrue(adapter.contains("createdAtText = formatUtcRelativeTimeText(context, item.createdAtUtc)"))
assertTrue(adapter.contains("message = item.message")) assertTrue(adapter.contains("R.string.creator_channel_donation_fallback_message"))
assertTrue(adapter.contains("GridLayoutManager(itemView.context, 4)")) assertTrue(adapter.contains("GridLayoutManager(itemView.context, 4)"))
assertTrue(rankingAdapter.contains("item.profileImageUrl")) assertTrue(rankingAdapter.contains("item.profileImageUrl"))
assertTrue(rankingAdapter.contains("item.rank.toString()")) assertTrue(rankingAdapter.contains("item.rank.toString()"))
@@ -1,14 +1,21 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation package kr.co.vividnext.sodalive.v2.creator.channel.donation
import android.app.Application import android.app.Application
import android.content.res.Configuration
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.donation.ui.CreatorChannelDonationAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.ui.bindCreatorChannelDonationCard
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -17,6 +24,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import java.io.File import java.io.File
import java.util.Locale
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class) @Config(sdk = [28], application = Application::class)
@@ -167,6 +175,63 @@ class CreatorChannelDonationFragmentLayoutTest {
assertTrue(donationCardBinder.contains("createdAtView.setTextColor(context.getColor(R.color.gray_700))")) assertTrue(donationCardBinder.contains("createdAtView.setTextColor(context.getColor(R.color.gray_700))"))
} }
@Test
fun `후원 can badge는 한국어 영어 일본어에서 단위 없이 포맷된 숫자만 표시한다`() {
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE)
.forEach { locale ->
val application = ApplicationProvider.getApplicationContext<Application>()
if (!ImageLoaderProvider.isInitialized) ImageLoaderProvider.init(application)
val context = application.createConfigurationContext(
Configuration(application.resources.configuration).apply { setLocale(locale) }
)
val canView = TextView(context)
bindCreatorChannelDonationCard(
View(context), ImageView(context), TextView(context), TextView(context), canView,
TextView(context), R.color.gray_200, "", "member", "time", 1000, "message"
)
assertEquals("1,000", canView.text.toString())
}
}
@Test
fun `후원 adapter는 item view locale로 기본 문구와 상대 시간을 표시한다`() {
val application = ApplicationProvider.getApplicationContext<Application>()
if (!ImageLoaderProvider.isInitialized) ImageLoaderProvider.init(application)
val twoMinutesAgoMillis = System.currentTimeMillis() - 2 * MINUTE
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val context = application.createConfigurationContext(
Configuration(application.resources.configuration).apply { setLocale(locale) }
)
val adapter = CreatorChannelDonationAdapter()
adapter.submitItems(
rankings = emptyList(),
donations = listOf(
CreatorChannelDonationUiModel(
nickname = "member",
profileImageUrl = "",
can = 50,
message = "",
createdAtUtc = twoMinutesAgoMillis.toString(),
headerColorResId = R.color.gray_200
)
)
)
val viewHolder = adapter.onCreateViewHolder(FrameLayout(context), adapter.getItemViewType(0))
adapter.onBindViewHolder(viewHolder, 0)
assertEquals(
context.getString(R.string.creator_channel_donation_fallback_message, 50),
viewHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_donation_message).text.toString()
)
assertEquals(
context.getString(R.string.character_comment_time_minutes, 2),
viewHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_donation_created_at).text.toString()
)
}
}
@Test @Test
fun `후원 문자열은 한국어 영어 일본어에 추가된다`() { fun `후원 문자열은 한국어 영어 일본어에 추가된다`() {
val ko = projectFile("app/src/main/res/values/strings.xml").readText() val ko = projectFile("app/src/main/res/values/strings.xml").readText()
@@ -194,4 +259,8 @@ class CreatorChannelDonationFragmentLayoutTest {
return candidates.firstOrNull { it.exists() } return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath") ?: error("Project file not found: $relativePath")
} }
private companion object {
const val MINUTE = 60_000L
}
} }
@@ -1,27 +1,15 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation package kr.co.vividnext.sodalive.v2.creator.channel.donation
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.MemberDonationRankingResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationRankingUiModels import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationRankingUiModels
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.toDonationUiModels
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelDonationMapperTest { class CreatorChannelDonationMapperTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val relativeTimeTextFormatter = AndroidUtcRelativeTimeTextFormatter(context)
@Test @Test
fun `ranking rank는 응답 순서 기준 1부터 시작하고 profileImage를 profileImageUrl로 매핑한다`() { fun `ranking rank는 응답 순서 기준 1부터 시작하고 profileImage를 profileImageUrl로 매핑한다`() {
val rankings = listOf( val rankings = listOf(
@@ -35,28 +23,40 @@ class CreatorChannelDonationMapperTest {
} }
@Test @Test
fun `후원 message가 blank이면 can을 포함한 fallback 문구를 사용한다`() { fun `후원 mapper는 message 원문을 보존한다`() {
val item = listOf(donation(can = 50, message = "")) val item = listOf(donation(can = 50, message = ""))
.toDonationUiModels(context, relativeTimeTextFormatter) .toDonationUiModels()
.single() .single()
assertEquals(context.getString(R.string.creator_channel_donation_fallback_message, 50), item.message) assertEquals("", item.message)
} }
@Test @Test
fun `후원 createdAtUtc는 상대 시간으로 매핑하고 can 기준 header color를 계산한다`() { fun `message는 API 원문을 유지한다`() {
val item = listOf(donation(can = 501, createdAtUtc = System.currentTimeMillis().toString())) val items = listOf(
.toDonationUiModels(context, relativeTimeTextFormatter) donation(can = 50, message = ""),
donation(can = 50, message = "응원합니다")
).toDonationUiModels()
assertEquals("", items[0].message)
assertEquals("응원합니다", items[1].message)
}
@Test
fun `후원 mapper는 createdAtUtc 원문을 보존하고 can 기준 header color를 계산한다`() {
val createdAtUtc = System.currentTimeMillis().toString()
val item = listOf(donation(can = 501, createdAtUtc = createdAtUtc))
.toDonationUiModels()
.single() .single()
assertEquals(context.getString(R.string.character_comment_time_just_now), item.createdAtText) assertEquals(createdAtUtc, item.createdAtUtc)
assertEquals(R.color.red_400, item.headerColorResId) assertEquals(R.color.red_400, item.headerColorResId)
} }
@Test @Test
fun `후원 header color는 501캔부터 red_400으로 매핑한다`() { fun `후원 header color는 501캔부터 red_400으로 매핑한다`() {
val items = listOf(donation(can = 500), donation(can = 501)) val items = listOf(donation(can = 500), donation(can = 501))
.toDonationUiModels(context, relativeTimeTextFormatter) .toDonationUiModels()
assertEquals(R.color.creator_channel_donation_cyan, items[0].headerColorResId) assertEquals(R.color.creator_channel_donation_cyan, items[0].headerColorResId)
assertEquals(R.color.red_400, items[1].headerColorResId) assertEquals(R.color.red_400, items[1].headerColorResId)
@@ -12,7 +12,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.SingleSubject import io.reactivex.rxjava3.subjects.SingleSubject
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
@@ -48,7 +47,7 @@ class CreatorChannelDonationPaginationTest {
SodaLiveApplicationHolder.init(context as Application) SodaLiveApplicationHolder.init(context as Application)
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelDonationViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelDonationViewModel(repository)
} }
@After @After
@@ -11,7 +11,6 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
@@ -48,7 +47,7 @@ class CreatorChannelDonationViewModelTest {
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
SharedPreferenceManager.can = 200 SharedPreferenceManager.can = 200
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelDonationViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelDonationViewModel(repository)
} }
@After @After
@@ -13,7 +13,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
@@ -47,7 +46,7 @@ class CreatorChannelFanTalkActionTest {
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
SharedPreferenceManager.userId = 10L SharedPreferenceManager.userId = 10L
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelFanTalkViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelFanTalkViewModel(repository)
} }
@After @After
@@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkRightAction
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.ui.CreatorChannelFanTalkAdapter
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.Locale
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelFanTalkAdapterLocaleTest {
@Before
fun setUp() {
if (!ImageLoaderProvider.isInitialized) {
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
}
}
@Test
fun `영어와 일본어 bind는 오염된 신고 문구를 현재 locale 문자열로 복원한다`() {
assertReportText(Locale.ENGLISH, "Report")
assertReportText(Locale.JAPANESE, "通報")
}
@Test
fun `영어와 일본어 목록 bind는 원글과 답글 raw UTC를 현재 View locale 상대 시간으로 표시한다`() {
assertRelativeTimeText(Locale.ENGLISH)
assertRelativeTimeText(Locale.JAPANESE)
}
private fun assertRelativeTimeText(locale: Locale) {
val context = localizedContext(locale)
val parent = FrameLayout(context)
val adapter = CreatorChannelFanTalkAdapter()
val holder = adapter.onCreateViewHolder(parent, 0)
val nowUtc = System.currentTimeMillis().toString()
adapter.submitItems(listOf(fanTalk(nowUtc)))
adapter.onBindViewHolder(holder, 0)
val expected = context.getString(R.string.character_comment_time_just_now)
assertEquals(
expected,
holder.itemView.findViewById<TextView>(R.id.tv_creator_channel_fantalk_time).text.toString()
)
assertEquals(
expected,
holder.itemView.findViewById<TextView>(R.id.tv_creator_channel_fantalk_reply_time).text.toString()
)
}
private fun assertReportText(locale: Locale, expected: String) {
val context = localizedContext(locale)
val parent = FrameLayout(context)
val adapter = CreatorChannelFanTalkAdapter()
val holder = adapter.onCreateViewHolder(parent, 0)
val report = holder.itemView.findViewById<TextView>(R.id.tv_creator_channel_fantalk_report)
report.text = "신고"
adapter.submitItems(listOf(fanTalk()))
adapter.onBindViewHolder(holder, 0)
assertEquals(expected, report.text.toString())
}
private fun localizedContext(locale: Locale): Context {
val context = ApplicationProvider.getApplicationContext<Context>()
val configuration = Configuration(context.resources.configuration)
configuration.setLocale(locale)
return context.createConfigurationContext(configuration)
}
private fun fanTalk(createdAtUtc: String = "2026-06-21T00:00:00Z") = CreatorChannelFanTalkUiModel(
fanTalkId = 1L,
writerId = 2L,
writerNickname = "writer",
writerProfileImageUrl = "",
content = "fan talk",
createdAtUtc = createdAtUtc,
reply = CreatorChannelFanTalkReplyUiModel(
fanTalkId = 3L,
writerId = 4L,
writerNickname = "creator",
writerProfileImageUrl = "",
content = "reply",
createdAtUtc = createdAtUtc
),
rightAction = CreatorChannelFanTalkRightAction.Report
)
}
@@ -1,10 +1,5 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk package kr.co.vividnext.sodalive.v2.creator.channel.fantalk
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkRightAction import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkRightAction
@@ -13,35 +8,33 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelFanTalkMapperTest { class CreatorChannelFanTalkMapperTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val relativeTimeTextFormatter = AndroidUtcRelativeTimeTextFormatter(context)
@Test @Test
fun `기본 필드와 상대 시간을 매핑한다`() { fun `기본 필드와 원글 답글 raw UTC를 변경 없이 매핑한다`() {
val item = listOf(fanTalk(createdAtUtc = System.currentTimeMillis().toString())) val item = listOf(
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) fanTalk(
createdAtUtc = "2026-06-21T00:00:00Z",
creatorReplies = listOf(reply(createdAtUtc = "2026-06-21T00:01:00Z"))
)
)
.toFanTalkUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertEquals(1L, item.fanTalkId) assertEquals(1L, item.fanTalkId)
assertEquals(10L, item.writerId) assertEquals(10L, item.writerId)
assertEquals("writer", item.writerNickname) assertEquals("writer", item.writerNickname)
assertEquals("profile.png", item.writerProfileImageUrl) assertEquals("profile.png", item.writerProfileImageUrl)
assertEquals(context.getString(R.string.character_comment_time_just_now), item.createdAtText) assertEquals("2026-06-21T00:00:00Z", item.createdAtUtc)
assertEquals("2026-06-21T00:01:00Z", item.reply?.createdAtUtc)
assertEquals("hello fan talk", item.content) assertEquals("hello fan talk", item.content)
} }
@Test @Test
fun `creatorReplies가 비어 있으면 reply는 null이다`() { fun `creatorReplies가 비어 있으면 reply는 null이다`() {
val item = listOf(fanTalk(creatorReplies = emptyList())) val item = listOf(fanTalk(creatorReplies = emptyList()))
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toFanTalkUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertNull(item.reply) assertNull(item.reply)
@@ -56,7 +49,7 @@ class CreatorChannelFanTalkMapperTest {
reply(fanTalkId = 3L, content = "second reply") reply(fanTalkId = 3L, content = "second reply")
) )
) )
).toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() ).toFanTalkUiModels(isOwner = false, currentUserId = 99L).single()
assertEquals(2L, item.reply?.fanTalkId) assertEquals(2L, item.reply?.fanTalkId)
assertEquals("first reply", item.reply?.content) assertEquals("first reply", item.reply?.content)
@@ -65,7 +58,7 @@ class CreatorChannelFanTalkMapperTest {
@Test @Test
fun `내가 쓴 글이면 수정과 삭제 owner more action이다`() { fun `내가 쓴 글이면 수정과 삭제 owner more action이다`() {
val item = listOf(fanTalk(writerId = 10L)) val item = listOf(fanTalk(writerId = 10L))
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .toFanTalkUiModels(isOwner = false, currentUserId = 10L)
.single() .single()
val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore
@@ -76,7 +69,7 @@ class CreatorChannelFanTalkMapperTest {
@Test @Test
fun `내 채널의 타인 글이면 삭제만 가능한 owner more action이다`() { fun `내 채널의 타인 글이면 삭제만 가능한 owner more action이다`() {
val item = listOf(fanTalk(writerId = 11L)) val item = listOf(fanTalk(writerId = 11L))
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) .toFanTalkUiModels(isOwner = true, currentUserId = 10L)
.single() .single()
val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore
@@ -87,7 +80,7 @@ class CreatorChannelFanTalkMapperTest {
@Test @Test
fun `내가 쓴 글도 아니고 내 채널도 아니면 신고 action이다`() { fun `내가 쓴 글도 아니고 내 채널도 아니면 신고 action이다`() {
val item = listOf(fanTalk(writerId = 11L)) val item = listOf(fanTalk(writerId = 11L))
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) .toFanTalkUiModels(isOwner = false, currentUserId = 10L)
.single() .single()
assertTrue(item.rightAction is CreatorChannelFanTalkRightAction.Report) assertTrue(item.rightAction is CreatorChannelFanTalkRightAction.Report)
@@ -96,7 +89,7 @@ class CreatorChannelFanTalkMapperTest {
@Test @Test
fun `원글과 답글 content가 빈 문자열이어도 item을 유지한다`() { fun `원글과 답글 content가 빈 문자열이어도 item을 유지한다`() {
val item = listOf(fanTalk(content = "", creatorReplies = listOf(reply(content = "")))) val item = listOf(fanTalk(content = "", creatorReplies = listOf(reply(content = ""))))
.toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) .toFanTalkUiModels(isOwner = false, currentUserId = 99L)
.single() .single()
assertEquals("", item.content) assertEquals("", item.content)
@@ -123,13 +116,14 @@ class CreatorChannelFanTalkMapperTest {
private fun reply( private fun reply(
fanTalkId: Long = 2L, fanTalkId: Long = 2L,
writerId: Long = 20L, writerId: Long = 20L,
content: String = "reply" content: String = "reply",
createdAtUtc: String = "2026-06-21T00:00:00Z"
) = CreatorChannelFanTalkReplyResponse( ) = CreatorChannelFanTalkReplyResponse(
fanTalkId = fanTalkId, fanTalkId = fanTalkId,
writerId = writerId, writerId = writerId,
writerNickname = "creator", writerNickname = "creator",
writerProfileImageUrl = "creator.png", writerProfileImageUrl = "creator.png",
content = content, content = content,
createdAtUtc = "2026-06-21T00:00:00Z" createdAtUtc = createdAtUtc
) )
} }
@@ -12,7 +12,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.SingleSubject import io.reactivex.rxjava3.subjects.SingleSubject
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse
@@ -46,7 +45,7 @@ class CreatorChannelFanTalkPaginationTest {
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
SharedPreferenceManager.userId = 10L SharedPreferenceManager.userId = 10L
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelFanTalkViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelFanTalkViewModel(repository)
} }
@After @After
@@ -11,7 +11,6 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse
@@ -45,7 +44,7 @@ class CreatorChannelFanTalkViewModelTest {
SharedPreferenceManager.token = "test-token" SharedPreferenceManager.token = "test-token"
SharedPreferenceManager.userId = 10L SharedPreferenceManager.userId = 10L
repository = org.mockito.kotlin.mock() repository = org.mockito.kotlin.mock()
viewModel = CreatorChannelFanTalkViewModel(repository, AndroidUtcRelativeTimeTextFormatter(context)) viewModel = CreatorChannelFanTalkViewModel(repository)
} }
@After @After

Some files were not shown because too many files have changed in this diff Show More