Compare commits
10 Commits
f508d847ea
...
7c44f3d66c
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c44f3d66c | |||
| 43b6496289 | |||
| 30a4a9ffc7 | |||
| 84cd9bbf41 | |||
| 75480e7a71 | |||
| 8b8b384b29 | |||
| 02cd583f56 | |||
| 3c21a764ae | |||
| d20d6aa971 | |||
| f50f987282 |
@@ -121,6 +121,9 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
|
|||||||
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||||
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
|
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
|
||||||
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
||||||
|
- 자동 테스트는 기본적으로 `app/src/test`의 로직/local unit test만 작성하고 실행한다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest` UI/계측 테스트를 작성하거나 `connectedDebugAndroidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 실행하지 않는다.
|
||||||
|
- UI 변경의 테스트 코드는 adapter 분기, mapper, formatter, presentation model, 라우팅처럼 화면 표현을 결정하는 로직만 검증하며 View 크기·간격·constraint·visibility 등 레이아웃 속성을 직접 검증하지 않는다.
|
||||||
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토를 수행하면 `docs/sample/sample-review.md`를 참조해 `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 결과를 기록한다.
|
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토를 수행하면 `docs/sample/sample-review.md`를 참조해 `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 결과를 기록한다.
|
||||||
- 리뷰 보고서명의 `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다. 예: `phase2-main-home-recommendation-review.md`
|
- 리뷰 보고서명의 `[리뷰범위]`는 `phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다. 예: `phase2-main-home-recommendation-review.md`
|
||||||
- 일반 빌드, 테스트, 린트 실행 결과는 리뷰 보고서를 만들지 않고 계획/TASK 문서의 Task별 검증 기록 또는 `Verification Log`에 남긴다.
|
- 일반 빌드, 테스트, 린트 실행 결과는 리뷰 보고서를 만들지 않고 계획/TASK 문서의 Task별 검증 기록 또는 `Verification Log`에 남긴다.
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ android {
|
|||||||
applicationId "kr.co.vividnext.sodalive"
|
applicationId "kr.co.vividnext.sodalive"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 244
|
versionCode 246
|
||||||
versionName "1.56.0"
|
versionName "1.56.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ dependencies {
|
|||||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
||||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||||
}
|
}
|
||||||
implementation "androidx.datastore:datastore-preferences:1.2.0"
|
implementation "androidx.datastore:datastore-preferences:1.2.1"
|
||||||
|
|
||||||
// Gson
|
// Gson
|
||||||
implementation "com.google.code.gson:gson:2.13.2"
|
implementation "com.google.code.gson:gson:2.13.2"
|
||||||
|
|||||||
@@ -6,12 +6,4 @@ sealed interface CommunityChange {
|
|||||||
data object Created : CommunityChange
|
data object Created : CommunityChange
|
||||||
|
|
||||||
data object Updated : CommunityChange
|
data object Updated : CommunityChange
|
||||||
|
|
||||||
data class Deleted(
|
|
||||||
val postId: Long
|
|
||||||
) : CommunityChange
|
|
||||||
|
|
||||||
data class PinChanged(
|
|
||||||
val postId: Long
|
|
||||||
) : CommunityChange
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
|||||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding
|
||||||
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.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.modify.CreatorCommunityModifyActivity
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||||
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.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
||||||
@@ -60,8 +61,9 @@ import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
|||||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||||
@@ -96,6 +98,7 @@ class CreatorChannelActivity :
|
|||||||
|
|
||||||
private val liveViewModel: LiveViewModel by inject()
|
private val liveViewModel: LiveViewModel by inject()
|
||||||
private val creatorCommunityRepository: CreatorCommunityRepository by inject()
|
private val creatorCommunityRepository: CreatorCommunityRepository by inject()
|
||||||
|
private val creatorChannelRepository: CreatorChannelRepository by inject()
|
||||||
private var creatorId: Long = 0L
|
private var creatorId: Long = 0L
|
||||||
private var currentHeader: CreatorChannelHeaderUiModel? = null
|
private var currentHeader: CreatorChannelHeaderUiModel? = null
|
||||||
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
|
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
|
||||||
@@ -107,6 +110,7 @@ class CreatorChannelActivity :
|
|||||||
private var isOwnerFabAnimating: Boolean = false
|
private var isOwnerFabAnimating: Boolean = false
|
||||||
private var isDonationFloatingButtonVisible: Boolean = false
|
private var isDonationFloatingButtonVisible: Boolean = false
|
||||||
private var isFanTalkFixedPlusVisible: Boolean = false
|
private var isFanTalkFixedPlusVisible: Boolean = false
|
||||||
|
private var isCommunityPostMutating: Boolean = false
|
||||||
private lateinit var loadingDialog: LoadingDialog
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
||||||
private val liveActionCoordinator: LiveActionCoordinator by lazy {
|
private val liveActionCoordinator: LiveActionCoordinator by lazy {
|
||||||
@@ -126,13 +130,6 @@ class CreatorChannelActivity :
|
|||||||
resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode)
|
resolveCommunityActivityResult(CommunityActivityResultSource.Write, result.resultCode)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private val communityPostModifyLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
handleCommunityChange(
|
|
||||||
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val communityDetailLauncher = registerForActivityResult(
|
private val communityDetailLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
@@ -140,6 +137,13 @@ class CreatorChannelActivity :
|
|||||||
resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode)
|
resolveCommunityActivityResult(CommunityActivityResultSource.Detail, result.resultCode)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
private val communityModifyLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
handleCommunityChange(
|
||||||
|
resolveCommunityActivityResult(CommunityActivityResultSource.Modify, result.resultCode)
|
||||||
|
)
|
||||||
|
}
|
||||||
private val fanTalkWriteLauncher = registerForActivityResult(
|
private val fanTalkWriteLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
@@ -627,6 +631,142 @@ class CreatorChannelActivity :
|
|||||||
handleCommunityAction(CommunityActionCommand.PostDetail(postId), communityDetailLauncher::launch)
|
handleCommunityAction(CommunityActionCommand.PostDetail(postId), communityDetailLauncher::launch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
|
||||||
|
if (!item.showMore) return
|
||||||
|
|
||||||
|
CreatorCommunityPostMenuBottomSheetDialog(
|
||||||
|
isFixed = item.isPinned,
|
||||||
|
isCreator = item.isOwner,
|
||||||
|
onClickPin = { updateCreatorChannelCommunityPostFixed(item) },
|
||||||
|
onClickModify = {
|
||||||
|
if (item.isOwner) {
|
||||||
|
communityModifyLauncher.launch(
|
||||||
|
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) },
|
||||||
|
onClickReport = {
|
||||||
|
CreatorCommunityReportDialog(
|
||||||
|
this,
|
||||||
|
layoutInflater
|
||||||
|
) { reason -> reportCreatorChannelCommunityPost(item, reason) }.show(screenWidth)
|
||||||
|
}
|
||||||
|
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem) {
|
||||||
|
if (!item.isOwner || isCommunityPostMutating) return
|
||||||
|
|
||||||
|
isCommunityPostMutating = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
if (response.success) {
|
||||||
|
handleCommunityChange(CommunityChange.Updated)
|
||||||
|
} else {
|
||||||
|
showCommunityMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
showCommunityMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteCreatorChannelCommunityPostDialog(item: CreatorChannelCommunityMenuItem) {
|
||||||
|
if (!item.isOwner) return
|
||||||
|
|
||||||
|
V2ModalDialog(
|
||||||
|
activity = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = getString(R.string.screen_creator_community_delete_title),
|
||||||
|
desc = getString(R.string.screen_creator_community_delete_desc),
|
||||||
|
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
||||||
|
confirmButtonClick = { deleteCreatorChannelCommunityPost(item) },
|
||||||
|
cancelButtonTitle = getString(R.string.cancel)
|
||||||
|
).show(screenWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem) {
|
||||||
|
if (!item.isOwner || isCommunityPostMutating) return
|
||||||
|
|
||||||
|
val request = Gson().toJson(
|
||||||
|
ModifyCommunityPostRequest(
|
||||||
|
creatorCommunityId = item.postId,
|
||||||
|
isActive = false
|
||||||
|
)
|
||||||
|
).toRequestBody("text/plain".toMediaType())
|
||||||
|
isCommunityPostMutating = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
if (response.success) {
|
||||||
|
handleCommunityChange(CommunityChange.Updated)
|
||||||
|
} else {
|
||||||
|
showCommunityMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
showCommunityMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reportCreatorChannelCommunityPost(
|
||||||
|
item: CreatorChannelCommunityMenuItem,
|
||||||
|
reason: String
|
||||||
|
) {
|
||||||
|
if (item.isOwner || !item.showMore || reason.isBlank() || isCommunityPostMutating) return
|
||||||
|
|
||||||
|
isCommunityPostMutating = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
if (response.success) {
|
||||||
|
showToast(
|
||||||
|
if (response.message.isNullOrBlank()) {
|
||||||
|
getString(R.string.character_comment_report_submitted)
|
||||||
|
} else {
|
||||||
|
response.message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showCommunityMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCommunityPostMutating = false
|
||||||
|
showCommunityMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCommunityMutationFailureToast(message: String? = null) {
|
||||||
|
showToast(message.takeUnless { it.isNullOrBlank() } ?: getString(R.string.common_error_unknown))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
|
||||||
override fun onCreatorChannelFanTalkContentChanged() {
|
override fun onCreatorChannelFanTalkContentChanged() {
|
||||||
updateViewPagerHeight {
|
updateViewPagerHeight {
|
||||||
postCheckCreatorChannelCurrentTabNeedsMore()
|
postCheckCreatorChannelCurrentTabNeedsMore()
|
||||||
@@ -697,93 +837,6 @@ class CreatorChannelActivity :
|
|||||||
).show(screenWidth)
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) {
|
|
||||||
CreatorCommunityPostMenuBottomSheetDialog(
|
|
||||||
isFixed = item.isPinned,
|
|
||||||
isCreator = true,
|
|
||||||
onClickPin = {
|
|
||||||
updateCreatorChannelCommunityPostFixed(item)
|
|
||||||
},
|
|
||||||
onClickModify = {
|
|
||||||
communityPostModifyLauncher.launch(
|
|
||||||
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
|
||||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClickDelete = {
|
|
||||||
showCreatorChannelCommunityDeleteDialog(item)
|
|
||||||
},
|
|
||||||
onClickReport = {}
|
|
||||||
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCreatorChannelCommunityDeleteDialog(item: CreatorChannelCommunityPostUiModel) {
|
|
||||||
V2ModalDialog(
|
|
||||||
activity = this,
|
|
||||||
layoutInflater = layoutInflater,
|
|
||||||
title = getString(R.string.screen_creator_community_delete_title),
|
|
||||||
desc = getString(R.string.screen_creator_community_delete_desc),
|
|
||||||
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
|
||||||
confirmButtonClick = {
|
|
||||||
deleteCreatorChannelCommunityPost(item)
|
|
||||||
},
|
|
||||||
cancelButtonTitle = getString(R.string.cancel),
|
|
||||||
cancelButtonClick = {}
|
|
||||||
).show(screenWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityPostUiModel) {
|
|
||||||
compositeDisposable.add(
|
|
||||||
creatorCommunityRepository.updateCommunityPostFixed(
|
|
||||||
postId = item.postId,
|
|
||||||
isFixed = !item.isPinned,
|
|
||||||
token = authToken()
|
|
||||||
)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
{ response ->
|
|
||||||
if (response.success) {
|
|
||||||
handleCommunityChange(CommunityChange.PinChanged(item.postId))
|
|
||||||
} else {
|
|
||||||
response.message?.let(::showToast)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ error -> error.message?.let(::showToast) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityPostUiModel) {
|
|
||||||
val request = ModifyCommunityPostRequest(
|
|
||||||
creatorCommunityId = item.postId,
|
|
||||||
isActive = false
|
|
||||||
)
|
|
||||||
val requestJson = Gson().toJson(request)
|
|
||||||
compositeDisposable.add(
|
|
||||||
creatorCommunityRepository.modifyCommunityPost(
|
|
||||||
postImage = null,
|
|
||||||
request = requestJson.toRequestBody("text/plain".toMediaType()),
|
|
||||||
token = authToken()
|
|
||||||
)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
{ response ->
|
|
||||||
if (response.success) {
|
|
||||||
handleCommunityChange(CommunityChange.Deleted(item.postId))
|
|
||||||
} else {
|
|
||||||
response.message?.let(::showToast)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ error -> error.message?.let(::showToast) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
|
||||||
|
|
||||||
private fun refreshCreatorChannelCommunity() {
|
private fun refreshCreatorChannelCommunity() {
|
||||||
findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()
|
findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()
|
||||||
}
|
}
|
||||||
@@ -795,9 +848,10 @@ class CreatorChannelActivity :
|
|||||||
homeActionDelegate?.refreshHome()
|
homeActionDelegate?.refreshHome()
|
||||||
refreshCreatorChannelCommunity()
|
refreshCreatorChannelCommunity()
|
||||||
}
|
}
|
||||||
CommunityChange.Updated,
|
CommunityChange.Updated -> {
|
||||||
is CommunityChange.Deleted,
|
homeActionDelegate?.refreshHome()
|
||||||
is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()
|
refreshCreatorChannelCommunity()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioConte
|
|||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||||
@@ -28,7 +29,8 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
|||||||
onSeriesClick = ::onSeriesClicked,
|
onSeriesClick = ::onSeriesClicked,
|
||||||
onDonationClick = ::onDonationClicked,
|
onDonationClick = ::onDonationClicked,
|
||||||
onSectionChevronClick = ::onSectionChevronClicked,
|
onSectionChevronClick = ::onSectionChevronClicked,
|
||||||
onCommunityClick = ::onCommunityClicked
|
onCommunityClick = ::onCommunityClicked,
|
||||||
|
onCommunityMoreClick = ::onCommunityMoreClicked
|
||||||
)
|
)
|
||||||
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
||||||
private val host: Host
|
private val host: Host
|
||||||
@@ -134,6 +136,10 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
|||||||
host.onCreatorChannelCommunityPostClicked(postId)
|
host.onCreatorChannelCommunityPostClicked(postId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onCommunityMoreClicked(item: CreatorChannelCommunityMenuItem) {
|
||||||
|
host.onCreatorChannelCommunityMoreClicked(item)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
|
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
|
||||||
host.onCreatorChannelCurrentLiveClicked(live)
|
host.onCreatorChannelCurrentLiveClicked(live)
|
||||||
}
|
}
|
||||||
@@ -150,6 +156,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
|||||||
fun onCreatorChannelDonationClicked()
|
fun onCreatorChannelDonationClicked()
|
||||||
fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)
|
fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)
|
||||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||||
|
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||||
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
|
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
||||||
@@ -29,14 +27,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
|||||||
private val viewModel: CreatorChannelCommunityViewModel by viewModel()
|
private val viewModel: CreatorChannelCommunityViewModel by viewModel()
|
||||||
private val listAdapter = CreatorChannelCommunityListAdapter(
|
private val listAdapter = CreatorChannelCommunityListAdapter(
|
||||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) },
|
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) },
|
||||||
onPlayClick = { item -> toggleCommunityAudio(item) },
|
onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }
|
||||||
onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) },
|
|
||||||
isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true }
|
|
||||||
)
|
)
|
||||||
private val gridAdapter = CreatorChannelCommunityGridAdapter(
|
private val gridAdapter = CreatorChannelCommunityGridAdapter(
|
||||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }
|
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }
|
||||||
)
|
)
|
||||||
private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null
|
|
||||||
private var currentContentState: CreatorChannelCommunityUiState.Content? = null
|
private var currentContentState: CreatorChannelCommunityUiState.Content? = null
|
||||||
private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null
|
private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null
|
||||||
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
||||||
@@ -45,7 +40,6 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
mediaPlayerManager = CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }
|
|
||||||
bindLoading()
|
bindLoading()
|
||||||
setupCommunityList()
|
setupCommunityList()
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
@@ -53,19 +47,12 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
mediaPlayerManager?.stopContent()
|
|
||||||
mediaPlayerManager = null
|
|
||||||
currentContentState = null
|
currentContentState = null
|
||||||
lastContentLayoutKey = null
|
lastContentLayoutKey = null
|
||||||
binding.rvCreatorChannelCommunity.adapter = null
|
binding.rvCreatorChannelCommunity.adapter = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
mediaPlayerManager?.pauseContent()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) {
|
private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) {
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
adapter = listAdapter
|
adapter = listAdapter
|
||||||
@@ -220,16 +207,11 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
|||||||
layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding)
|
layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleCommunityAudio(item: CreatorChannelCommunityPostUiModel) {
|
|
||||||
val audioUrl = item.audioUrl ?: return
|
|
||||||
mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Host {
|
interface Host {
|
||||||
fun isCreatorChannelOwner(): Boolean
|
fun isCreatorChannelOwner(): Boolean
|
||||||
fun onCreatorChannelCommunityContentChanged()
|
fun onCreatorChannelCommunityContentChanged()
|
||||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||||
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel)
|
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ 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.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
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.CreatorCommunityReportDialog
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PurchaseCommunityPostDialog
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.PurchaseCommunityPostDialog
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||||
@@ -40,6 +44,12 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
|
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
|
||||||
|
private val modifyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
viewModel.loadDetail(postId)
|
||||||
|
}
|
||||||
|
}
|
||||||
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
@@ -65,6 +75,11 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
||||||
|
binding.btnCreatorChannelCommunityDetailMore.setOnClickListener {
|
||||||
|
val content = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||||
|
?: return@setOnClickListener
|
||||||
|
showPostMenu(content.post)
|
||||||
|
}
|
||||||
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
|
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
|
||||||
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
|
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
|
||||||
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
||||||
@@ -103,6 +118,13 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
viewModel.consumePostChangedEvent()
|
viewModel.consumePostChangedEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModel.postDeletedEventLiveData.observe(this) { deleted ->
|
||||||
|
if (deleted == true) {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
viewModel.consumePostDeletedEvent()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModel.toastLiveData.observe(this) { event ->
|
viewModel.toastLiveData.observe(this) { event ->
|
||||||
event.consume()?.let { toast ->
|
event.consume()?.let { toast ->
|
||||||
val message = toast.message ?: toast.resId?.let(::getString)
|
val message = toast.message ?: toast.resId?.let(::getString)
|
||||||
@@ -150,6 +172,7 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
}
|
}
|
||||||
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
|
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
|
||||||
tvCreatorChannelCommunityDetailTime.text = post.createdAtText
|
tvCreatorChannelCommunityDetailTime.text = post.createdAtText
|
||||||
|
btnCreatorChannelCommunityDetailMore.isVisible = post.showMore
|
||||||
tvCreatorChannelCommunityDetailBody.text = post.content
|
tvCreatorChannelCommunityDetailBody.text = post.content
|
||||||
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
|
layoutCreatorChannelCommunityDetailReaction.isVisible = post.showReaction || content.isCommentAvailable
|
||||||
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
|
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
|
||||||
@@ -163,9 +186,6 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
ivCreatorChannelCommunityDetailHeart.setImageResource(
|
ivCreatorChannelCommunityDetailHeart.setImageResource(
|
||||||
if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart
|
if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart
|
||||||
)
|
)
|
||||||
layoutCreatorChannelCommunityDetailPrice.isVisible = post.isLocked
|
|
||||||
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
|
|
||||||
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
|
|
||||||
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
||||||
layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
|
layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
|
||||||
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
|
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
|
||||||
@@ -296,6 +316,37 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
|
|||||||
).show(screenWidth)
|
).show(screenWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPostMenu(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||||
|
CreatorCommunityPostMenuBottomSheetDialog(
|
||||||
|
isFixed = post.isPinned,
|
||||||
|
isCreator = post.isOwner,
|
||||||
|
onClickPin = viewModel::updateCommunityPostFixed,
|
||||||
|
onClickModify = {
|
||||||
|
modifyLauncher.launch(
|
||||||
|
Intent(this, CreatorCommunityModifyActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, post.postId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClickDelete = ::showDeletePostDialog,
|
||||||
|
onClickReport = {
|
||||||
|
CreatorCommunityReportDialog(this, layoutInflater, viewModel::reportCommunityPost).show(screenWidth)
|
||||||
|
}
|
||||||
|
).show(supportFragmentManager, CreatorCommunityPostMenuBottomSheetDialog::class.java.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeletePostDialog() {
|
||||||
|
V2ModalDialog(
|
||||||
|
activity = this,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = getString(R.string.screen_creator_community_delete_title),
|
||||||
|
desc = getString(R.string.screen_creator_community_delete_desc),
|
||||||
|
confirmButtonTitle = getString(R.string.confirm_delete_title),
|
||||||
|
confirmButtonClick = viewModel::deleteCommunityPost,
|
||||||
|
cancelButtonTitle = getString(R.string.cancel)
|
||||||
|
).show(screenWidth)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showDeleteCommentDialog(commentId: Long) {
|
private fun showDeleteCommentDialog(commentId: Long) {
|
||||||
V2ModalDialog(
|
V2ModalDialog(
|
||||||
activity = this,
|
activity = this,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.google.gson.Gson
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
@@ -12,12 +13,15 @@ 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.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.v2.creator.channel.CreatorChannelEvent
|
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
class CreatorChannelCommunityDetailViewModel(
|
class CreatorChannelCommunityDetailViewModel(
|
||||||
private val repository: CreatorChannelRepository,
|
private val repository: CreatorChannelRepository,
|
||||||
@@ -37,6 +41,10 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
val postChangedEventLiveData: LiveData<Boolean>
|
val postChangedEventLiveData: LiveData<Boolean>
|
||||||
get() = _postChangedEventLiveData
|
get() = _postChangedEventLiveData
|
||||||
|
|
||||||
|
private val _postDeletedEventLiveData = MutableLiveData<Boolean>()
|
||||||
|
val postDeletedEventLiveData: LiveData<Boolean>
|
||||||
|
get() = _postDeletedEventLiveData
|
||||||
|
|
||||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||||
get() = _toastLiveData
|
get() = _toastLiveData
|
||||||
@@ -49,6 +57,7 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
private var isSendingComment = false
|
private var isSendingComment = false
|
||||||
private var isModifyingComment = false
|
private var isModifyingComment = false
|
||||||
private var isPurchasingPost = false
|
private var isPurchasingPost = false
|
||||||
|
private var isMutatingPost = false
|
||||||
|
|
||||||
fun loadDetail(postId: Long) {
|
fun loadDetail(postId: Long) {
|
||||||
if (postId <= 0) return
|
if (postId <= 0) return
|
||||||
@@ -181,6 +190,10 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
_postChangedEventLiveData.value = false
|
_postChangedEventLiveData.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consumePostDeletedEvent() {
|
||||||
|
_postDeletedEventLiveData.value = false
|
||||||
|
}
|
||||||
|
|
||||||
fun startCommentEdit(commentId: Long, comment: String) {
|
fun startCommentEdit(commentId: Long, comment: String) {
|
||||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
if (commentId <= 0) return
|
if (commentId <= 0) return
|
||||||
@@ -245,6 +258,97 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCommunityPostFixed() {
|
||||||
|
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
|
if (!content.post.isOwner || isMutatingPost) return
|
||||||
|
|
||||||
|
isMutatingPost = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
legacyRepository.updateCommunityPostFixed(postId, !content.post.isPinned, authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isMutatingPost = false
|
||||||
|
if (response.success) {
|
||||||
|
_postChangedEventLiveData.value = true
|
||||||
|
loadDetail(postId)
|
||||||
|
} else {
|
||||||
|
showMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isMutatingPost = false
|
||||||
|
showMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteCommunityPost() {
|
||||||
|
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
|
if (!content.post.isOwner || isMutatingPost) return
|
||||||
|
|
||||||
|
val request = Gson().toJson(
|
||||||
|
ModifyCommunityPostRequest(
|
||||||
|
creatorCommunityId = postId,
|
||||||
|
isActive = false
|
||||||
|
)
|
||||||
|
).toRequestBody("text/plain".toMediaType())
|
||||||
|
isMutatingPost = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
legacyRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isMutatingPost = false
|
||||||
|
if (response.success) {
|
||||||
|
_postDeletedEventLiveData.value = true
|
||||||
|
} else {
|
||||||
|
showMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isMutatingPost = false
|
||||||
|
showMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportCommunityPost(reason: String) {
|
||||||
|
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||||
|
if (reason.isBlank() || content.post.isOwner || !content.post.showMore || isMutatingPost) return
|
||||||
|
|
||||||
|
isMutatingPost = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.reportCommunityPost(postId, reason, authToken())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ response ->
|
||||||
|
isMutatingPost = false
|
||||||
|
if (response.success) {
|
||||||
|
_toastLiveData.value = CreatorChannelEvent(
|
||||||
|
if (response.message.isNullOrBlank()) {
|
||||||
|
ToastMessage(resId = R.string.character_comment_report_submitted)
|
||||||
|
} else {
|
||||||
|
ToastMessage(message = response.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showMutationFailureToast(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isMutatingPost = false
|
||||||
|
showMutationFailureToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun submitCommentEdit(
|
private fun submitCommentEdit(
|
||||||
content: CreatorChannelCommunityDetailUiState.Content,
|
content: CreatorChannelCommunityDetailUiState.Content,
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
@@ -425,7 +529,9 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel(
|
private fun CreatorChannelCommunityPostDetailResponse.toUiModel(): CreatorChannelCommunityPostDetailUiModel {
|
||||||
|
val isOwner = creatorId == SharedPreferenceManager.userId
|
||||||
|
return CreatorChannelCommunityPostDetailUiModel(
|
||||||
postId = postId,
|
postId = postId,
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
creatorNickname = creatorNickname,
|
creatorNickname = creatorNickname,
|
||||||
@@ -440,10 +546,13 @@ class CreatorChannelCommunityDetailViewModel(
|
|||||||
commentCount = commentCount,
|
commentCount = commentCount,
|
||||||
isLiked = isLiked,
|
isLiked = isLiked,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
|
isOwner = isOwner,
|
||||||
|
showMore = isOwner || price == 0 || existOrdered,
|
||||||
isLocked = isLocked(),
|
isLocked = isLocked(),
|
||||||
showPaywall = isLocked(),
|
showPaywall = isLocked(),
|
||||||
showReaction = !isLocked()
|
showReaction = !isLocked()
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean {
|
private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean {
|
||||||
return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId
|
return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId
|
||||||
@@ -526,6 +635,8 @@ data class CreatorChannelCommunityPostDetailUiModel(
|
|||||||
val commentCount: Int,
|
val commentCount: Int,
|
||||||
val isLiked: Boolean,
|
val isLiked: Boolean,
|
||||||
val isPinned: Boolean,
|
val isPinned: Boolean,
|
||||||
|
val isOwner: Boolean,
|
||||||
|
val showMore: Boolean,
|
||||||
val isLocked: Boolean,
|
val isLocked: Boolean,
|
||||||
val showPaywall: Boolean,
|
val showPaywall: Boolean,
|
||||||
val showReaction: Boolean
|
val showReaction: Boolean
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
|||||||
val isLocked = price > 0 && !existOrdered && !isOwner
|
val isLocked = price > 0 && !existOrdered && !isOwner
|
||||||
val showOwnerActions = isOwner && creatorId == currentUserId
|
val showOwnerActions = isOwner && creatorId == currentUserId
|
||||||
val visibleImageUrl = imageUrl.takeUnless { isLocked }
|
val visibleImageUrl = imageUrl.takeUnless { isLocked }
|
||||||
val showPlayButton = !isLocked && !audioUrl.isNullOrBlank() && !visibleImageUrl.isNullOrBlank()
|
|
||||||
return CreatorChannelCommunityPostUiModel(
|
return CreatorChannelCommunityPostUiModel(
|
||||||
postId = postId,
|
postId = postId,
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
@@ -30,7 +29,6 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
|||||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||||
content = content,
|
content = content,
|
||||||
imageUrl = visibleImageUrl,
|
imageUrl = visibleImageUrl,
|
||||||
audioUrl = audioUrl,
|
|
||||||
price = price,
|
price = price,
|
||||||
existOrdered = existOrdered,
|
existOrdered = existOrdered,
|
||||||
likeCount = likeCount,
|
likeCount = likeCount,
|
||||||
@@ -40,11 +38,17 @@ private fun CreatorChannelCommunityPostResponse.toCommunityPostUiModel(
|
|||||||
showNotice = isPinned,
|
showNotice = isPinned,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
isLocked = isLocked,
|
isLocked = isLocked,
|
||||||
showOwnerMore = showOwnerActions,
|
|
||||||
showOwnerTopPrice = showOwnerActions && price > 0,
|
showOwnerTopPrice = showOwnerActions && price > 0,
|
||||||
showPlayButton = showPlayButton,
|
|
||||||
gridPreviewText = content.toGridPreviewText(),
|
gridPreviewText = content.toGridPreviewText(),
|
||||||
imageMode = toImageMode(isLocked, visibleImageUrl)
|
imageMode = toImageMode(isLocked, visibleImageUrl),
|
||||||
|
menuItem = CreatorChannelCommunityMenuItem(
|
||||||
|
postId = postId,
|
||||||
|
creatorId = creatorId,
|
||||||
|
currentUserId = currentUserId,
|
||||||
|
price = price,
|
||||||
|
existOrdered = existOrdered,
|
||||||
|
isPinned = isPinned
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.community.model
|
||||||
|
|
||||||
|
data class CreatorChannelCommunityMenuItem(
|
||||||
|
val postId: Long,
|
||||||
|
val creatorId: Long,
|
||||||
|
val currentUserId: Long,
|
||||||
|
val price: Int,
|
||||||
|
val existOrdered: Boolean,
|
||||||
|
val isPinned: Boolean
|
||||||
|
) {
|
||||||
|
val isOwner: Boolean
|
||||||
|
get() = creatorId == currentUserId
|
||||||
|
|
||||||
|
val showMore: Boolean
|
||||||
|
get() = isOwner || price == 0 || existOrdered
|
||||||
|
}
|
||||||
@@ -32,7 +32,6 @@ data class CreatorChannelCommunityPostUiModel(
|
|||||||
val createdAtText: String,
|
val createdAtText: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val audioUrl: String?,
|
|
||||||
val price: Int,
|
val price: Int,
|
||||||
val existOrdered: Boolean,
|
val existOrdered: Boolean,
|
||||||
val likeCount: Int,
|
val likeCount: Int,
|
||||||
@@ -42,9 +41,8 @@ data class CreatorChannelCommunityPostUiModel(
|
|||||||
val showNotice: Boolean,
|
val showNotice: Boolean,
|
||||||
val isPinned: Boolean,
|
val isPinned: Boolean,
|
||||||
val isLocked: Boolean,
|
val isLocked: Boolean,
|
||||||
val showOwnerMore: Boolean,
|
|
||||||
val showOwnerTopPrice: Boolean,
|
val showOwnerTopPrice: Boolean,
|
||||||
val showPlayButton: Boolean,
|
|
||||||
val gridPreviewText: String,
|
val gridPreviewText: String,
|
||||||
val imageMode: CreatorChannelCommunityImageMode
|
val imageMode: CreatorChannelCommunityImageMode,
|
||||||
|
val menuItem: CreatorChannelCommunityMenuItem
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBindi
|
|||||||
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
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
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.model.CreatorChannelCommunityPostUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
|
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
|
||||||
|
|
||||||
class CreatorChannelCommunityListAdapter(
|
class CreatorChannelCommunityListAdapter(
|
||||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
|
||||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
|
||||||
private val isPlayingContent: (Long) -> Boolean = { false }
|
|
||||||
) : RecyclerView.Adapter<CreatorChannelCommunityListAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<CreatorChannelCommunityListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
|
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
|
||||||
@@ -40,9 +39,7 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
return ViewHolder(
|
return ViewHolder(
|
||||||
ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
onPostClick,
|
onPostClick,
|
||||||
onPlayClick,
|
onMoreClick
|
||||||
onOwnerMoreClick,
|
|
||||||
isPlayingContent
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +52,7 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
class ViewHolder(
|
class ViewHolder(
|
||||||
private val binding: ItemCreatorChannelCommunityListBinding,
|
private val binding: ItemCreatorChannelCommunityListBinding,
|
||||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
|
||||||
private val isPlayingContent: (Long) -> Boolean
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -86,7 +81,7 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
|
|
||||||
val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked }
|
val visibleImageUrl = item.imageUrl.takeUnless { item.isLocked }
|
||||||
layoutCreatorChannelCommunityListImageContainer.isVisible =
|
layoutCreatorChannelCommunityListImageContainer.isVisible =
|
||||||
visibleImageUrl != null || item.isLocked || item.showPlayButton
|
visibleImageUrl != null || item.isLocked
|
||||||
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
|
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
|
||||||
resetCommunityImageHeight()
|
resetCommunityImageHeight()
|
||||||
if (visibleImageUrl != null) {
|
if (visibleImageUrl != null) {
|
||||||
@@ -119,19 +114,14 @@ class CreatorChannelCommunityListAdapter(
|
|||||||
}
|
}
|
||||||
layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked
|
layoutCreatorChannelCommunityListLockedOverlay.isVisible = item.isLocked
|
||||||
ivCreatorChannelCommunityListLock.isVisible = item.isLocked
|
ivCreatorChannelCommunityListLock.isVisible = item.isLocked
|
||||||
tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked
|
layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked
|
||||||
tvCreatorChannelCommunityListLockedPrice.text = item.price.moneyFormat()
|
tvCreatorChannelCommunityListLockedPrice.text = item.price.moneyFormat()
|
||||||
ivCreatorChannelCommunityListPlay.isVisible = item.showPlayButton
|
|
||||||
ivCreatorChannelCommunityListPlay.setImageResource(
|
|
||||||
if (isPlayingContent(item.postId)) R.drawable.ic_player_pause else R.drawable.ic_new_player_play
|
|
||||||
)
|
|
||||||
ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }
|
|
||||||
|
|
||||||
layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerMore || item.showOwnerTopPrice
|
layoutCreatorChannelCommunityListTopActions.isVisible = item.showOwnerTopPrice || item.menuItem.showMore
|
||||||
layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice
|
layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice
|
||||||
tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat()
|
tvCreatorChannelCommunityListTopPrice.text = item.price.moneyFormat()
|
||||||
ivCreatorChannelCommunityListOwnerMore.isVisible = item.showOwnerMore
|
btnCreatorChannelCommunityListMore.isVisible = item.menuItem.showMore
|
||||||
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }
|
btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetCommunityImageHeight() {
|
private fun resetCommunityImageHeight() {
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ data class CreatorChannelCommunityPostResponse(
|
|||||||
@SerializedName("dateUtc") val dateUtc: String,
|
@SerializedName("dateUtc") val dateUtc: String,
|
||||||
@SerializedName("existOrdered") val existOrdered: Boolean,
|
@SerializedName("existOrdered") val existOrdered: Boolean,
|
||||||
@SerializedName("likeCount") val likeCount: Int,
|
@SerializedName("likeCount") val likeCount: Int,
|
||||||
@SerializedName("commentCount") val commentCount: Int
|
@SerializedName("commentCount") val commentCount: Int,
|
||||||
|
@SerializedName("isPinned") val isPinned: Boolean,
|
||||||
|
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ class CreatorChannelRepository(
|
|||||||
token = token
|
token = token
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun reportCommunityPost(postId: Long, reason: String, token: String) = reportRepository.report(
|
||||||
|
request = ReportRequest(ReportType.COMMUNITY_POST, reason, communityPostId = postId),
|
||||||
|
token = token
|
||||||
|
)
|
||||||
|
|
||||||
fun deleteFanTalk(fanTalkId: Long, token: String) = explorerRepository.modifyCheers(
|
fun deleteFanTalk(fanTalkId: Long, token: String) = explorerRepository.modifyCheers(
|
||||||
request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false),
|
request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false),
|
||||||
token = token
|
token = token
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne
|
|||||||
?.let { add(CreatorChannelHomeSection.Schedules(it)) }
|
?.let { add(CreatorChannelHomeSection.Schedules(it)) }
|
||||||
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
|
audioContents.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.AudioContents(it)) }
|
||||||
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
|
||||||
communities.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Communities(it)) }
|
communities.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
|
||||||
add(CreatorChannelHomeSection.FanTalk(fanTalk))
|
add(CreatorChannelHomeSection.FanTalk(fanTalk))
|
||||||
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
|
||||||
add(CreatorChannelHomeSection.Activity(activity))
|
add(CreatorChannelHomeSection.Activity(activity))
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ sealed interface CreatorChannelHomeSection {
|
|||||||
data class Schedules(val schedules: List<CreatorChannelScheduleResponse>) : CreatorChannelHomeSection
|
data class Schedules(val schedules: List<CreatorChannelScheduleResponse>) : CreatorChannelHomeSection
|
||||||
data class AudioContents(val audioContents: List<CreatorChannelAudioContentResponse>) : CreatorChannelHomeSection
|
data class AudioContents(val audioContents: List<CreatorChannelAudioContentResponse>) : CreatorChannelHomeSection
|
||||||
data class Series(val series: List<CreatorChannelSeriesResponse>) : CreatorChannelHomeSection
|
data class Series(val series: List<CreatorChannelSeriesResponse>) : CreatorChannelHomeSection
|
||||||
data class Communities(val communities: List<CreatorChannelCommunityPostResponse>) : CreatorChannelHomeSection
|
data class Communities(
|
||||||
|
val communities: List<CreatorChannelCommunityPostResponse>,
|
||||||
|
val currentMemberId: Long
|
||||||
|
) : CreatorChannelHomeSection
|
||||||
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
|
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
|
||||||
data class Introduce(val introduce: String) : CreatorChannelHomeSection
|
data class Introduce(val introduce: String) : CreatorChannelHomeSection
|
||||||
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
|
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityP
|
|||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityMenuItem
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||||
@@ -43,7 +44,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
|
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
|
||||||
private val onDonationClick: () -> Unit = {},
|
private val onDonationClick: () -> Unit = {},
|
||||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {},
|
private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {},
|
||||||
private val onCommunityClick: (Long) -> Unit = {}
|
private val onCommunityClick: (Long) -> Unit = {},
|
||||||
|
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
||||||
|
|
||||||
private var items: List<CreatorChannelHomeSection> = emptyList()
|
private var items: List<CreatorChannelHomeSection> = emptyList()
|
||||||
@@ -65,7 +67,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
onSeriesClick,
|
onSeriesClick,
|
||||||
onDonationClick,
|
onDonationClick,
|
||||||
onSectionChevronClick,
|
onSectionChevronClick,
|
||||||
onCommunityClick
|
onCommunityClick,
|
||||||
|
onCommunityMoreClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +86,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
|
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
|
||||||
private val onDonationClick: () -> Unit,
|
private val onDonationClick: () -> Unit,
|
||||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit,
|
private val onSectionChevronClick: (CreatorChannelTab) -> Unit,
|
||||||
private val onCommunityClick: (Long) -> Unit
|
private val onCommunityClick: (Long) -> Unit,
|
||||||
|
private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
||||||
private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron)
|
private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron)
|
||||||
@@ -366,6 +370,14 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }
|
communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }
|
||||||
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
|
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
|
||||||
visibleCommunities.forEachIndexed { index, community ->
|
visibleCommunities.forEachIndexed { index, community ->
|
||||||
|
val menuItem = CreatorChannelCommunityMenuItem(
|
||||||
|
postId = community.postId,
|
||||||
|
creatorId = community.creatorId,
|
||||||
|
currentUserId = item.currentMemberId,
|
||||||
|
price = community.price,
|
||||||
|
existOrdered = community.existOrdered,
|
||||||
|
isPinned = community.isPinned
|
||||||
|
)
|
||||||
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
|
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
|
||||||
itemView.resources.configuration.screenWidthDp
|
itemView.resources.configuration.screenWidthDp
|
||||||
)
|
)
|
||||||
@@ -381,7 +393,8 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
setHideEmptyTextRows(true)
|
setHideEmptyTextRows(true)
|
||||||
bind(community.toFeedCommunityItem())
|
bind(community.toFeedCommunityItem(showMore = menuItem.showMore))
|
||||||
|
setOnMoreClick { onCommunityMoreClick(menuItem) }
|
||||||
}
|
}
|
||||||
bindCommunityImages(row, community)
|
bindCommunityImages(row, community)
|
||||||
row.layoutParams = LinearLayout.LayoutParams(
|
row.layoutParams = LinearLayout.LayoutParams(
|
||||||
@@ -443,7 +456,9 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(): FeedItem.Community = FeedItem.Community(
|
private fun CreatorChannelCommunityPostResponse.toFeedCommunityItem(
|
||||||
|
showMore: Boolean
|
||||||
|
): FeedItem.Community = FeedItem.Community(
|
||||||
feedId = postId.toString(),
|
feedId = postId.toString(),
|
||||||
creatorId = creatorId.toString(),
|
creatorId = creatorId.toString(),
|
||||||
creatorName = creatorNickname,
|
creatorName = creatorNickname,
|
||||||
@@ -458,7 +473,11 @@ class CreatorChannelHomeSectionAdapter(
|
|||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
price = price,
|
price = price,
|
||||||
existOrdered = existOrdered,
|
existOrdered = existOrdered,
|
||||||
showKeyword = false
|
isPinned = isPinned,
|
||||||
|
isCommentAvailable = isCommentAvailable,
|
||||||
|
hideReactionWhenLocked = true,
|
||||||
|
showKeyword = false,
|
||||||
|
showMore = showMore
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {
|
private fun bindIntroduce(item: CreatorChannelHomeSection.Introduce) {
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
setOnBannerClick { onBannerClick(it) }
|
setOnBannerClick { onBannerClick(it) }
|
||||||
}
|
}
|
||||||
liveAdapter.setOnLiveClick { onLiveClick(it) }
|
liveAdapter.setOnLiveClick { onLiveClick(it) }
|
||||||
liveAdapter.setOnMoreClick { openHomeOnAirLive() }
|
binding.tvHomeLiveAll.setOnClickListener { openHomeOnAirLive() }
|
||||||
binding.rvHomeLives.adapter = liveAdapter
|
binding.rvHomeLives.adapter = liveAdapter
|
||||||
binding.rvHomeRecentActivityCreators.adapter = recentActivityCreatorAdapter
|
binding.rvHomeRecentActivityCreators.adapter = recentActivityCreatorAdapter
|
||||||
binding.rvHomeRecentDebutCreators.adapter = recentDebutCreatorAdapter
|
binding.rvHomeRecentDebutCreators.adapter = recentDebutCreatorAdapter
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||||
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
|
||||||
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.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
@@ -12,15 +9,12 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
|
|||||||
import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailItem
|
import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailItem
|
||||||
import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView
|
import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView
|
||||||
|
|
||||||
class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveItemViewHolder>() {
|
||||||
private var items: List<HomeRecommendationLiveUiModel> = emptyList()
|
private var items: List<HomeRecommendationLiveUiModel> = emptyList()
|
||||||
private var showAllItem: Boolean = false
|
|
||||||
private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null
|
private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null
|
||||||
private var onMoreClick: (() -> Unit)? = null
|
|
||||||
|
|
||||||
fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
|
fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
|
||||||
this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
|
this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
|
||||||
showAllItem = items.isNotEmpty()
|
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,32 +22,15 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
|||||||
onClick = listener
|
onClick = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnMoreClick(listener: (() -> Unit)?) {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveItemViewHolder {
|
||||||
onMoreClick = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
|
|
||||||
return if (viewType == VIEW_TYPE_MORE) {
|
|
||||||
MoreViewHolder(createMoreView(parent))
|
|
||||||
} else {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_live_thumbnail_simple, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_live_thumbnail_simple, parent, false)
|
||||||
view.layoutParams = liveItemLayoutParams(parent)
|
view.layoutParams = liveItemLayoutParams(parent)
|
||||||
LiveItemViewHolder(view as LiveThumbnailSimpleView)
|
return LiveItemViewHolder(view as LiveThumbnailSimpleView)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: LiveItemViewHolder, position: Int) = holder.bind(items[position], onClick)
|
||||||
when (holder) {
|
|
||||||
is LiveItemViewHolder -> holder.bind(items[position], onClick)
|
|
||||||
is MoreViewHolder -> holder.bind(onMoreClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size + if (showAllItem) 1 else 0
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return if (showAllItem && position == itemCount - 1) VIEW_TYPE_MORE else VIEW_TYPE_LIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun liveItemLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams {
|
private fun liveItemLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams {
|
||||||
return RecyclerView.LayoutParams(
|
return RecyclerView.LayoutParams(
|
||||||
@@ -62,25 +39,9 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
|||||||
).apply { marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_14) }
|
).apply { marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_14) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMoreView(parent: ViewGroup): TextView {
|
|
||||||
return TextView(parent.context).apply {
|
|
||||||
layoutParams = RecyclerView.LayoutParams(
|
|
||||||
parent.resources.getDimensionPixelSize(R.dimen.home_live_more_width),
|
|
||||||
parent.resources.getDimensionPixelSize(R.dimen.home_live_row_height)
|
|
||||||
)
|
|
||||||
gravity = Gravity.CENTER
|
|
||||||
setBackgroundResource(R.color.black)
|
|
||||||
setText(R.string.screen_home_theme_all)
|
|
||||||
setTextAppearance(R.style.Typography_Body5)
|
|
||||||
setTextColor(parent.context.getColor(R.color.soda_400))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class LiveViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
class LiveItemViewHolder(
|
class LiveItemViewHolder(
|
||||||
private val view: LiveThumbnailSimpleView
|
private val view: LiveThumbnailSimpleView
|
||||||
) : LiveViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
fun bind(
|
fun bind(
|
||||||
item: HomeRecommendationLiveUiModel,
|
item: HomeRecommendationLiveUiModel,
|
||||||
onClick: ((HomeRecommendationLiveUiModel) -> Unit)?
|
onClick: ((HomeRecommendationLiveUiModel) -> Unit)?
|
||||||
@@ -99,17 +60,7 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MoreViewHolder(
|
|
||||||
private val view: TextView
|
|
||||||
) : LiveViewHolder(view) {
|
|
||||||
fun bind(onClick: (() -> Unit)?) {
|
|
||||||
view.setOnClickListener(if (onClick == null) null else View.OnClickListener { onClick.invoke() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_VISIBLE_LIVE_COUNT = 20
|
private const val MAX_VISIBLE_LIVE_COUNT = 20
|
||||||
private const val VIEW_TYPE_LIVE = 0
|
|
||||||
private const val VIEW_TYPE_MORE = 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var profileImage: ImageView? = null
|
private var profileImage: ImageView? = null
|
||||||
|
private var noticeRow: View? = null
|
||||||
|
private var moreButton: View? = null
|
||||||
private var creatorText: TextView? = null
|
private var creatorText: TextView? = null
|
||||||
private var createdAtText: TextView? = null
|
private var createdAtText: TextView? = null
|
||||||
private var bodyText: TextView? = null
|
private var bodyText: TextView? = null
|
||||||
@@ -27,15 +29,21 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
private var communityImage: ImageView? = null
|
private var communityImage: ImageView? = null
|
||||||
private var paidOverlay: View? = null
|
private var paidOverlay: View? = null
|
||||||
private var priceText: TextView? = null
|
private var priceText: TextView? = null
|
||||||
|
private var reactionRow: View? = null
|
||||||
|
private var commentIcon: ImageView? = null
|
||||||
private var commentCountText: TextView? = null
|
private var commentCountText: TextView? = null
|
||||||
|
private var likeIcon: ImageView? = null
|
||||||
private var likeCountText: TextView? = null
|
private var likeCountText: TextView? = null
|
||||||
private var currentItem: FeedItem.Community? = null
|
private var currentItem: FeedItem.Community? = null
|
||||||
private var clickListener: ((FeedItem) -> Unit)? = null
|
private var clickListener: ((FeedItem) -> Unit)? = null
|
||||||
|
private var moreClickListener: ((FeedItem.Community) -> Unit)? = null
|
||||||
private var hideEmptyTextRows: Boolean = false
|
private var hideEmptyTextRows: Boolean = false
|
||||||
|
|
||||||
override fun onFinishInflate() {
|
override fun onFinishInflate() {
|
||||||
super.onFinishInflate()
|
super.onFinishInflate()
|
||||||
|
noticeRow = findViewById(R.id.ll_feed_community_notice)
|
||||||
profileImage = findViewById(R.id.iv_feed_community_profile)
|
profileImage = findViewById(R.id.iv_feed_community_profile)
|
||||||
|
moreButton = findViewById(R.id.btn_feed_community_more)
|
||||||
creatorText = findViewById(R.id.tv_feed_community_creator)
|
creatorText = findViewById(R.id.tv_feed_community_creator)
|
||||||
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
||||||
bodyText = findViewById(R.id.tv_feed_community_body)
|
bodyText = findViewById(R.id.tv_feed_community_body)
|
||||||
@@ -43,7 +51,10 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
communityImage = findViewById(R.id.iv_feed_community_image)
|
communityImage = findViewById(R.id.iv_feed_community_image)
|
||||||
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
||||||
priceText = findViewById(R.id.tv_feed_community_price)
|
priceText = findViewById(R.id.tv_feed_community_price)
|
||||||
|
reactionRow = findViewById(R.id.ll_feed_community_reaction)
|
||||||
|
commentIcon = findViewById(R.id.iv_feed_community_comment)
|
||||||
commentCountText = findViewById(R.id.tv_feed_community_comment_count)
|
commentCountText = findViewById(R.id.tv_feed_community_comment_count)
|
||||||
|
likeIcon = findViewById(R.id.iv_feed_community_like)
|
||||||
likeCountText = findViewById(R.id.tv_feed_community_like_count)
|
likeCountText = findViewById(R.id.tv_feed_community_like_count)
|
||||||
clipToOutline = true
|
clipToOutline = true
|
||||||
outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
|
outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
|
||||||
@@ -59,7 +70,8 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
requireNotNull(createdAtText).text = item.createdAtText
|
requireNotNull(createdAtText).text = item.createdAtText
|
||||||
requireNotNull(bodyText).text = item.bodyText
|
requireNotNull(bodyText).text = item.bodyText
|
||||||
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
||||||
val isLocked = item.price > 0 && !item.existOrdered
|
requireNotNull(noticeRow).isVisible = item.isPinned
|
||||||
|
val isLocked = item.isLocked
|
||||||
val hasImage = !item.imageUrl.isNullOrBlank()
|
val hasImage = !item.imageUrl.isNullOrBlank()
|
||||||
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
requireNotNull(communityImageContainer).isVisible = hasImage || isLocked
|
||||||
resetCommunityImageHeight()
|
resetCommunityImageHeight()
|
||||||
@@ -69,9 +81,15 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
requireNotNull(paidOverlay).isVisible = isLocked
|
requireNotNull(paidOverlay).isVisible = isLocked
|
||||||
requireNotNull(priceText).text = item.price.toString()
|
requireNotNull(priceText).text = item.price.toString()
|
||||||
|
requireNotNull(reactionRow).isVisible = item.showReaction
|
||||||
|
requireNotNull(commentIcon).isVisible = item.showComment
|
||||||
requireNotNull(commentCountText).text = item.commentCount.toString()
|
requireNotNull(commentCountText).text = item.commentCount.toString()
|
||||||
|
requireNotNull(commentCountText).isVisible = item.showComment
|
||||||
|
requireNotNull(likeIcon).isVisible = item.showReaction
|
||||||
requireNotNull(likeCountText).text = item.likeCount.toString()
|
requireNotNull(likeCountText).text = item.likeCount.toString()
|
||||||
|
requireNotNull(likeCountText).isVisible = item.showReaction
|
||||||
applyClickState(item)
|
applyClickState(item)
|
||||||
|
applyMoreClickState(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun profileImageView(): ImageView = requireNotNull(profileImage)
|
fun profileImageView(): ImageView = requireNotNull(profileImage)
|
||||||
@@ -116,6 +134,11 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
currentItem?.let(::applyClickState)
|
currentItem?.let(::applyClickState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOnMoreClick(listener: ((FeedItem.Community) -> Unit)?) {
|
||||||
|
moreClickListener = listener
|
||||||
|
currentItem?.let(::applyMoreClickState)
|
||||||
|
}
|
||||||
|
|
||||||
fun setHideEmptyTextRows(hide: Boolean) {
|
fun setHideEmptyTextRows(hide: Boolean) {
|
||||||
hideEmptyTextRows = hide
|
hideEmptyTextRows = hide
|
||||||
currentItem?.let(::bind)
|
currentItem?.let(::bind)
|
||||||
@@ -130,6 +153,14 @@ class FeedCommunityView @JvmOverloads constructor(
|
|||||||
isClickable = listener != null
|
isClickable = listener != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyMoreClickState(item: FeedItem.Community) {
|
||||||
|
val listener = moreClickListener
|
||||||
|
requireNotNull(moreButton).isVisible = item.showMore
|
||||||
|
requireNotNull(moreButton).setOnClickListener(
|
||||||
|
if (listener == null) null else View.OnClickListener { listener(item) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateRootWidth(width: Int) {
|
private fun updateRootWidth(width: Int) {
|
||||||
val currentLayoutParams = layoutParams
|
val currentLayoutParams = layoutParams
|
||||||
layoutParams = if (currentLayoutParams == null) {
|
layoutParams = if (currentLayoutParams == null) {
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) {
|
|||||||
val audioUrl: String? = null,
|
val audioUrl: String? = null,
|
||||||
val price: Int = 0,
|
val price: Int = 0,
|
||||||
val existOrdered: Boolean = false,
|
val existOrdered: Boolean = false,
|
||||||
val showKeyword: Boolean = true
|
val isPinned: Boolean = false,
|
||||||
) : FeedItem(feedId, FeedVariant.Community)
|
val isCommentAvailable: Boolean = true,
|
||||||
|
val hideReactionWhenLocked: Boolean = false,
|
||||||
|
val showKeyword: Boolean = true,
|
||||||
|
val showMore: Boolean = false
|
||||||
|
) : FeedItem(feedId, FeedVariant.Community) {
|
||||||
|
val isLocked: Boolean
|
||||||
|
get() = price > 0 && !existOrdered
|
||||||
|
|
||||||
|
val showReaction: Boolean
|
||||||
|
get() = !hideReactionWhenLocked || !isLocked
|
||||||
|
|
||||||
|
val showComment: Boolean
|
||||||
|
get() = showReaction && isCommentAvailable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
app:layout_constraintEnd_toStartOf="@id/layout_creator_channel_community_detail_price"
|
app:layout_constraintEnd_toStartOf="@id/btn_creator_channel_community_detail_more"
|
||||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
|
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
|
||||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||||
tools:text="크리에이터" />
|
tools:text="크리에이터" />
|
||||||
@@ -91,36 +91,17 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
|
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
|
||||||
tools:text="2분 전" />
|
tools:text="2분 전" />
|
||||||
|
|
||||||
<LinearLayout
|
<ImageButton
|
||||||
android:id="@+id/layout_creator_channel_community_detail_price"
|
android:id="@+id/btn_creator_channel_community_detail_more"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="42dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="42dp"
|
||||||
android:background="@drawable/bg_creator_channel_community_price"
|
android:background="@android:color/transparent"
|
||||||
android:gravity="center"
|
android:contentDescription="@string/read_more"
|
||||||
android:orientation="horizontal"
|
android:src="@drawable/ic_seemore_vertical"
|
||||||
android:paddingHorizontal="@dimen/spacing_4"
|
|
||||||
android:paddingVertical="2dp"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="18dp"
|
|
||||||
android:layout_height="18dp"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_bar_cash" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_creator_channel_community_detail_price"
|
|
||||||
style="@style/Typography.Caption3"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:textColor="@color/black"
|
|
||||||
tools:text="300" />
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -222,22 +203,6 @@
|
|||||||
tools:text="Audio"
|
tools:text="Audio"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_creator_channel_community_detail_purchased"
|
|
||||||
style="@style/Typography.Caption2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:layout_marginTop="@dimen/spacing_8"
|
|
||||||
android:background="@drawable/bg_creator_channel_community_price"
|
|
||||||
android:includeFontPadding="false"
|
|
||||||
android:paddingHorizontal="@dimen/spacing_8"
|
|
||||||
android:paddingVertical="@dimen/spacing_4"
|
|
||||||
android:text="@string/creator_channel_community_purchased_badge"
|
|
||||||
android:textColor="@color/black"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_creator_channel_community_detail_reaction"
|
android:id="@+id/layout_creator_channel_community_detail_reaction"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -51,17 +51,30 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_12"
|
android:layout_marginTop="@dimen/spacing_12"
|
||||||
android:orientation="vertical">
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rv_home_lives"
|
android:id="@+id/rv_home_lives"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="@dimen/spacing_20"
|
android:paddingStart="@dimen/spacing_20"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/view_live_thumbnail_simple" />
|
tools:listitem="@layout/view_live_thumbnail_simple" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_home_live_all"
|
||||||
|
style="@style/Typography.Body5"
|
||||||
|
android:layout_width="@dimen/home_live_more_width"
|
||||||
|
android:layout_height="@dimen/home_live_row_height"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_20"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/screen_home_theme_all"
|
||||||
|
android:textColor="@color/soda_400" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -122,13 +122,13 @@
|
|||||||
tools:text="300" />
|
tools:text="300" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageButton
|
||||||
android:id="@+id/iv_creator_channel_community_list_owner_more"
|
android:id="@+id/btn_creator_channel_community_list_more"
|
||||||
android:layout_width="24dp"
|
android:layout_width="42dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="42dp"
|
||||||
android:layout_marginStart="@dimen/spacing_4"
|
android:background="@android:color/transparent"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@string/read_more"
|
||||||
android:src="@drawable/ic_new_more"
|
android:src="@drawable/ic_seemore_vertical"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -187,32 +187,34 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tv_creator_channel_community_list_locked_price"
|
android:id="@+id/layout_creator_channel_community_list_locked_price"
|
||||||
style="@style/Typography.Body3"
|
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:layout_marginTop="@dimen/spacing_4"
|
android:layout_marginTop="@dimen/spacing_4"
|
||||||
android:background="@drawable/bg_creator_channel_community_price"
|
android:background="@drawable/bg_creator_channel_community_price"
|
||||||
android:drawableStart="@drawable/ic_bar_cash"
|
|
||||||
android:drawablePadding="@dimen/spacing_6"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:includeFontPadding="false"
|
android:orientation="horizontal"
|
||||||
android:textColor="@color/black"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:text="30"
|
tools:visibility="visible">
|
||||||
tools:visibility="visible" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_creator_channel_community_list_play"
|
android:layout_width="18dp"
|
||||||
android:layout_width="48dp"
|
android:layout_height="18dp"
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:src="@drawable/ic_new_player_play"
|
android:src="@drawable/ic_bar_cash" />
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
<TextView
|
||||||
|
android:id="@+id/tv_creator_channel_community_list_locked_price"
|
||||||
|
style="@style/Typography.Body3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_6"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
tools:text="30" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -8,7 +8,33 @@
|
|||||||
android:padding="@dimen/spacing_14">
|
android:padding="@dimen/spacing_14">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_feed_community_notice"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_14"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_pin" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/Typography.Body5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="2dp"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:text="@string/creator_channel_community_notice"
|
||||||
|
android:textColor="@color/green_400" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="42dp"
|
android:layout_height="42dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
@@ -22,9 +48,10 @@
|
|||||||
tools:src="@drawable/ic_launcher_background" />
|
tools:src="@drawable/ic_launcher_background" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_8"
|
android:layout_marginStart="@dimen/spacing_8"
|
||||||
|
android:layout_weight="1"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -50,6 +77,16 @@
|
|||||||
android:textColor="@color/gray_500"
|
android:textColor="@color/gray_500"
|
||||||
tools:text="2분 전" />
|
tools:text="2분 전" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_feed_community_more"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/read_more"
|
||||||
|
android:src="@drawable/ic_seemore_vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -125,6 +162,7 @@
|
|||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_feed_community_reaction"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginTop="@dimen/spacing_16"
|
android:layout_marginTop="@dimen/spacing_16"
|
||||||
@@ -132,6 +170,7 @@
|
|||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/iv_feed_community_comment"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
@@ -148,6 +187,7 @@
|
|||||||
tools:text="5" />
|
tools:text="5" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/iv_feed_community_like"
|
||||||
android:layout_width="18dp"
|
android:layout_width="18dp"
|
||||||
android:layout_height="18dp"
|
android:layout_height="18dp"
|
||||||
android:layout_marginStart="15dp"
|
android:layout_marginStart="15dp"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
<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>
|
||||||
<string name="title_find_password">パスワード再設定</string>
|
<string name="title_find_password">パスワード再設定</string>
|
||||||
<string name="find_password_description_primary">登録されたメールアドレスに\n仮パスワードを送信します。</string>
|
<string name="find_password_description_primary">登録されたメールアドレスに\n仮パスワードを送信します。</string>
|
||||||
@@ -146,9 +146,9 @@
|
|||||||
<!-- Main / Home -->
|
<!-- Main / Home -->
|
||||||
<string name="tab_home">ホーム</string>
|
<string name="tab_home">ホーム</string>
|
||||||
<string name="tab_content">コンテンツ</string>
|
<string name="tab_content">コンテンツ</string>
|
||||||
<string name="tab_chat">チャット</string>
|
<string name="tab_chat">トーク</string>
|
||||||
<string name="tab_live">ライブ</string>
|
<string name="tab_live">ライブ</string>
|
||||||
<string name="tab_my">マイ</string>
|
<string name="tab_my">マイページ</string>
|
||||||
<string name="screen_chat_time_just_now">たった今</string>
|
<string name="screen_chat_time_just_now">たった今</string>
|
||||||
<string name="screen_chat_time_minutes">%1$d分前</string>
|
<string name="screen_chat_time_minutes">%1$d分前</string>
|
||||||
<string name="screen_chat_time_hours">%1$d時間前</string>
|
<string name="screen_chat_time_hours">%1$d時間前</string>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<string name="screen_chat_floating_button">新しいチャット</string>
|
<string name="screen_chat_floating_button">新しいチャット</string>
|
||||||
<string name="screen_chat_empty_all">まだ会話がありません。\nクリエイターと話してみましょう。</string>
|
<string name="screen_chat_empty_all">まだ会話がありません。\nクリエイターと話してみましょう。</string>
|
||||||
<string name="screen_chat_empty_ai">まだ会話がありません。\nAIクリエイターと話してみましょう。</string>
|
<string name="screen_chat_empty_ai">まだ会話がありません。\nAIクリエイターと話してみましょう。</string>
|
||||||
<string name="screen_chat_empty_dm">まだ会話がありません。\n好きなクリエイターにメッセージを送ってみましょう。</string>
|
<string name="screen_chat_empty_dm">まだメッセージがありません。\n好きなクリエイターにメッセージを送ってみましょう。</string>
|
||||||
<string name="screen_dm_chat_room_enter_failed">チャットルームに接続できませんでした</string>
|
<string name="screen_dm_chat_room_enter_failed">チャットルームに接続できませんでした</string>
|
||||||
<string name="live_now">On Air</string>
|
<string name="live_now">On Air</string>
|
||||||
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
|
||||||
@@ -189,24 +189,24 @@
|
|||||||
<string name="live_paid_warning">配信開始から%1$d時間%2$d分が経過しました。入室後30分以内に配信が終了する可能性があります。</string>
|
<string name="live_paid_warning">配信開始から%1$d時間%2$d分が経過しました。入室後30分以内に配信が終了する可能性があります。</string>
|
||||||
<string name="live_paid_confirm">決済して入室</string>
|
<string name="live_paid_confirm">決済して入室</string>
|
||||||
<string name="creator_channel_live_created_message">ライブが作成されました。</string>
|
<string name="creator_channel_live_created_message">ライブが作成されました。</string>
|
||||||
<string name="creator_channel_live_sort_owned">所持順</string>
|
<string name="creator_channel_live_sort_owned">保有順</string>
|
||||||
<string name="creator_channel_live_total_label">全体</string>
|
<string name="creator_channel_live_total_label">すべて</string>
|
||||||
<string name="creator_channel_live_empty_message">クリエイターがライブを準備中です。\n楽しみにお待ちください!</string>
|
<string name="creator_channel_live_empty_message">クリエイターがライブを準備しています。\nお楽しみに!</string>
|
||||||
<string name="creator_channel_live_error_message">ライブを読み込めませんでした。</string>
|
<string name="creator_channel_live_error_message">ライブを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_live_retry_button">再試行</string>
|
<string name="creator_channel_live_retry_button">再試行</string>
|
||||||
<string name="creator_channel_live_start_button">ライブを始める</string>
|
<string name="creator_channel_live_start_button">ライブを開始</string>
|
||||||
<string name="creator_channel_audio_empty_message">クリエイターがオーディオを準備中です。\n楽しみにお待ちください!</string>
|
<string name="creator_channel_audio_empty_message">クリエイターがオーディオを準備しています。\nお楽しみに!</string>
|
||||||
<string name="creator_channel_audio_error_message">オーディオを読み込めませんでした。</string>
|
<string name="creator_channel_audio_error_message">オーディオを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_audio_upload_button">オーディオを投稿</string>
|
<string name="creator_channel_audio_upload_button">オーディオを投稿</string>
|
||||||
<string name="creator_channel_audio_total_label">全体</string>
|
<string name="creator_channel_audio_total_label">すべて</string>
|
||||||
<string name="creator_channel_audio_owned_rate_message">全オーディオの%1$s%%を所持しています。</string>
|
<string name="creator_channel_audio_owned_rate_message">全オーディオの%1$s%%を所持しています。</string>
|
||||||
<string name="creator_channel_audio_owned_rate_count">%1$s/%2$s件</string>
|
<string name="creator_channel_audio_owned_rate_count">%1$s/%2$s件</string>
|
||||||
<string name="creator_channel_series_empty_message">クリエイターがシリーズを準備中です。\n楽しみにお待ちください!</string>
|
<string name="creator_channel_series_empty_message">クリエイターがシリーズを準備しています。\nお楽しみに!</string>
|
||||||
<string name="creator_channel_series_error_message">シリーズを読み込めませんでした。</string>
|
<string name="creator_channel_series_error_message">シリーズを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_series_retry_button">再試行</string>
|
<string name="creator_channel_series_retry_button">再試行</string>
|
||||||
<string name="creator_channel_series_status_proceeding">連載中</string>
|
<string name="creator_channel_series_status_proceeding">連載</string>
|
||||||
<string name="creator_channel_series_status_completed">完結</string>
|
<string name="creator_channel_series_status_completed">完結</string>
|
||||||
<string name="creator_channel_series_total_content_count">全体</string>
|
<string name="creator_channel_series_total_content_count">すべて</string>
|
||||||
<string name="creator_channel_series_subtitle_content_count">全%1$s話</string>
|
<string name="creator_channel_series_subtitle_content_count">全%1$s話</string>
|
||||||
<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>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<string name="screen_home_following">フォロー中</string>
|
<string name="screen_home_following">フォロー中</string>
|
||||||
<string name="dialog_unfollow_title">フォロー解除</string>
|
<string name="dialog_unfollow_title">フォロー解除</string>
|
||||||
<string name="dialog_unfollow_message">%1$sさんのフォロー를解除しますか?</string>
|
<string name="dialog_unfollow_message">%1$sさんのフォロー를解除しますか?</string>
|
||||||
<string name="screen_home_theme_all">全</string>
|
<string name="screen_home_theme_all">すべて</string>
|
||||||
<string name="screen_push_notification_title">通知</string>
|
<string name="screen_push_notification_title">通知</string>
|
||||||
<string name="screen_push_notification_empty">通知はありません。</string>
|
<string name="screen_push_notification_empty">通知はありません。</string>
|
||||||
<string name="screen_home_sort_revenue">売上</string>
|
<string name="screen_home_sort_revenue">売上</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">VOICE ON限定</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&Hot</string>
|
<string name="content_recommendation_section_new_and_hot">New&Hot</string>
|
||||||
<string name="content_recommendation_section_free_audio">無料オーディオ</string>
|
<string name="content_recommendation_section_free_audio">無料オーディオ</string>
|
||||||
@@ -304,18 +304,18 @@
|
|||||||
<string name="content_recommendation_section_recommended_audio">おすすめオーディオ</string>
|
<string name="content_recommendation_section_recommended_audio">おすすめオーディオ</string>
|
||||||
<string name="home_recommendation_section_live">ライブ</string>
|
<string name="home_recommendation_section_live">ライブ</string>
|
||||||
<string name="home_recommendation_section_recently_active_creators">最近活動したクリエイター</string>
|
<string name="home_recommendation_section_recently_active_creators">最近活動したクリエイター</string>
|
||||||
<string name="home_recommendation_section_recent_debut_creators">最近デビューしたクリエイター</string>
|
<string name="home_recommendation_section_recent_debut_creators">新着クリエイター</string>
|
||||||
<string name="home_recommendation_section_ai_characters">クリエイターと話しましょう!</string>
|
<string name="home_recommendation_section_ai_characters">クリエイターと話す</string>
|
||||||
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
|
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
|
||||||
<string name="home_recommendation_section_cheer_creators">最近応援が多いクリエイター</string>
|
<string name="home_recommendation_section_cheer_creators">最近応援の多いクリエイター</string>
|
||||||
<string name="home_recommendation_section_popular_community_posts">人気コミュニティ</string>
|
<string name="home_recommendation_section_popular_community_posts">人気コミュニティ</string>
|
||||||
<string name="screen_home_following_recent_chats_title">最近のチャット</string>
|
<string name="screen_home_following_recent_chats_title">最近のトーク</string>
|
||||||
<string name="screen_home_following_monthly_schedules_title">今月のスケジュール</string>
|
<string name="screen_home_following_monthly_schedules_title">今月のスケジュール</string>
|
||||||
<string name="screen_home_following_recent_news_title">最近のお知らせ</string>
|
<string name="screen_home_following_recent_news_title">最新情報</string>
|
||||||
<string name="screen_home_following_on_air">On Air</string>
|
<string name="screen_home_following_on_air">On Air</string>
|
||||||
<string name="screen_home_following_photo_content">グラビア</string>
|
<string name="screen_home_following_photo_content">フォト</string>
|
||||||
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
|
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
|
||||||
<string name="screen_home_following_creator_ranking_news_message">%1$sさんが今週のランキング%2$sに入りました!</string>
|
<string name="screen_home_following_creator_ranking_news_message">%1$sが今週のランキングで%2$sにランクイン!</string>
|
||||||
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
||||||
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</string>
|
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</string>
|
||||||
<string name="home_recommendation_activity_live">ライブ</string>
|
<string name="home_recommendation_activity_live">ライブ</string>
|
||||||
@@ -337,8 +337,8 @@
|
|||||||
<string name="creator_channel_tab_audio">オーディオ</string>
|
<string name="creator_channel_tab_audio">オーディオ</string>
|
||||||
<string name="creator_channel_tab_series">シリーズ</string>
|
<string name="creator_channel_tab_series">シリーズ</string>
|
||||||
<string name="creator_channel_tab_community">コミュニティ</string>
|
<string name="creator_channel_tab_community">コミュニティ</string>
|
||||||
<string name="creator_channel_tab_fantalk">ファンTalk</string>
|
<string name="creator_channel_tab_fantalk">ファントーク</string>
|
||||||
<string name="creator_channel_tab_donation">応援</string>
|
<string name="creator_channel_tab_donation">サポート</string>
|
||||||
<string name="creator_channel_follower_count">フォロワー %1$s人</string>
|
<string name="creator_channel_follower_count">フォロワー %1$s人</string>
|
||||||
<string name="creator_channel_chat_button">トークする</string>
|
<string name="creator_channel_chat_button">トークする</string>
|
||||||
<string name="creator_channel_dm_button">DMを送る</string>
|
<string name="creator_channel_dm_button">DMを送る</string>
|
||||||
@@ -348,63 +348,63 @@
|
|||||||
<string name="creator_channel_block_success">ブロックしました。</string>
|
<string name="creator_channel_block_success">ブロックしました。</string>
|
||||||
<string name="creator_channel_section_live">配信中</string>
|
<string name="creator_channel_section_live">配信中</string>
|
||||||
<string name="creator_channel_section_latest_audio">最新オーディオ</string>
|
<string name="creator_channel_section_latest_audio">最新オーディオ</string>
|
||||||
<string name="creator_channel_section_donation">応援</string>
|
<string name="creator_channel_section_donation">サポート</string>
|
||||||
<string name="creator_channel_section_notice">お知らせ</string>
|
<string name="creator_channel_section_notice">お知らせ</string>
|
||||||
<string name="creator_channel_section_schedule">予定</string>
|
<string name="creator_channel_section_schedule">予定</string>
|
||||||
<string name="creator_channel_section_audio">オーディオ</string>
|
<string name="creator_channel_section_audio">オーディオ</string>
|
||||||
<string name="creator_channel_section_series">シリーズ</string>
|
<string name="creator_channel_section_series">シリーズ</string>
|
||||||
<string name="creator_channel_section_community">コミュニティ</string>
|
<string name="creator_channel_section_community">コミュニティ</string>
|
||||||
<string name="creator_channel_section_fantalk">ファンTalk</string>
|
<string name="creator_channel_section_fantalk">ファントーク</string>
|
||||||
<string name="creator_channel_section_introduce">紹介</string>
|
<string name="creator_channel_section_introduce">紹介</string>
|
||||||
<string name="creator_channel_section_activity">活動</string>
|
<string name="creator_channel_section_activity">アクティビティ</string>
|
||||||
<string name="creator_channel_section_sns">SNS</string>
|
<string name="creator_channel_section_sns">SNS</string>
|
||||||
<string name="creator_channel_series_summary">%1$d件 · %2$s</string>
|
<string name="creator_channel_series_summary">%1$d件 · %2$s</string>
|
||||||
<string name="creator_channel_community_summary">%1$s · いいね %2$d · コメント %3$d</string>
|
<string name="creator_channel_community_summary">%1$s · いいね %2$d · コメント %3$d</string>
|
||||||
<string name="creator_channel_community_view_mode_list">リスト型</string>
|
<string name="creator_channel_community_view_mode_list">リスト</string>
|
||||||
<string name="creator_channel_community_view_mode_grid">サムネイル型</string>
|
<string name="creator_channel_community_view_mode_grid">グリッド</string>
|
||||||
<string name="creator_channel_community_empty_message">クリエイターがコミュニティを準備中です。\n楽しみにお待ちください!</string>
|
<string name="creator_channel_community_empty_message">クリエイターがコミュニティを準備しています。\nお楽しみに!</string>
|
||||||
<string name="creator_channel_community_error_message">コミュニティを読み込めませんでした。</string>
|
<string name="creator_channel_community_error_message">コミュニティを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_community_retry_button">再試行</string>
|
<string name="creator_channel_community_retry_button">再試行</string>
|
||||||
<string name="creator_channel_community_notice">お知らせ</string>
|
<string name="creator_channel_community_notice">お知らせ</string>
|
||||||
<string name="creator_channel_community_detail_title">コミュニティ</string>
|
<string name="creator_channel_community_detail_title">コミュニティ</string>
|
||||||
<string name="creator_channel_community_reply_title">返信</string>
|
<string name="creator_channel_community_reply_title">返信</string>
|
||||||
<string name="creator_channel_community_comment_placeholder">コメントを入力してください</string>
|
<string name="creator_channel_community_comment_placeholder">コメントを入力</string>
|
||||||
<string name="creator_channel_community_reply_placeholder">返信を入力してください</string>
|
<string name="creator_channel_community_reply_placeholder">返信を入力</string>
|
||||||
<string name="creator_channel_community_comment_header">コメント %1$s</string>
|
<string name="creator_channel_community_comment_header">コメント %1$s</string>
|
||||||
<string name="creator_channel_community_comment_unavailable">この投稿ではコメントを利用できません。</string>
|
<string name="creator_channel_community_comment_unavailable">この投稿ではコメントを利用できません。</string>
|
||||||
<string name="creator_channel_community_invalid_post">投稿を読み込めません。</string>
|
<string name="creator_channel_community_invalid_post">投稿を読み込めません。</string>
|
||||||
<string name="creator_channel_community_purchased_badge">購入済み</string>
|
<string name="creator_channel_community_purchased_badge">購入済み</string>
|
||||||
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
|
||||||
<string name="creator_channel_fantalk_empty_title">最初の応援を\n待っています!</string>
|
<string name="creator_channel_fantalk_empty_title">最初の応援を\n待っています!</string>
|
||||||
<string name="creator_channel_fantalk_support_action">応援を残す</string>
|
<string name="creator_channel_fantalk_support_action">ファンレターを送る</string>
|
||||||
<string name="creator_channel_fantalk_empty_message">まだ応援がありません。\n最初にクリエイターを応援してみましょう!</string>
|
<string name="creator_channel_fantalk_empty_message">まだファンレターがありません。\n最初のファンレターを送ってみましょう!</string>
|
||||||
<string name="creator_channel_fantalk_error_message">ファントークを読み込めませんでした。</string>
|
<string name="creator_channel_fantalk_error_message">ファントークを読み込めませんでした。</string>
|
||||||
<string name="creator_channel_fantalk_retry">再試行</string>
|
<string name="creator_channel_fantalk_retry">再試行</string>
|
||||||
<string name="creator_channel_fantalk_all_label">全体</string>
|
<string name="creator_channel_fantalk_all_label">すべて</string>
|
||||||
<string name="creator_channel_fantalk_report">通報</string>
|
<string name="creator_channel_fantalk_report">通報</string>
|
||||||
<string name="creator_channel_fantalk_edit">編集する</string>
|
<string name="creator_channel_fantalk_edit">編集</string>
|
||||||
<string name="creator_channel_fantalk_delete">削除する</string>
|
<string name="creator_channel_fantalk_delete">削除</string>
|
||||||
<string name="creator_channel_activity_debut">デビュー</string>
|
<string name="creator_channel_activity_debut">デビュー</string>
|
||||||
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
<string name="creator_channel_activity_debut_format">%1$s(%2$s)</string>
|
||||||
<string name="creator_channel_activity_live_count">ライブ総配信数</string>
|
<string name="creator_channel_activity_live_count">ライブ配信回数</string>
|
||||||
<string name="creator_channel_activity_live_count_format">%1$d回</string>
|
<string name="creator_channel_activity_live_count_format">%1$d回</string>
|
||||||
<string name="creator_channel_activity_live_duration">ライブ累計時間</string>
|
<string name="creator_channel_activity_live_duration">ライブ累計時間</string>
|
||||||
<string name="creator_channel_activity_live_duration_format">%1$d時間</string>
|
<string name="creator_channel_activity_live_duration_format">%1$d時間</string>
|
||||||
<string name="creator_channel_activity_live_contributor">ライブ累計参加者</string>
|
<string name="creator_channel_activity_live_contributor">ライブ累計参加者数</string>
|
||||||
<string name="creator_channel_activity_live_contributor_format">%1$d人</string>
|
<string name="creator_channel_activity_live_contributor_format">%1$d人</string>
|
||||||
<string name="creator_channel_activity_audio_count">オーディオ数</string>
|
<string name="creator_channel_activity_audio_count">オーディオ数</string>
|
||||||
<string name="creator_channel_activity_audio_count_format">%1$d件</string>
|
<string name="creator_channel_activity_audio_count_format">%1$d件</string>
|
||||||
<string name="creator_channel_activity_series_count">シリーズ数</string>
|
<string name="creator_channel_activity_series_count">シリーズ数</string>
|
||||||
<string name="creator_channel_activity_series_count_format">%1$d件</string>
|
<string name="creator_channel_activity_series_count_format">%1$d件</string>
|
||||||
<string name="creator_channel_more_ready">準備中です。</string>
|
<string name="creator_channel_more_ready">準備中です。</string>
|
||||||
<string name="creator_channel_donation_button">応援する</string>
|
<string name="creator_channel_donation_button">サポートする</string>
|
||||||
<string name="creator_channel_donation_all_label">すべて</string>
|
<string name="creator_channel_donation_all_label">すべて</string>
|
||||||
<string name="creator_channel_donation_ranking_title">応援ランキング</string>
|
<string name="creator_channel_donation_ranking_title">サポートランキング</string>
|
||||||
<string name="creator_channel_donation_ranking_all">すべて見る</string>
|
<string name="creator_channel_donation_ranking_all">すべて見る</string>
|
||||||
<string name="creator_channel_donation_action">応援する</string>
|
<string name="creator_channel_donation_action">サポートする</string>
|
||||||
<string name="creator_channel_donation_error_message">応援履歴を読み込めませんでした。</string>
|
<string name="creator_channel_donation_error_message">応援履歴を読み込めませんでした。</string>
|
||||||
<string name="creator_channel_donation_retry">再試行</string>
|
<string name="creator_channel_donation_retry">再試行</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_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>
|
||||||
@@ -478,8 +478,8 @@
|
|||||||
<string name="creator_channel_fantalk_write_send">送信</string>
|
<string name="creator_channel_fantalk_write_send">送信</string>
|
||||||
<string name="creator_channel_fantalk_write_sending">送信中</string>
|
<string name="creator_channel_fantalk_write_sending">送信中</string>
|
||||||
<string name="creator_channel_fantalk_write_exit_title">入力終了</string>
|
<string name="creator_channel_fantalk_write_exit_title">入力終了</string>
|
||||||
<string name="creator_channel_fantalk_write_exit_desc">入力を終了しますか?\n今離れると、入力した内容は保存されません。</string>
|
<string name="creator_channel_fantalk_write_exit_desc">入力を終了しますか?\n終了すると、入力内容は保存されません。</string>
|
||||||
<string name="creator_channel_fantalk_write_exit_cancel">続けて書く</string>
|
<string name="creator_channel_fantalk_write_exit_cancel">続ける</string>
|
||||||
<string name="creator_channel_fantalk_write_exit_confirm">終了</string>
|
<string name="creator_channel_fantalk_write_exit_confirm">終了</string>
|
||||||
<string name="character_comment_load_failed">@string/common_error_unknown</string>
|
<string name="character_comment_load_failed">@string/common_error_unknown</string>
|
||||||
<string name="character_comment_report_reason_commercial">望まない商業コンテンツまたはスパム</string>
|
<string name="character_comment_report_reason_commercial">望まない商業コンテンツまたはスパム</string>
|
||||||
@@ -504,7 +504,7 @@
|
|||||||
<string name="screen_character_tab_recommend_title">おすすめキャラ</string>
|
<string name="screen_character_tab_recommend_title">おすすめキャラ</string>
|
||||||
<string name="screen_character_tab_new_badge">N</string>
|
<string name="screen_character_tab_new_badge">N</string>
|
||||||
<string name="screen_new_characters_all_title">新着キャラ一覧</string>
|
<string name="screen_new_characters_all_title">新着キャラ一覧</string>
|
||||||
<string name="screen_new_characters_all_total_label">全</string>
|
<string name="screen_new_characters_all_total_label">すべて</string>
|
||||||
<string name="screen_new_characters_all_total_unit">個</string>
|
<string name="screen_new_characters_all_total_unit">個</string>
|
||||||
<string name="screen_chat_tab_character">キャラクター</string>
|
<string name="screen_chat_tab_character">キャラクター</string>
|
||||||
<string name="screen_chat_tab_original">作品別</string>
|
<string name="screen_chat_tab_original">作品別</string>
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
<string name="screen_message_notice_autodelete">※ 保存していない受信メッセージは3日後に自動削除されます。</string>
|
<string name="screen_message_notice_autodelete">※ 保存していない受信メッセージは3日後に自動削除されます。</string>
|
||||||
<string name="screen_message_filter_receive">受信メッセージ</string>
|
<string name="screen_message_filter_receive">受信メッセージ</string>
|
||||||
<string name="screen_message_filter_sent">送信メッセージ</string>
|
<string name="screen_message_filter_sent">送信メッセージ</string>
|
||||||
<string name="screen_message_filter_keep">コレクション</string>
|
<string name="screen_message_filter_keep">ライブラリ</string>
|
||||||
<string name="screen_message_empty">メッセージがありません。\n友達と交流してみましょう!</string>
|
<string name="screen_message_empty">メッセージがありません。\n友達と交流してみましょう!</string>
|
||||||
<string name="screen_message_loading_text">テキストメッセージを読み込み中。</string>
|
<string name="screen_message_loading_text">テキストメッセージを読み込み中。</string>
|
||||||
<string name="screen_message_loading_voice">ボイスメッセージを読み込み中。</string>
|
<string name="screen_message_loading_voice">ボイスメッセージを読み込み中。</string>
|
||||||
@@ -663,7 +663,7 @@
|
|||||||
<string name="screen_live_room_v2v_signature_off_label">字幕 OFF</string>
|
<string name="screen_live_room_v2v_signature_off_label">字幕 OFF</string>
|
||||||
<string name="screen_live_room_v2v_signature_on_label">字幕 ON</string>
|
<string name="screen_live_room_v2v_signature_on_label">字幕 ON</string>
|
||||||
<string name="screen_live_room_bg_off_label">背景 OFF</string>
|
<string name="screen_live_room_bg_off_label">背景 OFF</string>
|
||||||
<string name="screen_live_room_notice">告知</string>
|
<string name="screen_live_room_notice">お知らせ</string>
|
||||||
<string name="screen_live_room_menu">メニュー表</string>
|
<string name="screen_live_room_menu">メニュー表</string>
|
||||||
<string name="screen_live_room_participants">リスナー</string>
|
<string name="screen_live_room_participants">リスナー</string>
|
||||||
<string name="screen_live_room_new_chat">新しいチャット</string>
|
<string name="screen_live_room_new_chat">新しいチャット</string>
|
||||||
@@ -716,7 +716,7 @@
|
|||||||
<string name="screen_live_room_age_badge">R-18</string>
|
<string name="screen_live_room_age_badge">R-18</string>
|
||||||
<string name="screen_live_room_heart_ranking_title">現在のライブ・ハートランキング</string>
|
<string name="screen_live_room_heart_ranking_title">現在のライブ・ハートランキング</string>
|
||||||
<string name="screen_live_room_total_label">合計</string>
|
<string name="screen_live_room_total_label">合計</string>
|
||||||
<string name="screen_live_room_total_count_label">全</string>
|
<string name="screen_live_room_total_count_label">すべて</string>
|
||||||
<string name="screen_live_room_people_unit">名</string>
|
<string name="screen_live_room_people_unit">名</string>
|
||||||
<string name="screen_live_room_donation_ranking_title">現在のライブ・ギフトランキング</string>
|
<string name="screen_live_room_donation_ranking_title">現在のライブ・ギフトランキング</string>
|
||||||
<string name="screen_live_room_donation_normal_label">一般</string>
|
<string name="screen_live_room_donation_normal_label">一般</string>
|
||||||
@@ -729,7 +729,7 @@
|
|||||||
<string name="screen_live_room_donation_history_title">ギフト履歴</string>
|
<string name="screen_live_room_donation_history_title">ギフト履歴</string>
|
||||||
<string name="screen_live_room_donation_history_empty">ギフト履歴がありません。</string>
|
<string name="screen_live_room_donation_history_empty">ギフト履歴がありません。</string>
|
||||||
<string name="screen_live_room_close">閉じる</string>
|
<string name="screen_live_room_close">閉じる</string>
|
||||||
<string name="screen_live_room_donation_title">ギフトする</string>
|
<string name="screen_live_room_donation_title">サポートする</string>
|
||||||
<string name="screen_live_room_charge">チャージ</string>
|
<string name="screen_live_room_charge">チャージ</string>
|
||||||
<string name="screen_live_room_secret_mission">シークレットミッション</string>
|
<string name="screen_live_room_secret_mission">シークレットミッション</string>
|
||||||
<string name="screen_live_room_donation_plus_10">10</string>
|
<string name="screen_live_room_donation_plus_10">10</string>
|
||||||
@@ -756,7 +756,7 @@
|
|||||||
<string name="screen_live_room_menu_preset_3">メニュー 3</string>
|
<string name="screen_live_room_menu_preset_3">メニュー 3</string>
|
||||||
<string name="screen_live_room_menu_hint">メニュー表を作成してください。</string>
|
<string name="screen_live_room_menu_hint">メニュー表を作成してください。</string>
|
||||||
<string name="screen_live_room_entry_message_label">入室メッセージ</string>
|
<string name="screen_live_room_entry_message_label">入室メッセージ</string>
|
||||||
<string name="screen_live_room_update_action">修正する</string>
|
<string name="screen_live_room_update_action">編集</string>
|
||||||
<string name="screen_live_room_menu_first_required">メニュー1を先に設定してください</string>
|
<string name="screen_live_room_menu_first_required">メニュー1を先に設定してください</string>
|
||||||
<string name="screen_live_room_menu_first_second_required">メニュー1と2を先に設定してください</string>
|
<string name="screen_live_room_menu_first_second_required">メニュー1と2を先に設定してください</string>
|
||||||
<string name="screen_live_room_cancel_reason_required">キャンセル理由を入力してください。</string>
|
<string name="screen_live_room_cancel_reason_required">キャンセル理由を入力してください。</string>
|
||||||
@@ -831,7 +831,7 @@
|
|||||||
<string name="screen_live_room_create_age_label">年齢制限</string>
|
<string name="screen_live_room_create_age_label">年齢制限</string>
|
||||||
<string name="screen_live_room_create_age_all">全年齢</string>
|
<string name="screen_live_room_create_age_all">全年齢</string>
|
||||||
<string name="screen_live_room_create_gender_restriction_label">性別制限</string>
|
<string name="screen_live_room_create_gender_restriction_label">性別制限</string>
|
||||||
<string name="screen_live_room_create_gender_restriction_all">全体</string>
|
<string name="screen_live_room_create_gender_restriction_all">すべて</string>
|
||||||
<string name="screen_live_room_create_gender_restriction_male_only">男性のみ</string>
|
<string name="screen_live_room_create_gender_restriction_male_only">男性のみ</string>
|
||||||
<string name="screen_live_room_create_gender_restriction_female_only">女性のみ</string>
|
<string name="screen_live_room_create_gender_restriction_female_only">女性のみ</string>
|
||||||
<string name="screen_live_room_create_price_label">チケット価格</string>
|
<string name="screen_live_room_create_price_label">チケット価格</string>
|
||||||
@@ -930,7 +930,7 @@
|
|||||||
<string name="screen_can_status_label_paid">決済CAN</string>
|
<string name="screen_can_status_label_paid">決済CAN</string>
|
||||||
<string name="screen_can_status_label_reward">リワードCAN</string>
|
<string name="screen_can_status_label_reward">リワードCAN</string>
|
||||||
<string name="screen_can_status_unit">CAN</string>
|
<string name="screen_can_status_unit">CAN</string>
|
||||||
<string name="screen_can_status_charge_action">チャージする</string>
|
<string name="screen_can_status_charge_action">チャージ</string>
|
||||||
<!-- Can Charge -->
|
<!-- Can Charge -->
|
||||||
<string name="screen_can_charge_title">チャージ</string>
|
<string name="screen_can_charge_title">チャージ</string>
|
||||||
<string name="screen_can_charge_tab_pg">PG</string>
|
<string name="screen_can_charge_tab_pg">PG</string>
|
||||||
@@ -973,7 +973,7 @@
|
|||||||
<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>
|
||||||
<!-- Block Member -->
|
<!-- Block Member -->
|
||||||
<string name="screen_block_member_title">ブロックリスト</string>
|
<string name="screen_block_member_title">ブロックリスト</string>
|
||||||
@@ -992,7 +992,7 @@
|
|||||||
<string name="screen_creator_detail_debut_before">デビュー前</string>
|
<string name="screen_creator_detail_debut_before">デビュー前</string>
|
||||||
<string name="screen_creator_detail_live_count">ライブ総回数</string>
|
<string name="screen_creator_detail_live_count">ライブ総回数</string>
|
||||||
<string name="screen_creator_detail_live_time">ライブ累積時間</string>
|
<string name="screen_creator_detail_live_time">ライブ累積時間</string>
|
||||||
<string name="screen_creator_detail_live_contributor_count">ライブ累積参加者</string>
|
<string name="screen_creator_detail_live_contributor_count">ライブ累計参加者数</string>
|
||||||
<string name="screen_creator_detail_content_count">登録コンテンツ数</string>
|
<string name="screen_creator_detail_content_count">登録コンテンツ数</string>
|
||||||
<string name="screen_creator_detail_sns">SNS</string>
|
<string name="screen_creator_detail_sns">SNS</string>
|
||||||
<string name="screen_user_profile_latest_content_scheduled">公開予定</string>
|
<string name="screen_user_profile_latest_content_scheduled">公開予定</string>
|
||||||
@@ -1025,7 +1025,7 @@
|
|||||||
<string name="screen_user_profile_reply_write">返信を書く</string>
|
<string name="screen_user_profile_reply_write">返信を書く</string>
|
||||||
<string name="creator_channel_fantalk_reply_replace_description">既存の返信が新しい返信に置き換えられます。\n修正しますか?</string>
|
<string name="creator_channel_fantalk_reply_replace_description">既存の返信が新しい返信に置き換えられます。\n修正しますか?</string>
|
||||||
<string name="screen_user_follower_list_title">フォロワーリスト</string>
|
<string name="screen_user_follower_list_title">フォロワーリスト</string>
|
||||||
<string name="screen_user_follower_list_total_label">全</string>
|
<string name="screen_user_follower_list_total_label">すべて</string>
|
||||||
<string name="screen_user_follower_list_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
<string name="screen_user_follower_list_error_invalid_request">無効なリクエストです。\n恐れ入りますが、もう一度お試しください。</string>
|
||||||
<string name="screen_user_profile_series_title">シリーズ</string>
|
<string name="screen_user_profile_series_title">シリーズ</string>
|
||||||
<string name="screen_user_profile_series_total_count">全%1$d話</string>
|
<string name="screen_user_profile_series_total_count">全%1$d話</string>
|
||||||
@@ -1038,7 +1038,7 @@
|
|||||||
<string name="screen_user_profile_donation_stat_this_month">今月(昨日まで)</string>
|
<string name="screen_user_profile_donation_stat_this_month">今月(昨日まで)</string>
|
||||||
<string name="screen_user_profile_donation_can_unit">CAN</string>
|
<string name="screen_user_profile_donation_can_unit">CAN</string>
|
||||||
<string name="screen_user_profile_donation_can_unit_with_space">\u0020CAN</string>
|
<string name="screen_user_profile_donation_can_unit_with_space">\u0020CAN</string>
|
||||||
<string name="screen_user_profile_donation_total_label">全</string>
|
<string name="screen_user_profile_donation_total_label">すべて</string>
|
||||||
<string name="screen_user_profile_donation_total_unit">個</string>
|
<string name="screen_user_profile_donation_total_unit">個</string>
|
||||||
<string name="screen_user_profile_donation_total_can_format">%1$s %2$s</string>
|
<string name="screen_user_profile_donation_total_can_format">%1$s %2$s</string>
|
||||||
<string name="screen_user_profile_donation_period_weekly">週間</string>
|
<string name="screen_user_profile_donation_period_weekly">週間</string>
|
||||||
@@ -1083,7 +1083,7 @@
|
|||||||
<string name="screen_creator_community_modify_title">投稿修正</string>
|
<string name="screen_creator_community_modify_title">投稿修正</string>
|
||||||
<string name="screen_creator_community_modify_submit">修正</string>
|
<string name="screen_creator_community_modify_submit">修正</string>
|
||||||
<string name="dialog_creator_follow_notify_title">通知</string>
|
<string name="dialog_creator_follow_notify_title">通知</string>
|
||||||
<string name="dialog_creator_follow_notify_all">全</string>
|
<string name="dialog_creator_follow_notify_all">すべて</string>
|
||||||
<string name="dialog_creator_follow_notify_none">なし</string>
|
<string name="dialog_creator_follow_notify_none">なし</string>
|
||||||
<string name="dialog_creator_follow_unfollow">フォロー解除</string>
|
<string name="dialog_creator_follow_unfollow">フォロー解除</string>
|
||||||
<string name="screen_user_profile_reservation_title">%1$sCANで予約</string>
|
<string name="screen_user_profile_reservation_title">%1$sCANで予約</string>
|
||||||
@@ -1095,7 +1095,7 @@
|
|||||||
<string name="screen_my_charge_can">CANチャージ</string>
|
<string name="screen_my_charge_can">CANチャージ</string>
|
||||||
<string name="screen_my_point_reward">ポイントリワード</string>
|
<string name="screen_my_point_reward">ポイントリワード</string>
|
||||||
<string name="screen_my_recent_prefix">最近聴いた</string>
|
<string name="screen_my_recent_prefix">最近聴いた</string>
|
||||||
<string name="screen_my_storage">コレクション</string>
|
<string name="screen_my_storage">ライブラリ</string>
|
||||||
<string name="screen_my_block_list">ブロックリスト</string>
|
<string name="screen_my_block_list">ブロックリスト</string>
|
||||||
<string name="screen_my_morning_call">モーニングコール</string>
|
<string name="screen_my_morning_call">モーニングコール</string>
|
||||||
<string name="screen_my_notice">お知らせ</string>
|
<string name="screen_my_notice">お知らせ</string>
|
||||||
@@ -1191,7 +1191,7 @@
|
|||||||
<string name="dialog_sensitive_content_enable_title">あなたは18歳以上ですか?</string>
|
<string name="dialog_sensitive_content_enable_title">あなたは18歳以上ですか?</string>
|
||||||
<string name="dialog_sensitive_content_enable_message">このコンテンツは18歳以上のみ利用できます!</string>
|
<string name="dialog_sensitive_content_enable_message">このコンテンツは18歳以上のみ利用できます!</string>
|
||||||
<string name="screen_content_settings_sensitive_content_guide">センシティブなコンテンツを表示するには「センシティブなコンテンツ表示」スイッチをオンにしてください。</string>
|
<string name="screen_content_settings_sensitive_content_guide">センシティブなコンテンツを表示するには「センシティブなコンテンツ表示」スイッチをオンにしてください。</string>
|
||||||
<string name="screen_content_settings_all">全</string>
|
<string name="screen_content_settings_all">すべて</string>
|
||||||
<string name="screen_content_settings_male">男性向け</string>
|
<string name="screen_content_settings_male">男性向け</string>
|
||||||
<string name="screen_content_settings_female">女性向け</string>
|
<string name="screen_content_settings_female">女性向け</string>
|
||||||
<string name="screen_notification_settings_title">通知設定</string>
|
<string name="screen_notification_settings_title">通知設定</string>
|
||||||
@@ -1314,8 +1314,8 @@
|
|||||||
<string name="audio_content_upload_select_audio_title">オーディオ選択</string>
|
<string name="audio_content_upload_select_audio_title">オーディオ選択</string>
|
||||||
<string name="screen_audio_content_new_content">新しいコンテンツを投稿する</string>
|
<string name="screen_audio_content_new_content">新しいコンテンツを投稿する</string>
|
||||||
<string name="screen_audio_content_sort_newest">最新順</string>
|
<string name="screen_audio_content_sort_newest">最新順</string>
|
||||||
<string name="screen_audio_content_sort_price_high">価格が高い順</string>
|
<string name="screen_audio_content_sort_price_high">高い順</string>
|
||||||
<string name="screen_audio_content_sort_price_low">価格が低い順</string>
|
<string name="screen_audio_content_sort_price_low">安い順</string>
|
||||||
<string name="screen_audio_content_sort_popularity">人気順</string>
|
<string name="screen_audio_content_sort_popularity">人気順</string>
|
||||||
<string name="screen_audio_content_empty">表示するコンテンツがありません。</string>
|
<string name="screen_audio_content_empty">表示するコンテンツがありません。</string>
|
||||||
<string name="screen_audio_content_box_title">Myライブラリ</string>
|
<string name="screen_audio_content_box_title">Myライブラリ</string>
|
||||||
@@ -1335,13 +1335,13 @@
|
|||||||
<string name="audio_content_order_confirm_notice_rental_simple">コンテンツをレンタルしますか?</string>
|
<string name="audio_content_order_confirm_notice_rental_simple">コンテンツをレンタルしますか?</string>
|
||||||
<string name="audio_content_order_confirm_notice_keep_simple">コンテンツを購入しますか?</string>
|
<string name="audio_content_order_confirm_notice_keep_simple">コンテンツを購入しますか?</string>
|
||||||
<string name="audio_content_order_plus">+</string>
|
<string name="audio_content_order_plus">+</string>
|
||||||
<string name="audio_content_label_all">全</string>
|
<string name="audio_content_label_all">すべて</string>
|
||||||
<string name="audio_content_total_unit">個</string>
|
<string name="audio_content_total_unit">個</string>
|
||||||
<string name="audio_content_price_free">無料</string>
|
<string name="audio_content_price_free">無料</string>
|
||||||
<string name="audio_content_tag_free">無料</string>
|
<string name="audio_content_tag_free">無料</string>
|
||||||
<string name="audio_content_badge_scheduled">公開予定</string>
|
<string name="audio_content_badge_scheduled">公開予定</string>
|
||||||
<string name="audio_content_badge_point">ポイント</string>
|
<string name="audio_content_badge_point">ポイント</string>
|
||||||
<string name="audio_content_badge_owned">所持中</string>
|
<string name="audio_content_badge_owned">保有中</string>
|
||||||
<string name="audio_content_badge_rented">レンタル中</string>
|
<string name="audio_content_badge_rented">レンタル中</string>
|
||||||
<string name="audio_content_badge_sold_out">Sold Out</string>
|
<string name="audio_content_badge_sold_out">Sold Out</string>
|
||||||
<string name="audio_content_playlist_create">+ 新しいプレイリストを作成</string>
|
<string name="audio_content_playlist_create">+ 新しいプレイリストを作成</string>
|
||||||
@@ -1400,7 +1400,7 @@
|
|||||||
<string name="screen_audio_content_detail_next">次の話</string>
|
<string name="screen_audio_content_detail_next">次の話</string>
|
||||||
<string name="screen_audio_content_detail_share_title">オーディオ共有</string>
|
<string name="screen_audio_content_detail_share_title">オーディオ共有</string>
|
||||||
<string name="screen_audio_content_detail_share">共有</string>
|
<string name="screen_audio_content_detail_share">共有</string>
|
||||||
<string name="screen_audio_content_detail_donation">ギフト</string>
|
<string name="screen_audio_content_detail_donation">サポート</string>
|
||||||
<string name="screen_audio_content_detail_limited_edition">限定版</string>
|
<string name="screen_audio_content_detail_limited_edition">限定版</string>
|
||||||
<string name="screen_audio_content_detail_remaining">残り数量</string>
|
<string name="screen_audio_content_detail_remaining">残り数量</string>
|
||||||
<string name="screen_audio_content_detail_buyer_title">購入者</string>
|
<string name="screen_audio_content_detail_buyer_title">購入者</string>
|
||||||
@@ -1481,7 +1481,7 @@
|
|||||||
<string name="alarm_add_calendar_content_description">日付選択</string>
|
<string name="alarm_add_calendar_content_description">日付選択</string>
|
||||||
<string name="alarm_select_title">コンテンツ選択</string>
|
<string name="alarm_select_title">コンテンツ選択</string>
|
||||||
<string name="alarm_select_empty">所持中のコンテンツがありません。\n先にマーケットで購入してください。</string>
|
<string name="alarm_select_empty">所持中のコンテンツがありません。\n先にマーケットで購入してください。</string>
|
||||||
<string name="alarm_select_badge_owned">所持中</string>
|
<string name="alarm_select_badge_owned">保有中</string>
|
||||||
<string name="alarm_select_badge_rental">レンタル中</string>
|
<string name="alarm_select_badge_rental">レンタル中</string>
|
||||||
|
|
||||||
<!-- 追加翻訳 -->
|
<!-- 追加翻訳 -->
|
||||||
@@ -1505,7 +1505,7 @@
|
|||||||
<string name="audio_content_home_content_title">人気短編</string>
|
<string name="audio_content_home_content_title">人気短編</string>
|
||||||
<string name="audio_content_home_creator_title">人気クリエイター</string>
|
<string name="audio_content_home_creator_title">人気クリエイター</string>
|
||||||
<string name="audio_content_home_more">もっと見る ></string>
|
<string name="audio_content_home_more">もっと見る ></string>
|
||||||
<string name="audio_content_home_search_placeholder">検索ワードを入力</string>
|
<string name="audio_content_home_search_placeholder">キーワードを入力</string>
|
||||||
<string name="audio_content_home_series_title">人気シリーズ</string>
|
<string name="audio_content_home_series_title">人気シリーズ</string>
|
||||||
<string name="audio_content_home_tab_alarm">モーニングコール</string>
|
<string name="audio_content_home_tab_alarm">モーニングコール</string>
|
||||||
<string name="audio_content_home_tab_asmr">ASMR</string>
|
<string name="audio_content_home_tab_asmr">ASMR</string>
|
||||||
|
|||||||
@@ -44,13 +44,4 @@ class CommunityChangeTest {
|
|||||||
|
|
||||||
assertEquals(List(CommunityActivityResultSource.entries.size) { CommunityChange.Ignored }, changes)
|
assertEquals(List(CommunityActivityResultSource.entries.size) { CommunityChange.Ignored }, changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `삭제와 고정 변경은 post ID를 가진 명시적 변경으로 표현한다`() {
|
|
||||||
val deleted = CommunityChange.Deleted(postId = 31L)
|
|
||||||
val pinChanged = CommunityChange.PinChanged(postId = 32L)
|
|
||||||
|
|
||||||
assertEquals(31L, deleted.postId)
|
|
||||||
assertEquals(32L, pinChanged.postId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,164 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("private fun handleCommunityChange(change: CommunityChange)"))
|
assertTrue(source.contains("private fun handleCommunityChange(change: CommunityChange)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `홈과 커뮤니티 list 더보기는 동일 menu item을 Activity Host까지 전달한다`() {
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val homeFragment = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
|
||||||
|
).readText()
|
||||||
|
val communityFragment = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
||||||
|
).readText()
|
||||||
|
val activity = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
adapter.contains("private val onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}")
|
||||||
|
)
|
||||||
|
assertTrue(adapter.contains("currentUserId = item.currentMemberId"))
|
||||||
|
assertTrue(adapter.contains("showMore = menuItem.showMore"))
|
||||||
|
assertTrue(adapter.contains("setOnMoreClick { onCommunityMoreClick(menuItem) }"))
|
||||||
|
assertTrue(adapter.contains("row.setOnClickListener { onCommunityClick(community.postId) }"))
|
||||||
|
assertTrue(homeFragment.contains("onCommunityMoreClick = ::onCommunityMoreClicked"))
|
||||||
|
assertTrue(homeFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
|
||||||
|
assertTrue(
|
||||||
|
homeFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
|
||||||
|
)
|
||||||
|
assertTrue(communityFragment.contains("host.onCreatorChannelCommunityMoreClicked(item)"))
|
||||||
|
assertTrue(
|
||||||
|
communityFragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)")
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
activity.contains(
|
||||||
|
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 더보기는 하나의 Activity callback에서 권한별 기존 메뉴와 수정 결과를 연결한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val handlerSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)",
|
||||||
|
endMarker = "override fun onCreatorChannelFanTalkContentChanged"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
Regex(
|
||||||
|
Regex.escape(
|
||||||
|
"override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.findAll(source)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
assertTrue(handlerSource.contains("if (!item.showMore) return"))
|
||||||
|
assertTrue(handlerSource.contains("CreatorCommunityPostMenuBottomSheetDialog("))
|
||||||
|
assertTrue(handlerSource.contains("isFixed = item.isPinned"))
|
||||||
|
assertTrue(handlerSource.contains("isCreator = item.isOwner"))
|
||||||
|
assertTrue(handlerSource.contains("onClickPin = { updateCreatorChannelCommunityPostFixed(item) }"))
|
||||||
|
assertTrue(handlerSource.contains("onClickDelete = { showDeleteCreatorChannelCommunityPostDialog(item) }"))
|
||||||
|
assertTrue(handlerSource.contains("CreatorCommunityReportDialog("))
|
||||||
|
assertTrue(handlerSource.contains("if (item.isOwner)"))
|
||||||
|
assertFalse(handlerSource.contains("isCreatorChannelOwner()"))
|
||||||
|
assertTrue(source.contains("private val communityModifyLauncher = registerForActivityResult"))
|
||||||
|
assertTrue(source.contains("CommunityActivityResultSource.Modify"))
|
||||||
|
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
|
||||||
|
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 고정과 삭제 mutation은 역할과 중복을 재확인하고 성공만 목록을 갱신한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val pinSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem)",
|
||||||
|
endMarker = "private fun showDeleteCreatorChannelCommunityPostDialog"
|
||||||
|
)
|
||||||
|
val deleteSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem)",
|
||||||
|
endMarker = "private fun reportCreatorChannelCommunityPost"
|
||||||
|
)
|
||||||
|
val deleteDialogSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun showDeleteCreatorChannelCommunityPostDialog",
|
||||||
|
endMarker = "private fun deleteCreatorChannelCommunityPost"
|
||||||
|
)
|
||||||
|
|
||||||
|
listOf(pinSource, deleteSource).forEach { mutationSource ->
|
||||||
|
assertTrue(
|
||||||
|
mutationSource.contains(
|
||||||
|
"if (!item.isOwner || isCommunityPostMutating) return"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertFalse(mutationSource.contains("isCreatorChannelOwner()"))
|
||||||
|
assertTrue(mutationSource.contains("isCommunityPostMutating = true"))
|
||||||
|
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(mutationSource).count())
|
||||||
|
assertTrue(mutationSource.contains("if (response.success)"))
|
||||||
|
assertTrue(mutationSource.contains("handleCommunityChange(CommunityChange.Updated)"))
|
||||||
|
assertTrue(mutationSource.contains("showCommunityMutationFailureToast"))
|
||||||
|
}
|
||||||
|
assertTrue(
|
||||||
|
pinSource.contains(
|
||||||
|
"creatorCommunityRepository.updateCommunityPostFixed(item.postId, !item.isPinned, authToken())"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(deleteDialogSource.contains("if (!item.isOwner) return"))
|
||||||
|
assertFalse(deleteDialogSource.contains("isCreatorChannelOwner()"))
|
||||||
|
assertTrue(deleteSource.contains("ModifyCommunityPostRequest("))
|
||||||
|
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
|
||||||
|
assertTrue(deleteSource.contains("isActive = false"))
|
||||||
|
assertTrue(deleteSource.contains("Gson().toJson"))
|
||||||
|
assertTrue(deleteSource.contains("toRequestBody(\"text/plain\".toMediaType())"))
|
||||||
|
assertTrue(
|
||||||
|
deleteSource.contains(
|
||||||
|
"creatorCommunityRepository.modifyCommunityPost(postImage = null, request = request, token = authToken())"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `커뮤니티 신고 mutation은 방문자 역할과 중복을 재확인하고 성공 토스트만 표시한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val reportSource = sourceSection(
|
||||||
|
source = source,
|
||||||
|
startMarker = "private fun reportCreatorChannelCommunityPost(",
|
||||||
|
endMarker = "private fun showCommunityMutationFailureToast"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
reportSource.contains(
|
||||||
|
"if (item.isOwner || !item.showMore || reason.isBlank() || " +
|
||||||
|
"isCommunityPostMutating) return"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertFalse(reportSource.contains("isCreatorChannelOwner()"))
|
||||||
|
assertTrue(reportSource.contains("isCommunityPostMutating = true"))
|
||||||
|
assertEquals(2, "isCommunityPostMutating = false".toRegex().findAll(reportSource).count())
|
||||||
|
assertTrue(
|
||||||
|
reportSource.contains(
|
||||||
|
"creatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(reportSource.contains("response.message.isNullOrBlank()"))
|
||||||
|
assertTrue(reportSource.contains("R.string.character_comment_report_submitted"))
|
||||||
|
assertTrue(reportSource.contains("showCommunityMutationFailureToast"))
|
||||||
|
assertFalse(reportSource.contains("handleCommunityChange("))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() {
|
fun `follow notify source는 Phase 11 직접 팔로우 알림 액션을 연결한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
@@ -653,80 +811,12 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post"))
|
assertTrue(source.contains("iconResId = R.drawable.ic_new_upload_community_post"))
|
||||||
assertTrue(source.contains("textResId = R.string.creator_channel_owner_fab_community"))
|
assertTrue(source.contains("textResId = R.string.creator_channel_owner_fab_community"))
|
||||||
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()"))
|
assertTrue(source.contains("CreatorChannelTab.Community.ordinal -> onOwnerFabCommunityClicked()"))
|
||||||
assertTrue(source.contains("private val communityPostModifyLauncher"))
|
assertFalse(source.contains("communityPostModifyLauncher"))
|
||||||
assertTrue(source.contains("CreatorCommunityModifyActivity::class.java"))
|
assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||||
assertTrue(source.contains("putExtra(Constants.EXTRA_COMMUNITY_POST_ID, item.postId)"))
|
|
||||||
assertTrue(source.contains("creatorCommunityRepository.updateCommunityPostFixed("))
|
|
||||||
assertTrue(source.contains("isFixed = !item.isPinned"))
|
|
||||||
assertTrue(source.contains("creatorCommunityRepository.modifyCommunityPost("))
|
|
||||||
assertTrue(source.contains("isActive = false"))
|
|
||||||
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()"))
|
assertTrue(source.contains("findCommunityFragment()?.onCreatorChannelCommunityRefreshRequested()"))
|
||||||
assertFalse(source.contains("onCreatorChannelCommunityOwnerMoreClicked(postId: Long)"))
|
|
||||||
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked(item.postId)"))
|
|
||||||
assertFalse(source.contains("onClickPin = {},"))
|
|
||||||
assertFalse(source.contains("onClickModify = {},"))
|
|
||||||
assertFalse(source.contains("onClickDelete = {},"))
|
|
||||||
assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()"))
|
assertTrue(fragment.contains("fun onCreatorChannelCommunityRefreshRequested()"))
|
||||||
assertTrue(
|
assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
|
||||||
|
assertFalse(fragment.contains("toggleCommunityAudio"))
|
||||||
fragment.contains(
|
|
||||||
"CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertTrue(fragment.contains("mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl))"))
|
|
||||||
assertTrue(fragment.contains("mediaPlayerManager?.stopContent()"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `커뮤니티 고정 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
|
|
||||||
val source = projectFile(
|
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
|
||||||
).readText()
|
|
||||||
val fixedSource = sourceSection(
|
|
||||||
source = source,
|
|
||||||
startMarker = "private fun updateCreatorChannelCommunityPostFixed",
|
|
||||||
endMarker = "private fun deleteCreatorChannelCommunityPost"
|
|
||||||
)
|
|
||||||
val successMarker = "if (response.success)"
|
|
||||||
val beforeSuccessSource = fixedSource.substring(0, fixedSource.indexOf(successMarker))
|
|
||||||
val successSource = fixedSource.substring(
|
|
||||||
fixedSource.indexOf(successMarker),
|
|
||||||
fixedSource.indexOf("} else", fixedSource.indexOf(successMarker))
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue(fixedSource.contains("postId = item.postId"))
|
|
||||||
assertTrue(fixedSource.contains("isFixed = !item.isPinned"))
|
|
||||||
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.PinChanged(item.postId))"))
|
|
||||||
assertFalse(beforeSuccessSource.contains("CommunityChange.PinChanged"))
|
|
||||||
assertEquals(1, fixedSource.split("CommunityChange.PinChanged").size - 1)
|
|
||||||
assertFalse(fixedSource.contains("CommunityChange.PinChanged(creatorId)"))
|
|
||||||
assertFalse(fixedSource.contains("refreshCreatorChannelCommunity()"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `커뮤니티 삭제 성공은 실제 post id로 단일 변경 handler를 호출한다`() {
|
|
||||||
val source = projectFile(
|
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
|
||||||
).readText()
|
|
||||||
val deleteSource = sourceSection(
|
|
||||||
source = source,
|
|
||||||
startMarker = "private fun deleteCreatorChannelCommunityPost",
|
|
||||||
endMarker = "private fun authToken()"
|
|
||||||
)
|
|
||||||
val successMarker = "if (response.success)"
|
|
||||||
val beforeSuccessSource = deleteSource.substring(0, deleteSource.indexOf(successMarker))
|
|
||||||
val successSource = deleteSource.substring(
|
|
||||||
deleteSource.indexOf(successMarker),
|
|
||||||
deleteSource.indexOf("} else", deleteSource.indexOf(successMarker))
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue(deleteSource.contains("creatorCommunityId = item.postId"))
|
|
||||||
assertTrue(deleteSource.contains("isActive = false"))
|
|
||||||
assertTrue(successSource.contains("handleCommunityChange(CommunityChange.Deleted(item.postId))"))
|
|
||||||
assertFalse(beforeSuccessSource.contains("CommunityChange.Deleted"))
|
|
||||||
assertEquals(1, deleteSource.split("CommunityChange.Deleted").size - 1)
|
|
||||||
assertFalse(deleteSource.contains("CommunityChange.Deleted(creatorId)"))
|
|
||||||
assertFalse(deleteSource.contains("refreshCreatorChannelCommunity()"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -743,18 +833,14 @@ class CreatorChannelActivitySourceTest {
|
|||||||
handlerSource.indexOf("CommunityChange.Created"),
|
handlerSource.indexOf("CommunityChange.Created"),
|
||||||
handlerSource.indexOf("CommunityChange.Updated")
|
handlerSource.indexOf("CommunityChange.Updated")
|
||||||
)
|
)
|
||||||
val mutationSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated"))
|
val updatedSource = handlerSource.substring(handlerSource.indexOf("CommunityChange.Updated"))
|
||||||
|
|
||||||
assertTrue(createdSource.contains("homeActionDelegate?.refreshHome()"))
|
assertTrue(createdSource.contains("homeActionDelegate?.refreshHome()"))
|
||||||
assertTrue(createdSource.contains("refreshCreatorChannelCommunity()"))
|
assertTrue(createdSource.contains("refreshCreatorChannelCommunity()"))
|
||||||
assertTrue(
|
assertTrue(updatedSource.contains("homeActionDelegate?.refreshHome()"))
|
||||||
mutationSource.contains(
|
assertTrue(updatedSource.contains("refreshCreatorChannelCommunity()"))
|
||||||
"CommunityChange.Updated,\n" +
|
assertFalse(updatedSource.contains("CommunityChange.Deleted"))
|
||||||
" is CommunityChange.Deleted,\n" +
|
assertFalse(updatedSource.contains("CommunityChange.PinChanged"))
|
||||||
" is CommunityChange.PinChanged -> refreshCreatorChannelCommunity()"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertFalse(mutationSource.contains("homeActionDelegate?.refreshHome()"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -2003,7 +2089,7 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp("))
|
assertTrue(adapter.contains("val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp("))
|
||||||
assertTrue(adapter.contains("rootWidthDp = communityWidthDp"))
|
assertTrue(adapter.contains("rootWidthDp = communityWidthDp"))
|
||||||
assertTrue(adapter.contains("setHideEmptyTextRows(true)"))
|
assertTrue(adapter.contains("setHideEmptyTextRows(true)"))
|
||||||
assertTrue(adapter.contains("bind(community.toFeedCommunityItem())"))
|
assertTrue(adapter.contains("bind(community.toFeedCommunityItem(showMore = menuItem.showMore))"))
|
||||||
assertTrue(adapter.contains("bindCommunityImages(row, community)"))
|
assertTrue(adapter.contains("bindCommunityImages(row, community)"))
|
||||||
assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)"))
|
assertTrue(adapter.contains("BlurTransformation(itemView.context, 25f, 2.5f)"))
|
||||||
assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams("))
|
assertTrue(adapter.contains("row.layoutParams = LinearLayout.LayoutParams("))
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ class CreatorChannelHomeMapperTest {
|
|||||||
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `현재 회원 id를 커뮤니티 section에 보존한다`() {
|
||||||
|
val communities = response().toUiContent(currentMemberId = 77L)
|
||||||
|
.sections
|
||||||
|
.filterIsInstance<CreatorChannelHomeSection.Communities>()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
assertEquals(77L, communities.currentMemberId)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() {
|
fun `null 단건 콘텐츠와 빈 리스트와 blank SNS는 후원 empty와 팬Talk empty section을 생성한다`() {
|
||||||
val content = response(
|
val content = response(
|
||||||
@@ -267,7 +277,9 @@ class CreatorChannelHomeMapperTest {
|
|||||||
dateUtc = "2026-06-11T12:00:00Z",
|
dateUtc = "2026-06-11T12:00:00Z",
|
||||||
existOrdered = false,
|
existOrdered = false,
|
||||||
likeCount = 1,
|
likeCount = 1,
|
||||||
commentCount = 2
|
commentCount = 2,
|
||||||
|
isPinned = false,
|
||||||
|
isCommentAvailable = true
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun fanTalk() = CreatorChannelFanTalkSummaryResponse(
|
private fun fanTalk() = CreatorChannelFanTalkSummaryResponse(
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import com.google.gson.Gson
|
|||||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelHomeResponse
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class CreatorChannelHomeModelsTest {
|
class CreatorChannelHomeModelsTest {
|
||||||
@@ -36,6 +38,15 @@ class CreatorChannelHomeModelsTest {
|
|||||||
assertEquals("https://x.example", response.sns.xUrl)
|
assertEquals("https://x.example", response.sns.xUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home response preserves community pinned and comment availability`() {
|
||||||
|
val response = gson.fromJson(homeJson(scheduleType = "AUDIO"), CreatorChannelHomeResponse::class.java)
|
||||||
|
|
||||||
|
val community = response.communities.single()
|
||||||
|
assertTrue(community.isPinned)
|
||||||
|
assertFalse(community.isCommentAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
private fun homeJson(scheduleType: String): String = """
|
private fun homeJson(scheduleType: String): String = """
|
||||||
{
|
{
|
||||||
"creator": {
|
"creator": {
|
||||||
@@ -63,7 +74,24 @@ class CreatorChannelHomeModelsTest {
|
|||||||
],
|
],
|
||||||
"audioContents": [],
|
"audioContents": [],
|
||||||
"series": [],
|
"series": [],
|
||||||
"communities": [],
|
"communities": [
|
||||||
|
{
|
||||||
|
"postId": 21,
|
||||||
|
"creatorId": 1,
|
||||||
|
"creatorNickname": "creator",
|
||||||
|
"creatorProfileUrl": "https://example.com/profile.png",
|
||||||
|
"imageUrl": null,
|
||||||
|
"audioUrl": null,
|
||||||
|
"content": "community",
|
||||||
|
"price": 0,
|
||||||
|
"dateUtc": "2026-06-12T00:00:00Z",
|
||||||
|
"existOrdered": false,
|
||||||
|
"likeCount": 6,
|
||||||
|
"commentCount": 5,
|
||||||
|
"isPinned": true,
|
||||||
|
"isCommentAvailable": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"fanTalk": {
|
"fanTalk": {
|
||||||
"totalCount": 0,
|
"totalCount": 0,
|
||||||
"latestFanTalk": null
|
"latestFanTalk": null
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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 org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -29,6 +30,7 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
|||||||
val reply = inflateView(R.layout.activity_creator_channel_community_reply)
|
val reply = inflateView(R.layout.activity_creator_channel_community_reply)
|
||||||
|
|
||||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back))
|
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_back))
|
||||||
|
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_more))
|
||||||
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header))
|
assertNotNull(detail.findViewById<TextView>(R.id.tv_creator_channel_community_detail_comment_header))
|
||||||
assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments))
|
assertNotNull(detail.findViewById<RecyclerView>(R.id.rv_creator_channel_community_detail_comments))
|
||||||
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall))
|
assertNotNull(detail.findViewById<View>(R.id.layout_creator_channel_community_detail_paywall))
|
||||||
@@ -39,6 +41,9 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
|||||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
|
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_send))
|
||||||
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel))
|
assertNotNull(detail.findViewById<ImageButton>(R.id.btn_creator_channel_community_detail_edit_cancel))
|
||||||
assertRemovedId("tv_creator_channel_community_detail_title")
|
assertRemovedId("tv_creator_channel_community_detail_title")
|
||||||
|
assertRemovedId("layout_creator_channel_community_detail_price")
|
||||||
|
assertRemovedId("tv_creator_channel_community_detail_price")
|
||||||
|
assertRemovedId("tv_creator_channel_community_detail_purchased")
|
||||||
assertRemovedId("tv_creator_channel_community_detail_comment_unavailable")
|
assertRemovedId("tv_creator_channel_community_detail_comment_unavailable")
|
||||||
|
|
||||||
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back))
|
assertNotNull(reply.findViewById<ImageButton>(R.id.btn_creator_channel_community_reply_back))
|
||||||
@@ -156,6 +161,23 @@ class CreatorChannelCommunityDetailReplyUiContractTest {
|
|||||||
assertTrue(detailActivity.contains("btn_audio_content_play"))
|
assertTrue(detailActivity.contains("btn_audio_content_play"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `상세 게시글 더보기는 기존 관리 신고 UI와 수정 launcher를 재사용한다`() {
|
||||||
|
val detailActivity = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/" +
|
||||||
|
"CreatorChannelCommunityDetailActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(detailActivity.contains("CreatorCommunityPostMenuBottomSheetDialog"))
|
||||||
|
assertTrue(detailActivity.contains("CreatorCommunityReportDialog"))
|
||||||
|
assertTrue(detailActivity.contains("CreatorCommunityModifyActivity"))
|
||||||
|
assertTrue(detailActivity.contains("modifyLauncher"))
|
||||||
|
assertTrue(detailActivity.contains("Constants.EXTRA_COMMUNITY_POST_ID"))
|
||||||
|
assertTrue(detailActivity.contains("btnCreatorChannelCommunityDetailMore"))
|
||||||
|
assertFalse(detailActivity.contains("layoutCreatorChannelCommunityDetailPrice"))
|
||||||
|
assertFalse(detailActivity.contains("tvCreatorChannelCommunityDetailPurchased"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
|
fun `댓글 답글 Adapter는 PopupMenu 대신 regular font popup을 사용한다`() {
|
||||||
val commentAdapter = projectFile(
|
val commentAdapter = projectFile(
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.argThat
|
import org.mockito.kotlin.argThat
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.isNull
|
||||||
import org.mockito.kotlin.never
|
import org.mockito.kotlin.never
|
||||||
import org.mockito.kotlin.times
|
import org.mockito.kotlin.times
|
||||||
import org.mockito.kotlin.verify
|
import org.mockito.kotlin.verify
|
||||||
@@ -469,6 +471,99 @@ class CreatorChannelCommunityDetailViewModelTest {
|
|||||||
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시글 더보기는 작성자 또는 무료 또는 구매 완료일 때만 표시한다`() {
|
||||||
|
SharedPreferenceManager.userId = 10L
|
||||||
|
val cases = listOf(
|
||||||
|
detailResponse(isCommentAvailable = false, creatorId = 10L, price = 300, existOrdered = false),
|
||||||
|
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 0, existOrdered = false),
|
||||||
|
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = true),
|
||||||
|
detailResponse(isCommentAvailable = false, creatorId = 999L, price = 300, existOrdered = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
val results = cases.map { response ->
|
||||||
|
stubDetail(response)
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
(viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content).post
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(results[0].isOwner)
|
||||||
|
assertTrue(results[0].showMore)
|
||||||
|
assertFalse(results[1].isOwner)
|
||||||
|
assertTrue(results[1].showMore)
|
||||||
|
assertTrue(results[2].showMore)
|
||||||
|
assertFalse(results[3].showMore)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시글 고정 성공은 반전 값을 요청하고 상세 재조회와 변경 이벤트를 노출한다`() {
|
||||||
|
whenever(repository.getCommunityPostDetail(POST_ID, AUTH_TOKEN)).thenReturn(
|
||||||
|
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = false), null)),
|
||||||
|
Single.just(ApiResponse(true, detailResponse(isCommentAvailable = false, creatorId = 10L, isPinned = true), null))
|
||||||
|
)
|
||||||
|
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
|
||||||
|
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
|
||||||
|
viewModel.updateCommunityPostFixed()
|
||||||
|
|
||||||
|
val state = viewModel.detailStateLiveData.requireValue() as CreatorChannelCommunityDetailUiState.Content
|
||||||
|
assertTrue(state.post.isPinned)
|
||||||
|
assertTrue(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||||
|
verify(legacyRepository).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
|
||||||
|
verify(repository, times(2)).getCommunityPostDetail(POST_ID, AUTH_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시글 삭제 성공은 삭제 이벤트를 노출한다`() {
|
||||||
|
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
|
||||||
|
whenever(legacyRepository.modifyCommunityPost(isNull(), any(), eq(AUTH_TOKEN)))
|
||||||
|
.thenReturn(Single.just(ApiResponse(true, Any(), null)))
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
|
||||||
|
viewModel.deleteCommunityPost()
|
||||||
|
|
||||||
|
assertTrue(viewModel.postDeletedEventLiveData.requireValue() == true)
|
||||||
|
verify(legacyRepository).modifyCommunityPost(
|
||||||
|
isNull(),
|
||||||
|
argThat { requestBody ->
|
||||||
|
okio.Buffer().also(requestBody::writeTo).readUtf8().contains("\"isActive\":false")
|
||||||
|
},
|
||||||
|
eq(AUTH_TOKEN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시글 신고 성공은 repository 호출과 toast만 노출한다`() {
|
||||||
|
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 999L))
|
||||||
|
whenever(repository.reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN))
|
||||||
|
.thenReturn(Single.just(ApiResponse(true, Any(), "접수됨")))
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
|
||||||
|
viewModel.reportCommunityPost("스팸")
|
||||||
|
|
||||||
|
verify(repository).reportCommunityPost(POST_ID, "스팸", AUTH_TOKEN)
|
||||||
|
assertEquals("접수됨", viewModel.toastLiveData.requireValue()?.consume()?.message)
|
||||||
|
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `게시글 mutation 진행 중에는 중복 요청을 막고 실패 시 성공 이벤트를 노출하지 않는다`() {
|
||||||
|
stubDetail(detailResponse(isCommentAvailable = false, creatorId = 10L))
|
||||||
|
val subject = PublishSubject.create<ApiResponse<Any>>()
|
||||||
|
whenever(legacyRepository.updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN))
|
||||||
|
.thenReturn(subject.firstOrError())
|
||||||
|
viewModel.loadDetail(POST_ID)
|
||||||
|
|
||||||
|
viewModel.updateCommunityPostFixed()
|
||||||
|
viewModel.updateCommunityPostFixed()
|
||||||
|
subject.onNext(ApiResponse(false, null, "고정 실패"))
|
||||||
|
|
||||||
|
verify(legacyRepository, times(1)).updateCommunityPostFixed(POST_ID, true, AUTH_TOKEN)
|
||||||
|
assertEquals("고정 실패", viewModel.toastLiveData.requireValue()?.consume()?.message)
|
||||||
|
assertFalse(viewModel.postChangedEventLiveData.requireValue() == true)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
|
fun `댓글 UI 모델은 답글 화면 재포맷을 위해 원본 createdAtUtc를 보존한다`() {
|
||||||
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
|
stubDetail(detailResponse(isCommentAvailable = true, commentIds = listOf(11L)))
|
||||||
@@ -571,7 +666,8 @@ class CreatorChannelCommunityDetailViewModelTest {
|
|||||||
existOrdered: Boolean = true,
|
existOrdered: Boolean = true,
|
||||||
creatorId: Long = 100L,
|
creatorId: Long = 100L,
|
||||||
commentIds: List<Long> = emptyList(),
|
commentIds: List<Long> = emptyList(),
|
||||||
commentsHasNext: Boolean = false
|
commentsHasNext: Boolean = false,
|
||||||
|
isPinned: Boolean = false
|
||||||
) = CreatorChannelCommunityPostDetailResponse(
|
) = CreatorChannelCommunityPostDetailResponse(
|
||||||
postId = POST_ID,
|
postId = POST_ID,
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
@@ -587,7 +683,7 @@ class CreatorChannelCommunityDetailViewModelTest {
|
|||||||
likeCount = likeCount,
|
likeCount = likeCount,
|
||||||
commentCount = 2,
|
commentCount = 2,
|
||||||
isLiked = isLiked,
|
isLiked = isLiked,
|
||||||
isPinned = false,
|
isPinned = isPinned,
|
||||||
comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext)
|
comments = commentsResponse(ids = commentIds, hasNext = commentsHasNext)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
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
|
||||||
@@ -62,7 +63,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 재생 반응 owner 영역을 제공한다`() {
|
fun `커뮤니티 list item layout은 프로필 본문 이미지 잠금 반응과 작성자 금액을 제공한다`() {
|
||||||
val item = inflateView(R.layout.item_creator_channel_community_list)
|
val item = inflateView(R.layout.item_creator_channel_community_list)
|
||||||
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText()
|
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_community_list.xml").readText()
|
||||||
val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container))
|
val imageContainer = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_image_container))
|
||||||
@@ -77,24 +78,29 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image))
|
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_image))
|
||||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay))
|
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_locked_overlay))
|
||||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_locked_price))
|
||||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_play))
|
|
||||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
|
||||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
|
||||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
|
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
|
||||||
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price))
|
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price))
|
||||||
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_owner_more))
|
|
||||||
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
|
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
|
||||||
|
val moreButton = requireNotNull(item.findViewById<View>(R.id.btn_creator_channel_community_list_more))
|
||||||
assertSame(imageContainer, (lockIcon.parent as View).parent)
|
assertSame(imageContainer, (lockIcon.parent as View).parent)
|
||||||
assertSame(imageContainer, (lockedPrice.parent as View).parent)
|
assertSame(imageContainer, ((lockedPrice.parent as View).parent as View).parent)
|
||||||
assertSame(
|
assertSame(
|
||||||
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
||||||
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent
|
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent
|
||||||
)
|
)
|
||||||
|
assertSame(
|
||||||
|
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
|
||||||
|
moreButton.parent
|
||||||
|
)
|
||||||
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
|
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
|
||||||
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
|
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
|
||||||
assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\""))
|
assertTrue(itemLayout.contains("android:background=\"@drawable/bg_creator_channel_community_price\""))
|
||||||
assertTrue(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
assertFalse(itemLayout.contains("android:drawableStart=\"@drawable/ic_bar_cash\""))
|
||||||
assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_list_play\""))
|
assertTrue(itemLayout.contains("android:src=\"@drawable/ic_bar_cash\""))
|
||||||
|
assertFalse(itemLayout.contains("iv_creator_channel_community_list_play"))
|
||||||
|
assertFalse(itemLayout.contains("iv_creator_channel_community_list_owner_more"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -138,7 +144,7 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `커뮤니티 fragment source는 pagination view mode owner padding stopContent 계약을 사용한다`() {
|
fun `커뮤니티 fragment source는 pagination view mode owner padding과 상세 이동 계약을 사용한다`() {
|
||||||
val fragment = projectFile(
|
val fragment = projectFile(
|
||||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt"
|
||||||
).readText()
|
).readText()
|
||||||
@@ -161,8 +167,10 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize("))
|
assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize("))
|
||||||
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
|
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
|
||||||
assertTrue(fragment.contains("applyOwnerCtaPadding"))
|
assertTrue(fragment.contains("applyOwnerCtaPadding"))
|
||||||
assertTrue(fragment.contains("pauseContent"))
|
assertFalse(fragment.contains("CreatorCommunityMediaPlayerManager"))
|
||||||
assertTrue(fragment.contains("stopContent"))
|
assertFalse(fragment.contains("toggleCommunityAudio"))
|
||||||
|
assertFalse(fragment.contains("onPlayClick"))
|
||||||
|
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -181,19 +189,25 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment"))
|
assertTrue(listAdapter.contains("showComment") && listAdapter.contains("isVisible = item.showComment"))
|
||||||
assertTrue(listAdapter.contains("isLocked"))
|
assertTrue(listAdapter.contains("isLocked"))
|
||||||
assertTrue(gridAdapter.contains("isLocked"))
|
assertTrue(gridAdapter.contains("isLocked"))
|
||||||
assertTrue(listAdapter.contains("showPlayButton"))
|
|
||||||
assertTrue(listAdapter.contains("showOwnerMore"))
|
|
||||||
assertTrue(listAdapter.contains("isPlayingContent"))
|
|
||||||
assertTrue(listAdapter.contains("R.drawable.ic_player_pause"))
|
|
||||||
assertTrue(listAdapter.contains("R.drawable.ic_new_player_play"))
|
|
||||||
assertTrue(listAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
assertTrue(listAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
||||||
|
assertTrue(listAdapter.contains("private val onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit = {}"))
|
||||||
assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
assertTrue(listAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
||||||
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListPlay.setOnClickListener { onPlayClick(item) }"))
|
assertTrue(
|
||||||
assertTrue(listAdapter.contains("onOwnerMoreClick(item)"))
|
listAdapter.contains(
|
||||||
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }"))
|
"btnCreatorChannelCommunityListMore.setOnClickListener { onMoreClick(item.menuItem) }"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(listAdapter.contains("item.showOwnerTopPrice || item.menuItem.showMore"))
|
||||||
|
assertFalse(listAdapter.contains("onPlayClick"))
|
||||||
|
assertFalse(listAdapter.contains("onOwnerMoreClick"))
|
||||||
|
assertFalse(listAdapter.contains("isPlayingContent"))
|
||||||
|
assertFalse(listAdapter.contains("showPlayButton"))
|
||||||
|
assertFalse(listAdapter.contains("showOwnerMore"))
|
||||||
assertTrue(gridAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
assertTrue(gridAdapter.contains("private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}"))
|
||||||
assertTrue(gridAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
assertTrue(gridAdapter.contains("root.setOnClickListener { onPostClick(item) }"))
|
||||||
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
|
assertFalse(gridAdapter.contains("onMoreClick"))
|
||||||
|
assertFalse(gridAdapter.contains("CreatorChannelCommunityMenuItem"))
|
||||||
|
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
|
||||||
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
|
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
|
||||||
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
|
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
|
||||||
assertTrue(gridAdapter.contains(".asBitmap()"))
|
assertTrue(gridAdapter.contains(".asBitmap()"))
|
||||||
@@ -211,8 +225,17 @@ class CreatorChannelCommunityFragmentLayoutTest {
|
|||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(fragment.contains("onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }"))
|
assertTrue(fragment.contains("onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }"))
|
||||||
|
assertTrue(fragment.contains("onMoreClick = { item -> host.onCreatorChannelCommunityMoreClicked(item) }"))
|
||||||
assertTrue(fragment.contains("private val gridAdapter = CreatorChannelCommunityGridAdapter("))
|
assertTrue(fragment.contains("private val gridAdapter = CreatorChannelCommunityGridAdapter("))
|
||||||
assertTrue(fragment.contains("fun onCreatorChannelCommunityPostClicked(postId: Long)"))
|
assertTrue(fragment.contains("fun onCreatorChannelCommunityPostClicked(postId: Long)"))
|
||||||
|
assertTrue(fragment.contains("fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)"))
|
||||||
|
assertFalse(fragment.contains("onPlayClick"))
|
||||||
|
assertFalse(fragment.contains("onCreatorChannelCommunityOwnerMoreClicked"))
|
||||||
|
assertFalse(
|
||||||
|
fragment.substringAfter("private val gridAdapter = CreatorChannelCommunityGridAdapter(")
|
||||||
|
.substringBefore(")\n private var currentContentState")
|
||||||
|
.contains("onMoreClick")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class CreatorChannelCommunityMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `유료 미구매 타인 게시글은 잠금 상태이고 play button을 숨긴다`() {
|
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(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single()
|
||||||
@@ -74,7 +74,6 @@ class CreatorChannelCommunityMapperTest {
|
|||||||
assertTrue(item.isLocked)
|
assertTrue(item.isLocked)
|
||||||
assertNull(item.imageUrl)
|
assertNull(item.imageUrl)
|
||||||
assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode)
|
assertEquals(CreatorChannelCommunityImageMode.LockedGray, item.imageMode)
|
||||||
assertFalse(item.showPlayButton)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -93,7 +92,7 @@ class CreatorChannelCommunityMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `고정 게시글 여부는 owner more 정책에서 사용할 수 있도록 UI 모델에 보존한다`() {
|
fun `고정 게시글 여부는 UI 모델에 보존한다`() {
|
||||||
val pinnedItem = listOf(communityPost(isPinned = true))
|
val pinnedItem = listOf(communityPost(isPinned = true))
|
||||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
|
||||||
.single()
|
.single()
|
||||||
@@ -108,33 +107,32 @@ class CreatorChannelCommunityMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `본인 또는 구매한 사용자는 이미지와 오디오가 있으면 play button을 표시한다`() {
|
fun `메뉴 권한은 작성자 무료 구매 여부에 따라 계산하고 고정 상태를 보존한다`() {
|
||||||
val ownerItem = listOf(
|
val authorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false, isPinned = true))
|
||||||
communityPost(price = 100, existOrdered = false, imageUrl = "image.png", audioUrl = "audio.mp3")
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
|
||||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L).single()
|
.single()
|
||||||
val orderedItem = listOf(
|
val visitorFree = listOf(communityPost(price = 0, existOrdered = false))
|
||||||
communityPost(price = 100, existOrdered = true, imageUrl = "image.png", audioUrl = "audio.mp3")
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single()
|
.single()
|
||||||
|
val visitorPaidOrdered = listOf(communityPost(price = 100, existOrdered = true))
|
||||||
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||||
|
.single()
|
||||||
|
val visitorPaidNotOrdered = listOf(communityPost(price = 100, existOrdered = false))
|
||||||
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
||||||
|
.single()
|
||||||
|
|
||||||
assertFalse(ownerItem.isLocked)
|
assertTrue(authorPaidNotOrdered.menuItem.isOwner)
|
||||||
assertTrue(ownerItem.showPlayButton)
|
assertTrue(authorPaidNotOrdered.menuItem.showMore)
|
||||||
assertFalse(orderedItem.isLocked)
|
assertTrue(authorPaidNotOrdered.menuItem.isPinned)
|
||||||
assertTrue(orderedItem.showPlayButton)
|
assertTrue(authorPaidNotOrdered.isPinned)
|
||||||
|
assertTrue(visitorFree.menuItem.showMore)
|
||||||
|
assertTrue(visitorPaidOrdered.menuItem.showMore)
|
||||||
|
assertFalse(visitorPaidNotOrdered.menuItem.isOwner)
|
||||||
|
assertFalse(visitorPaidNotOrdered.menuItem.showMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `오디오 또는 이미지가 없으면 play button을 숨긴다`() {
|
fun `본인 채널에 본인이 쓴 유료 게시글에서만 top price를 표시한다`() {
|
||||||
val items = listOf(
|
|
||||||
communityPost(postId = 1L, imageUrl = null, audioUrl = "audio.mp3"),
|
|
||||||
communityPost(postId = 2L, imageUrl = "image.png", audioUrl = null),
|
|
||||||
communityPost(postId = 3L, imageUrl = "image.png", audioUrl = " ")
|
|
||||||
).toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L)
|
|
||||||
|
|
||||||
assertEquals(listOf(false, false, false), items.map { it.showPlayButton })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `본인 채널에 본인이 쓴 게시글에서만 owner more와 유료 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(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L)
|
||||||
.single()
|
.single()
|
||||||
@@ -148,13 +146,9 @@ class CreatorChannelCommunityMapperTest {
|
|||||||
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
|
.toCommunityPostUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
assertTrue(ownerPaid.showOwnerMore)
|
|
||||||
assertTrue(ownerPaid.showOwnerTopPrice)
|
assertTrue(ownerPaid.showOwnerTopPrice)
|
||||||
assertTrue(ownerFree.showOwnerMore)
|
|
||||||
assertFalse(ownerFree.showOwnerTopPrice)
|
assertFalse(ownerFree.showOwnerTopPrice)
|
||||||
assertFalse(otherCreator.showOwnerMore)
|
|
||||||
assertFalse(otherCreator.showOwnerTopPrice)
|
assertFalse(otherCreator.showOwnerTopPrice)
|
||||||
assertFalse(otherChannel.showOwnerMore)
|
|
||||||
assertFalse(otherChannel.showOwnerTopPrice)
|
assertFalse(otherChannel.showOwnerTopPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.home
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
@@ -659,6 +658,19 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
|
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `popular community mapper preserves locked item reactions`() {
|
||||||
|
val item = popularCommunityData(
|
||||||
|
audioUrl = null,
|
||||||
|
price = 100,
|
||||||
|
existOrdered = false
|
||||||
|
).toUiModel().item
|
||||||
|
|
||||||
|
assertTrue(item.isLocked)
|
||||||
|
assertTrue(item.showReaction)
|
||||||
|
assertTrue(item.showComment)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home relative time formatter converts active creator utc timestamp to relative time text`() {
|
fun `home relative time formatter converts active creator utc timestamp to relative time text`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
@@ -1044,54 +1056,29 @@ class HomeMainFragmentLayoutTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `홈 라이브 어댑터는 라이브가 있으면 전체 항목을 추가한다`() {
|
fun `홈 라이브 어댑터는 최대 스무 개 라이브만 표시한다`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
||||||
val parent = RecyclerView(context)
|
|
||||||
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
|
||||||
val adapter = HomeLiveAdapter()
|
val adapter = HomeLiveAdapter()
|
||||||
|
|
||||||
adapter.submitItems(emptyList())
|
adapter.submitItems(emptyList())
|
||||||
assertEquals(0, adapter.itemCount)
|
assertEquals(0, adapter.itemCount)
|
||||||
|
|
||||||
adapter.submitItems(listOf(liveItem(1L)))
|
adapter.submitItems(listOf(liveItem(1L)))
|
||||||
assertEquals(2, adapter.itemCount)
|
assertEquals(1, adapter.itemCount)
|
||||||
val viewHolder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(1))
|
|
||||||
assertEquals(
|
|
||||||
context.getString(R.string.screen_home_theme_all),
|
|
||||||
(viewHolder.itemView as TextView).text.toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
adapter.submitItems((1L..20L).map(::liveItem))
|
adapter.submitItems((1L..20L).map(::liveItem))
|
||||||
assertEquals(21, adapter.itemCount)
|
assertEquals(20, adapter.itemCount)
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `home live adapter appends all item after twenty lives`() {
|
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
||||||
val parent = RecyclerView(context)
|
|
||||||
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
|
||||||
val adapter = HomeLiveAdapter()
|
|
||||||
|
|
||||||
adapter.submitItems((1L..21L).map(::liveItem))
|
adapter.submitItems((1L..21L).map(::liveItem))
|
||||||
val viewHolder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(20))
|
assertEquals(20, adapter.itemCount)
|
||||||
adapter.onBindViewHolder(viewHolder, 20)
|
|
||||||
val moreText = viewHolder.itemView as TextView
|
|
||||||
|
|
||||||
assertEquals(21, adapter.itemCount)
|
|
||||||
assertEquals(context.getString(R.string.screen_home_theme_all), moreText.text.toString())
|
|
||||||
assertEquals(58.dpToPx(), moreText.layoutParams.width)
|
|
||||||
assertEquals(102.dpToPx(), moreText.layoutParams.height)
|
|
||||||
assertEquals(context.getColor(R.color.soda_400), moreText.currentTextColor)
|
|
||||||
assertEquals(context.getColor(R.color.black), (moreText.background as ColorDrawable).color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home live adapter caps lives before all item`() {
|
fun `홈 라이브 어댑터는 스물두 개 입력도 스무 개로 제한한다`() {
|
||||||
val adapter = HomeLiveAdapter()
|
val adapter = HomeLiveAdapter()
|
||||||
|
|
||||||
adapter.submitItems((1L..22L).map(::liveItem))
|
adapter.submitItems((1L..22L).map(::liveItem))
|
||||||
|
|
||||||
assertEquals(21, adapter.itemCount)
|
assertEquals(20, adapter.itemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1534,7 +1521,9 @@ class HomeMainFragmentLayoutTest {
|
|||||||
|
|
||||||
private fun popularCommunityData(
|
private fun popularCommunityData(
|
||||||
audioUrl: String?,
|
audioUrl: String?,
|
||||||
createdAt: String = "2분 전"
|
createdAt: String = "2분 전",
|
||||||
|
price: Int = 0,
|
||||||
|
existOrdered: Boolean = false
|
||||||
): HomePopularCommunityPostItem {
|
): HomePopularCommunityPostItem {
|
||||||
return HomePopularCommunityPostItem(
|
return HomePopularCommunityPostItem(
|
||||||
postId = 1L,
|
postId = 1L,
|
||||||
@@ -1544,11 +1533,11 @@ class HomeMainFragmentLayoutTest {
|
|||||||
imageUrl = null,
|
imageUrl = null,
|
||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
content = "본문",
|
content = "본문",
|
||||||
price = 0,
|
price = price,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
likeCount = 6L,
|
likeCount = 6L,
|
||||||
commentCount = 5L,
|
commentCount = 5L,
|
||||||
existOrdered = false
|
existOrdered = existOrdered
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,17 @@ class HomeMainFragmentLoginGuardSourceTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment 라이브 전체 버튼은 Fragment에서 공통 라이브 화면으로 이동한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
val setupSource = source.substringFrom("private fun setUpRecommendationAdapters()")
|
||||||
|
assertTrue(setupSource.contains("binding.tvHomeLiveAll.setOnClickListener { openHomeOnAirLive() }"))
|
||||||
|
assertFalse(setupSource.contains("liveAdapter.setOnMoreClick"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
|
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.widget.feed
|
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -102,4 +104,71 @@ class FeedItemTest {
|
|||||||
assertEquals(5, item.commentCount)
|
assertEquals(5, item.commentCount)
|
||||||
assertEquals(6, item.likeCount)
|
assertEquals(6, item.likeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `locked paid community item hides reactions and comments`() {
|
||||||
|
val item = communityItem(
|
||||||
|
price = 100,
|
||||||
|
existOrdered = false,
|
||||||
|
hideReactionWhenLocked = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(item.isLocked)
|
||||||
|
assertFalse(item.showReaction)
|
||||||
|
assertFalse(item.showComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `locked paid community item preserves reactions by default`() {
|
||||||
|
val item = communityItem(price = 100, existOrdered = false)
|
||||||
|
|
||||||
|
assertTrue(item.isLocked)
|
||||||
|
assertTrue(item.showReaction)
|
||||||
|
assertTrue(item.showComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `free community item with comments disabled keeps likes only`() {
|
||||||
|
val item = communityItem(price = 0, isCommentAvailable = false)
|
||||||
|
|
||||||
|
assertFalse(item.isLocked)
|
||||||
|
assertTrue(item.showReaction)
|
||||||
|
assertFalse(item.showComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `community item preserves pinned state`() {
|
||||||
|
val item = communityItem(isPinned = true)
|
||||||
|
|
||||||
|
assertTrue(item.isPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `community item hides more action by default`() {
|
||||||
|
assertFalse(communityItem().showMore)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun communityItem(
|
||||||
|
price: Int = 0,
|
||||||
|
existOrdered: Boolean = false,
|
||||||
|
isPinned: Boolean = false,
|
||||||
|
isCommentAvailable: Boolean = true,
|
||||||
|
hideReactionWhenLocked: Boolean = false
|
||||||
|
) = FeedItem.Community(
|
||||||
|
feedId = "feed-community-policy",
|
||||||
|
creatorId = "creator-1",
|
||||||
|
creatorName = "크리에이터 이름",
|
||||||
|
creatorImageUrl = "https://example.com/profile.png",
|
||||||
|
postId = "post-policy",
|
||||||
|
bodyText = "커뮤니티 본문",
|
||||||
|
keywordText = "",
|
||||||
|
createdAtText = "2분 전",
|
||||||
|
commentCount = 5,
|
||||||
|
likeCount = 6,
|
||||||
|
price = price,
|
||||||
|
existOrdered = existOrdered,
|
||||||
|
isPinned = isPinned,
|
||||||
|
isCommentAvailable = isCommentAvailable,
|
||||||
|
hideReactionWhenLocked = hideReactionWhenLocked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,28 @@ class FeedViewTest {
|
|||||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `community more click은 현재 bind된 item만 전달한다`() {
|
||||||
|
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||||
|
val first = sampleCommunityItem(bodyText = "첫 번째", keywordText = "", showMore = true)
|
||||||
|
val second = sampleCommunityItem(bodyText = "두 번째", keywordText = "", showMore = true)
|
||||||
|
var clickedItem: FeedItem.Community? = null
|
||||||
|
var rootClickedItem: FeedItem? = null
|
||||||
|
|
||||||
|
view.setOnMoreClick { clickedItem = it }
|
||||||
|
view.setOnFeedClick { rootClickedItem = it }
|
||||||
|
view.bind(first)
|
||||||
|
view.bind(second)
|
||||||
|
view.findViewById<View>(R.id.btn_feed_community_more).performClick()
|
||||||
|
|
||||||
|
assertSame(second, clickedItem)
|
||||||
|
assertEquals(null, rootClickedItem)
|
||||||
|
|
||||||
|
view.performClick()
|
||||||
|
|
||||||
|
assertSame(second, rootClickedItem)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `community layout uses runtime image height instead of fixed crop height`() {
|
fun `community layout uses runtime image height instead of fixed crop height`() {
|
||||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||||
@@ -201,7 +223,8 @@ class FeedViewTest {
|
|||||||
audioUrl: String? = null,
|
audioUrl: String? = null,
|
||||||
price: Int = 0,
|
price: Int = 0,
|
||||||
existOrdered: Boolean = false,
|
existOrdered: Boolean = false,
|
||||||
showKeyword: Boolean = true
|
showKeyword: Boolean = true,
|
||||||
|
showMore: Boolean = false
|
||||||
) = FeedItem.Community(
|
) = FeedItem.Community(
|
||||||
feedId = "feed-community-1",
|
feedId = "feed-community-1",
|
||||||
creatorId = "creator-1",
|
creatorId = "creator-1",
|
||||||
@@ -217,7 +240,8 @@ class FeedViewTest {
|
|||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
price = price,
|
price = price,
|
||||||
existOrdered = existOrdered,
|
existOrdered = existOrdered,
|
||||||
showKeyword = showKeyword
|
showKeyword = showKeyword,
|
||||||
|
showMore = showMore
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Int.dpToPx(): Int {
|
private fun Int.dpToPx(): Int {
|
||||||
|
|||||||
@@ -1353,6 +1353,119 @@ git diff --check
|
|||||||
- 2026-07-31: 사용자 인터뷰에서 라이브 0개는 기존처럼 섹션을 숨기고 1개 이상이면 최대 20개 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. 기존 20개 상한·`전체` 디자인·클릭 목적지·로그인 가드·API 계약을 유지하고, `HomeLiveAdapter` 내부 상태명과 조건 및 `HomeMainFragmentLayoutTest` 경계 테스트만 변경하는 Phase 18을 작성했다. production·test 구현은 시작하지 않았다.
|
- 2026-07-31: 사용자 인터뷰에서 라이브 0개는 기존처럼 섹션을 숨기고 1개 이상이면 최대 20개 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. 기존 20개 상한·`전체` 디자인·클릭 목적지·로그인 가드·API 계약을 유지하고, `HomeLiveAdapter` 내부 상태명과 조건 및 `HomeMainFragmentLayoutTest` 경계 테스트만 변경하는 Phase 18을 작성했다. production·test 구현은 시작하지 않았다.
|
||||||
- 2026-07-31: Phase 18을 TDD로 구현했다. RED는 라이브 1개에서 `itemCount`가 기대 2, 실제 1로 실패했고, GREEN은 `showAllItem = items.isNotEmpty()` 최소 변경 후 focused 74개 테스트 성공으로 확인했다. 전체 1,199 tests와 compile·ktlint·diff 검증도 성공했으며 기존 `MoreViewHolder`·클릭 연결·20개 cap은 변경하지 않았다.
|
- 2026-07-31: Phase 18을 TDD로 구현했다. RED는 라이브 1개에서 `itemCount`가 기대 2, 실제 1로 실패했고, GREEN은 `showAllItem = items.isNotEmpty()` 최소 변경 후 focused 74개 테스트 성공으로 확인했다. 전체 1,199 tests와 compile·ktlint·diff 검증도 성공했으며 기존 `MoreViewHolder`·클릭 연결·20개 cap은 변경하지 않았다.
|
||||||
|
|
||||||
|
## Phase 19: 현재 진행 중인 라이브 `전체` 항목 오른쪽 고정
|
||||||
|
|
||||||
|
**Phase 결과:** 메인 홈 추천 탭에서 라이브 목록만 가로 스크롤되고, `전체` 항목은 스크롤 위치와 관계없이 오른쪽 끝의 예약된 영역에 고정된다.
|
||||||
|
|
||||||
|
**선행 조건:** Phase 18 완료, `prd.md`의 `2026-08-03 변경: 현재 진행 중인 라이브 전체 항목 오른쪽 고정` 승인.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P19-T1`, `P19-R1`, `P19-GATE` 완료, RED/GREEN 로직 회귀와 resource merge·compile·lint 결과를 Task 검증 기록과 `Verification Log`에 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 19.1: 스크롤 목록과 고정 `전체` 항목 분리
|
||||||
|
|
||||||
|
**Goal 실행 `P19-T1`:** `HomeLiveAdapter`는 최대 20개의 라이브 item만 제공하고, `fragment_v2_main_home.xml`의 고정 `전체` 뷰가 기존 로그인 가드 경로를 호출한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Phase 19 선행 조건 충족.
|
||||||
|
- **완료 증거:** RED/GREEN 체크박스 완료, 아래 S1~S3와 인접 라우팅 회귀 자동 검증, focused test·resource merge·compile·ktlint·diff 결과 누적.
|
||||||
|
- **범위 밖:** `전체` 디자인·문구·클릭 목적지·로그인 가드 변경, 라이브 최대 20개 정책·item 순서·입장 흐름 변경, 추천/전체 라이브 API 변경, overlay·커스텀 LayoutManager·신규 abstraction·dependency 추가, 레거시 홈 수정.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt`
|
||||||
|
- Modify: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `HomeLiveAdapter.submitItems(items)`, `MAX_VISIBLE_LIVE_COUNT = 20`, `HomeMainFragment.openHomeOnAirLive()`의 기존 `ensureV2Access(AccessRequirement.Login)` 계약.
|
||||||
|
- Produces: adapter의 `0개 -> 0`, `1~20개 -> 원본 개수`, `21개 이상 -> 20` item 계약과, `ll_home_live_section`의 스크롤 `rv_home_lives` + 고정 `tv_home_live_all` 형제 구조.
|
||||||
|
|
||||||
|
**시나리오 계약:**
|
||||||
|
|
||||||
|
- `S1` 빈 목록: adapter item 수가 0이고 `bindLiveSection()`의 기존 빈 목록 숨김 로직을 유지한다. 자동 증거는 `HomeMainFragmentLayoutTest`의 adapter 경계 로직 테스트다.
|
||||||
|
- `S2` 라이브 1~20개: adapter item 수가 입력 개수와 같고 adapter가 별도 `전체` item을 만들지 않는다. 자동 증거는 `HomeMainFragmentLayoutTest`의 adapter 경계 로직 테스트다.
|
||||||
|
- `S3` 라이브 21개 이상 및 인접 회귀: adapter item 수가 정확히 20이고, 고정 `전체` 터치는 기존 `openHomeOnAirLive()` 로그인 가드를 사용하며 라이브 item은 기존 `LiveActionCoordinator` 흐름을 유지한다. 자동 증거는 `HomeMainFragmentLayoutTest`와 `HomeMainFragmentLoginGuardSourceTest`다.
|
||||||
|
|
||||||
|
- [x] **RED:** `HomeMainFragmentLayoutTest`의 adapter 계약을 `전체` item 포함 방식에서 0개·1개·20개·21개 이상 라이브 item 수만 검증하는 로직 테스트로 변경한다. View 크기·간격·부모 관계·visibility 같은 레이아웃 속성 테스트는 추가하지 않는다.
|
||||||
|
- [x] **RED:** `HomeMainFragmentLoginGuardSourceTest`에 `setUpRecommendationAdapters()`가 `binding.tvHomeLiveAll.setOnClickListener { openHomeOnAirLive() }`를 연결하고 `liveAdapter.setOnMoreClick`을 사용하지 않는지 검증하는 한글 테스트를 추가한다. 기존 `openHomeOnAirLive()` 로그인 가드와 라이브 item 공통 Action 검증은 유지한다.
|
||||||
|
- [x] **RED 확인:** 아래 focused test를 실행해 adapter의 추가 `전체` item과 기존 `setOnMoreClick` 연결 때문에 신규 로직 assertion이 의도대로 실패하는지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" \
|
||||||
|
--no-parallel --console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `fragment_v2_main_home.xml`에서 `ll_home_live_section`을 수평 컨테이너로 바꾸고, `rv_home_lives`는 `0dp + weight 1` 및 시작 padding을 사용하며, 기존 스타일과 끝 여백을 유지한 `tv_home_live_all`을 오른쪽 형제 뷰로 추가한다.
|
||||||
|
- [x] **GREEN:** `HomeLiveAdapter`에서 `showAllItem`, `onMoreClick`, `MoreViewHolder`, `createMoreView()`, view type 분기를 제거하고 최대 20개의 라이브 item만 bind한다. `HomeMainFragment`는 고정 뷰 터치를 기존 `openHomeOnAirLive()`에 직접 연결한다.
|
||||||
|
- [x] **GREEN 확인:** RED 확인과 같은 focused test를 다시 실행해 S1~S3와 기존 로그인 가드·라이브 item 라우팅 검증이 성공하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 이번 변경으로 사용되지 않는 import·adapter 코드만 제거하고 아래 직접 영향 회귀 및 품질 명령을 실행해 결과를 검증 기록에 누적한다. UI/계측 테스트, 기기·에뮬레이터 조작, 스크린샷·시각 QA는 실행하지 않는다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck \
|
||||||
|
--no-parallel --console=plain
|
||||||
|
./gradlew :app:testDebugUnitTest --no-parallel --console=plain
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
검증 기록:
|
||||||
|
|
||||||
|
- 2026-08-03: 사용자 승인과 코드 조사 결과를 기준으로 Phase 19 계획을 작성했다. `전체`는 overlay가 아닌 예약된 형제 영역에 고정하고, Phase 18의 상시 표시·빈 목록 숨김·20개 cap·기존 디자인·라우팅 계약은 유지한다. production·test 구현은 시작하지 않았다.
|
||||||
|
- 2026-08-03: `HomeMainFragmentLayoutTest`와 `HomeMainFragmentLoginGuardSourceTest`에 Phase 19 실패 테스트를 먼저 반영했다. focused 실행은 89 tests 중 신규 계약 4개만 실패했다. 실패는 수평 컨테이너 미적용(`HomeMainFragmentLayoutTest.kt:1043`), 라이브 1개 및 22개 입력에서 adapter가 `전체`를 추가해 기대 개수와 불일치(`:1088`, `:1103`), 고정 `tvHomeLiveAll` 클릭 연결 부재(`HomeMainFragmentLoginGuardSourceTest.kt:220`)로 확인되어 요구 동작 미구현에 따른 의도한 RED다.
|
||||||
|
- 2026-08-03: `fragment_v2_main_home.xml`에 예약 폭을 사용하는 고정 `tv_home_live_all` 형제 뷰를 추가하고, `HomeLiveAdapter`는 최대 20개의 라이브 item만 제공하도록 adapter 소유 `전체` 상태·ViewHolder·callback을 제거했다. `HomeMainFragment`는 고정 뷰 터치를 기존 `openHomeOnAirLive()`에 직접 연결했다. 동일 focused test는 GREEN으로 `BUILD SUCCESSFUL`이었고 `--rerun-tasks` 강제 재실행도 성공했다. `mergeDebugResources`, `compileDebugKotlin`, `ktlintCheck`, `git diff --check`도 성공했으며 ktlint 설정 및 Gradle/Kotlin deprecation warning은 기존 경고다.
|
||||||
|
- 2026-08-04: 사용자 확인에 따라 Phase 19의 최종 테스트 범위를 로직/local unit test로 정정했다. 임시 `app/src/androidTest` 계측 테스트, 레이아웃 속성 unit test, 스크린샷·시각 QA 산출물과 QA 전용 에뮬레이터를 제거했다. 앞선 레이아웃 assertion RED 기록은 최종 완료 증거에서 제외하고 adapter 개수 경계와 로그인 가드·라이브 라우팅 로직 테스트만 유지한다.
|
||||||
|
- 2026-08-04: 코드 리뷰 `REV-P19-001`에서 초기 `Loading` 상태가 content를 bind하지 않아 XML 기본 `VISIBLE`인 고정 `전체`가 단독 노출될 수 있음을 확인했다. 저장소 정책상 레이아웃 속성 테스트는 남기지 않고 `ll_home_live_section` 기본값을 `GONE`으로 최소 수정했으며 기존 `bindLiveSection()`의 빈 목록 숨김·비어 있지 않은 목록 표시 로직은 유지했다.
|
||||||
|
- 2026-08-04: focused `HomeMainFragmentLayoutTest`·`HomeMainFragmentLoginGuardSourceTest`, `mergeDebugResources`, `compileDebugKotlin`이 성공했다. 전체 `testDebugUnitTest` 첫 실행은 이번 diff 밖의 기존 `CreatorChannelHomeViewModelTest` 비결정적 assertion 1건이 실패했지만 해당 클래스 격리 실행과 전체 재실행은 성공했다. 전역 `ktlintCheck`는 기존 `audio_content` package underscore 등 이번 diff 밖 위반으로 실패했고 변경된 Kotlin/test 파일은 오류 목록에 없었다.
|
||||||
|
- 2026-08-04: 독립 최종 리뷰에서 adapter item-count 테스트에 남은 미사용 `context`·`RecyclerView` setup 3줄을 제거했다. focused 로직 테스트와 `git diff --check`를 재실행해 성공했고 재리뷰에서 차단 사항 없이 `APPROVE` 판정을 받았다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] 라이브 목록만 가로 스크롤되고 `전체` 항목은 오른쪽 끝에 고정된다.
|
||||||
|
- [x] 고정 `전체` 항목이 라이브 item을 가리지 않고 별도 영역을 차지한다.
|
||||||
|
- [x] 빈 목록은 adapter item 0개이며 라이브 섹션 전체가 숨겨진다.
|
||||||
|
- [x] 라이브 1~20개는 adapter item 수가 입력 개수와 같고, 21개 이상은 정확히 20개다.
|
||||||
|
- [x] 기존 `전체` 디자인·문구·끝 여백·클릭 목적지·로그인 가드와 라이브 item 간격·입장 흐름이 유지된다.
|
||||||
|
- [x] 신규 abstraction·dependency·overlay·커스텀 LayoutManager·레거시 수정·commit이 없다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Task R19.1: 초기 로딩 중 고정 `전체` 단독 노출 방지
|
||||||
|
|
||||||
|
**Goal 실행 `P19-R1`:** `REV-P19-001`에서 확인한 초기 데이터 바인딩 전 고정 `전체` 단독 노출 가능성을 제거한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P19-001` 확정, `HomeRecommendationUiState.Loading`이 content를 bind하지 않는 현재 흐름 확인.
|
||||||
|
- **완료 증거:** `ll_home_live_section` 기본 숨김, 기존 `bindLiveSection()` 표시 정책 유지, resource merge·compile과 Phase 19 로직 회귀 성공.
|
||||||
|
- **범위 밖:** UI/계측 테스트, 레이아웃 속성 unit test, 로딩 상태 구조 변경, 관련 없는 홈 섹션 수정.
|
||||||
|
- **TDD 예외 사유:** 수정 대상이 XML 초기 visibility 속성이며 저장소 정책상 레이아웃 속성 테스트를 작성하지 않는다. 코드 흐름 대조와 resource merge·compile로 검증한다.
|
||||||
|
|
||||||
|
- [x] `ll_home_live_section`을 XML에서 기본 `GONE`으로 설정한다.
|
||||||
|
- [x] `bindLiveSection()`이 빈 목록은 숨기고 비어 있지 않은 목록은 표시하는 기존 로직을 유지한다.
|
||||||
|
- [x] focused 로직 테스트, resource merge, compile, ktlint와 diff check를 실행해 결과를 기록한다.
|
||||||
|
|
||||||
|
#### Phase 19 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P19-GATE`:** S1~S3의 adapter 경계·라우팅 로직과 resource/compile 계약을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P19-T1`, `P19-R1` 체크박스와 검증 기록 완료.
|
||||||
|
- **완료 증거:** focused·전체 unit test, resource merge, compile, ktlint 결과 분리, diff check와 아래 자동·정적 대조 기록.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, 범위 밖 실패 수정, API·레거시·디자인 변경.
|
||||||
|
|
||||||
|
자동·정적 대조:
|
||||||
|
|
||||||
|
- [x] adapter가 빈 입력 0개, 1~20개 입력 원본 개수, 21개 이상 입력 20개를 반환한다.
|
||||||
|
- [x] adapter에 `전체` item용 view type·ViewHolder·callback이 남지 않는다.
|
||||||
|
- [x] `tv_home_live_all`은 기존 `openHomeOnAirLive()` 로그인 가드 경로를 사용한다.
|
||||||
|
- [x] resource merge와 compile이 성공하고 `app/src/androidTest` 신규 테스트가 없다.
|
||||||
|
- [x] UI/계측 테스트, 기기·에뮬레이터 조작, 스크린샷·시각 QA를 완료 증거로 사용하지 않는다.
|
||||||
|
|
||||||
|
중단 조건: S1~S3 로직 회귀, 전체 unit test, resource merge, compile, ktlint, diff check와 리뷰 승인이 끝나면 즉시 중단한다.
|
||||||
|
|
||||||
## Decision Log
|
## Decision Log
|
||||||
|
|
||||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
@@ -1361,8 +1474,13 @@ git diff --check
|
|||||||
| 2026-07-31 | `DEC-018` | 확정 | Phase 7의 2026-06-05 보안 재리뷰에서 승인된 유료 미구매 원본 이미지 미로드 정책을 기준으로 삼고, commit `2d58a876`에서 재도입된 client-side blur 원본 로드를 회귀로 수정한다. | 보안 재리뷰·검증 기록, 현재 `HomePopularCommunityAdapter`와 반대 방향의 test 변경 이력 교차 검토 | `REV-P7-001`, `P7-R1` |
|
| 2026-07-31 | `DEC-018` | 확정 | Phase 7의 2026-06-05 보안 재리뷰에서 승인된 유료 미구매 원본 이미지 미로드 정책을 기준으로 삼고, commit `2d58a876`에서 재도입된 client-side blur 원본 로드를 회귀로 수정한다. | 보안 재리뷰·검증 기록, 현재 `HomePopularCommunityAdapter`와 반대 방향의 test 변경 이력 교차 검토 | `REV-P7-001`, `P7-R1` |
|
||||||
| 2026-07-31 | `DEC-019` | 확정 | 두 번째 `Phase 12`와 그 하위 `Task 10.*`는 기존 기록을 보존하면서 `P12B` 계열 고유 별칭을 추가해 이후 리뷰와 회귀 작업에서 사용한다. | `work-plan-docs.md`의 Goal/Task 추적성 규칙과 현재 문서의 중복 식별자 | `REV-P12B-001`, `P12B-R1` |
|
| 2026-07-31 | `DEC-019` | 확정 | 두 번째 `Phase 12`와 그 하위 `Task 10.*`는 기존 기록을 보존하면서 `P12B` 계열 고유 별칭을 추가해 이후 리뷰와 회귀 작업에서 사용한다. | `work-plan-docs.md`의 Goal/Task 추적성 규칙과 현재 문서의 중복 식별자 | `REV-P12B-001`, `P12B-R1` |
|
||||||
| 2026-07-31 | `DEC-020` | 확정 | 현재 진행 중인 라이브가 0개이면 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시한다. | 사용자 인터뷰 승인과 기존 `HomeLiveAdapter`·전체 라이브 이동 계약 확인 | `P18-T1`, `P18-GATE`, `prd.md`의 `2026-07-31 변경: 현재 진행 중인 라이브 전체 항목 상시 표시` |
|
| 2026-07-31 | `DEC-020` | 확정 | 현재 진행 중인 라이브가 0개이면 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시한다. | 사용자 인터뷰 승인과 기존 `HomeLiveAdapter`·전체 라이브 이동 계약 확인 | `P18-T1`, `P18-GATE`, `prd.md`의 `2026-07-31 변경: 현재 진행 중인 라이브 전체 항목 상시 표시` |
|
||||||
|
| 2026-08-03 | `DEC-021` | 확정 | Phase 18의 `전체` 상시 표시 정책은 유지하되 adapter 마지막 item 배치를 폐기하고, 라이브 목록과 영역을 나누는 오른쪽 고정 형제 뷰로 변경한다. | 사용자 권고안 승인, 현재 수평 RecyclerView·adapter·로그인 가드 흐름 조사 | `P19-T1`, `P19-GATE`, `prd.md`의 `2026-08-03 변경: 현재 진행 중인 라이브 전체 항목 오른쪽 고정` |
|
||||||
|
| 2026-08-04 | `DEC-022` | 확정 | 자동 테스트는 `app/src/test`의 로직/local unit test만 사용하고, 사용자 명시 요청 없이는 UI/계측 테스트·기기/에뮬레이터 조작·스크린샷·시각 QA를 실행하지 않는다. | 사용자 직접 확인과 기존 `code-style.md`의 로직 테스트 전용 원칙 | `AGENTS.md`, 테스트/계획 가이드, `P19-T1`, `P19-R1`, `P19-GATE` |
|
||||||
|
|
||||||
## Verification Log
|
## Verification Log
|
||||||
|
- 2026-08-04: 사용자 확인에 따라 UI 테스트 없이 로직/local unit test만 수행하는 정책을 `AGENTS.md`, `build-test-style.md`, `code-style.md`, `work-plan-docs.md`, `sample-plan-task.md`에 명시했다. Phase 19의 임시 `androidTest`, 레이아웃 속성 테스트, 스크린샷·시각 QA 산출물과 QA 전용 에뮬레이터를 제거하고 계획·Gate를 adapter 경계와 라우팅 로직 검증 기준으로 정정했다. focused 로직 테스트와 resource merge·compile은 성공했고, 전체 unit test는 기존 비결정적 1건 격리 성공 후 전체 재실행이 성공했다. 전역 ktlint는 이번 diff 밖의 기존 위반으로 실패해 분리 기록했다.
|
||||||
|
- 2026-08-04: Phase 19 최종 독립 리뷰의 유일한 차단 사항이었던 item-count 테스트 미사용 UI setup을 제거했다. focused 로직 테스트와 diff check 성공 후 재리뷰가 `APPROVE`로 종료됐으며 UI/계측 테스트는 추가하거나 재실행하지 않았다.
|
||||||
|
- 2026-08-03: Phase 19 문서 준비로 현재 v2 홈 데이터·adapter·layout·클릭 흐름과 최근 Phase 18 변경을 확인하고, 사용자 승인 설계를 PRD와 `P19-T1`·`P19-GATE`에 반영했다. S1 빈 목록, S2 1~20개 스크롤 고정, S3 21개 이상 cap·기존 라우팅 회귀를 자동·표면 검증 계약으로 정의했다. production·test 구현은 시작하지 않았다.
|
||||||
- 2026-07-31: Phase 18 변경 코드 리뷰 및 최신 검증을 수행했다. `HomeLiveAdapter`의 0개·1~20개·21개 이상 경계와 기존 `MoreViewHolder`, 로그인 가드, `HomeOnAirLiveActivity` 이동 경로를 대조해 확정 발견 사항이 없음을 판정했다. focused 74 tests·compile·ktlint와 `--rerun-tasks` 전체 162 suites·1,199 tests가 실패 0·오류 0으로 성공했고 staged·unstaged diff check와 문서 변경 후 `./gradlew tasks --all --console=plain`도 성공했다. 프로젝트 전체 `lintDebug`는 이번 diff 밖의 기존 28 errors·767 warnings로 실패했으며 첫 오류는 `AndroidManifest.xml:316`의 `FacebookActivity` `MissingClass`다. 상세 결과는 `reviews/phase18-live-all-item-review.md`에 기록했다.
|
- 2026-07-31: Phase 18 변경 코드 리뷰 및 최신 검증을 수행했다. `HomeLiveAdapter`의 0개·1~20개·21개 이상 경계와 기존 `MoreViewHolder`, 로그인 가드, `HomeOnAirLiveActivity` 이동 경로를 대조해 확정 발견 사항이 없음을 판정했다. focused 74 tests·compile·ktlint와 `--rerun-tasks` 전체 162 suites·1,199 tests가 실패 0·오류 0으로 성공했고 staged·unstaged diff check와 문서 변경 후 `./gradlew tasks --all --console=plain`도 성공했다. 프로젝트 전체 `lintDebug`는 이번 diff 밖의 기존 28 errors·767 warnings로 실패했으며 첫 오류는 `AndroidManifest.xml:316`의 `FacebookActivity` `MissingClass`다. 상세 결과는 `reviews/phase18-live-all-item-review.md`에 기록했다.
|
||||||
- 2026-07-31: Phase 18 TDD와 Gate 검증을 완료했다. 구현 전 focused baseline 73개 성공 후 신규 경계 테스트가 라이브 1개에서 `expected:<2> but was:<1>`로 RED 실패했고, `HomeLiveAdapter`의 `hasMore`를 `showAllItem`으로 바꾸고 `items.isNotEmpty()` 조건을 적용한 뒤 focused 74개가 성공했다. `:app:testDebugUnitTest --no-parallel --console=plain` 전체 실행은 162 suites, 1,199 tests, 실패 0·오류 0·skip 0으로 성공했고 `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`도 성공했다. 기존 deprecation warning 외 실패는 없었으며 신규 파일·dependency·commit은 생성하지 않았다.
|
- 2026-07-31: Phase 18 TDD와 Gate 검증을 완료했다. 구현 전 focused baseline 73개 성공 후 신규 경계 테스트가 라이브 1개에서 `expected:<2> but was:<1>`로 RED 실패했고, `HomeLiveAdapter`의 `hasMore`를 `showAllItem`으로 바꾸고 `items.isNotEmpty()` 조건을 적용한 뒤 focused 74개가 성공했다. `:app:testDebugUnitTest --no-parallel --console=plain` 전체 실행은 162 suites, 1,199 tests, 실패 0·오류 0·skip 0으로 성공했고 `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`도 성공했다. 기존 deprecation warning 외 실패는 없었으며 신규 파일·dependency·commit은 생성하지 않았다.
|
||||||
- 2026-07-31: Phase 18 문서 준비 검증으로 PRD의 0개·1~20개·21개 이상 표시 계약과 Non-Goal을 `P18-T1` 및 `P18-GATE`에 대조했고 누락된 확정 요구사항이 없음을 확인했다. `P18`, `Task 18`, `showAllItem` 추적 검색과 placeholder/TODO 검색, `git diff --check`가 성공했으며 변경 파일은 `prd.md`, `plan-task.md` 두 문서뿐이다. `./gradlew tasks --all --console=plain` 최초 sandbox 실행은 사용자 Gradle cache lock 접근 제한으로 실패했고, 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료됐다. production·test 구현은 시작하지 않았고 커밋은 생성하지 않았다.
|
- 2026-07-31: Phase 18 문서 준비 검증으로 PRD의 0개·1~20개·21개 이상 표시 계약과 Non-Goal을 `P18-T1` 및 `P18-GATE`에 대조했고 누락된 확정 요구사항이 없음을 확인했다. `P18`, `Task 18`, `showAllItem` 추적 검색과 placeholder/TODO 검색, `git diff --check`가 성공했으며 변경 파일은 `prd.md`, `plan-task.md` 두 문서뿐이다. `./gradlew tasks --all --console=plain` 최초 sandbox 실행은 사용자 Gradle cache lock 접근 제한으로 실패했고, 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료됐다. production·test 구현은 시작하지 않았고 커밋은 생성하지 않았다.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
- 추천 탭이 선택된 `HomeMainFragment`에 메인 홈 추천 UI를 구현한다.
|
- 추천 탭이 선택된 `HomeMainFragment`에 메인 홈 추천 UI를 구현한다.
|
||||||
- Figma에서 기존 widget으로 대응 가능한 항목은 기존 v2 widget을 우선 재사용한다.
|
- Figma에서 기존 widget으로 대응 가능한 항목은 기존 v2 widget을 우선 재사용한다.
|
||||||
- 재사용이 어려운 영역만 최소 범위 신규 UI 또는 adapter item으로 정의한다.
|
- 재사용이 어려운 영역만 최소 범위 신규 UI 또는 adapter item으로 정의한다.
|
||||||
- 현재 진행 중인 라이브가 1개 이상이면 라이브 가로 목록 마지막에 `전체` 항목을 항상 표시해 전체 라이브 목록으로 이동할 수 있게 한다.
|
- 현재 진행 중인 라이브가 1개 이상이면 가로 목록의 스크롤과 관계없이 오른쪽 끝에 `전체` 항목을 고정해 전체 라이브 목록으로 이동할 수 있게 한다.
|
||||||
- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다.
|
- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다.
|
||||||
- 최하단 사업자 정보 섹션은 표시하되, 기본 최대 3줄 말줄임과 `더보기`/`접기` 토글을 제공한다.
|
- 최하단 사업자 정보 섹션은 표시하되, 기본 최대 3줄 말줄임과 `더보기`/`접기` 토글을 제공한다.
|
||||||
- `HomeActiveCreatorItem.activityType`은 백엔드 code를 앱 내부 enum 또는 mapper로 변환하고, 표시 문구는 string resource 기반 다국어 처리를 적용한다.
|
- `HomeActiveCreatorItem.activityType`은 백엔드 code를 앱 내부 enum 또는 mapper로 변환하고, 표시 문구는 string resource 기반 다국어 처리를 적용한다.
|
||||||
@@ -56,7 +56,7 @@ Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을
|
|||||||
|
|
||||||
## 6. User Stories
|
## 6. User Stories
|
||||||
- 사용자는 홈에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 빠르게 탐색하고 싶다.
|
- 사용자는 홈에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 빠르게 탐색하고 싶다.
|
||||||
- 사용자는 현재 진행 중인 라이브가 적더라도 `전체` 항목을 통해 전체 라이브 목록 화면으로 이동하고 싶다.
|
- 사용자는 현재 진행 중인 라이브 목록을 어느 위치까지 스크롤했는지와 관계없이 오른쪽 끝의 `전체` 항목을 통해 전체 라이브 목록 화면으로 이동하고 싶다.
|
||||||
- 사용자는 배너를 스와이프하거나 터치해 관련 이벤트, 크리에이터, 시리즈, 외부 링크 또는 앱 내부 딥링크로 이동하고 싶다.
|
- 사용자는 배너를 스와이프하거나 터치해 관련 이벤트, 크리에이터, 시리즈, 외부 링크 또는 앱 내부 딥링크로 이동하고 싶다.
|
||||||
- 사용자는 최근 활동한 크리에이터의 활동 유형을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다.
|
- 사용자는 최근 활동한 크리에이터의 활동 유형을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다.
|
||||||
- 사용자는 여러 크리에이터를 한 번에 팔로우하기 전에 확인창에서 동작을 검토하고, 확인하거나 취소할 수 있기를 원한다.
|
- 사용자는 여러 크리에이터를 한 번에 팔로우하기 전에 확인창에서 동작을 검토하고, 확인하거나 취소할 수 있기를 원한다.
|
||||||
@@ -148,8 +148,9 @@ data class HomePopularCommunityPostItem(
|
|||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
- `lives`가 비어 있으면 기존 정책대로 라이브 섹션 전체를 숨기고 `전체` 항목도 표시하지 않는다.
|
- `lives`가 비어 있으면 기존 정책대로 라이브 섹션 전체를 숨기고 `전체` 항목도 표시하지 않는다.
|
||||||
- `lives`가 1개 이상 20개 이하이면 모든 라이브 항목 뒤에 `전체` 항목을 표시한다.
|
- `lives`가 1개 이상이면 라이브 목록과 영역을 나누는 형제 뷰로 `전체` 항목을 오른쪽 끝에 고정하며, 라이브 목록의 가로 스크롤에 함께 이동하지 않는다.
|
||||||
- `lives`가 21개 이상이면 기존 정책대로 라이브를 최대 20개까지 표시하고 마지막에 `전체` 항목을 표시한다.
|
- 고정 `전체` 항목은 라이브 목록을 가리지 않고 별도 폭을 차지하며, 라이브 목록만 남은 영역에서 스크롤된다.
|
||||||
|
- `lives`가 21개 이상이면 기존 정책대로 라이브를 최대 20개까지 표시하고 고정 `전체` 항목을 함께 표시한다.
|
||||||
- `전체` 항목의 기존 크기, 색상, typography와 클릭 시 `HomeOnAirLiveActivity`로 이동하는 동작을 유지한다.
|
- `전체` 항목의 기존 크기, 색상, typography와 클릭 시 `HomeOnAirLiveActivity`로 이동하는 동작을 유지한다.
|
||||||
- 라이브 item 표시 순서, item 클릭 입장 흐름, 추천 API와 전체 라이브 목록 API 계약은 변경하지 않는다.
|
- 라이브 item 표시 순서, item 클릭 입장 흐름, 추천 API와 전체 라이브 목록 API 계약은 변경하지 않는다.
|
||||||
|
|
||||||
@@ -404,7 +405,7 @@ HomeRecommendation 화면
|
|||||||
- API 모델은 기존 네트워크 스택과 JSON 파서 규칙을 따른다.
|
- API 모델은 기존 네트워크 스택과 JSON 파서 규칙을 따른다.
|
||||||
- string resource 기반 다국어 처리를 적용하며, UI 표시 문자열을 Kotlin 코드에 직접 하드코딩하지 않는다.
|
- string resource 기반 다국어 처리를 적용하며, UI 표시 문자열을 Kotlin 코드에 직접 하드코딩하지 않는다.
|
||||||
- Figma에 있는 기존 widget과 다른 신규 UI는 구현 계획에서 파일 경로와 테스트 범위를 명시한 뒤 최소 범위로 작성한다.
|
- Figma에 있는 기존 widget과 다른 신규 UI는 구현 계획에서 파일 경로와 테스트 범위를 명시한 뒤 최소 범위로 작성한다.
|
||||||
- 신규 순수 변환 로직(activity type mapper, count/time formatter 등)은 가능하면 local unit test로 검증한다.
|
- 신규 테스트는 `app/src/test`의 로직/local unit test로 한정한다. UI 변경은 adapter 분기, mapper, formatter, presentation model, 라우팅처럼 화면 표현을 결정하는 로직만 검증하고 레이아웃 속성·계측·스크린샷 테스트는 작성하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -418,7 +419,7 @@ HomeRecommendation 화면
|
|||||||
- `HomeActiveCreatorItem.creatorId`가 UI model까지 보존되고, `LIVE` item은 `targetId` 유무에 따라 라이브 룸 또는 크리에이터 채널 route로 분기된다.
|
- `HomeActiveCreatorItem.creatorId`가 UI model까지 보존되고, `LIVE` item은 `targetId` 유무에 따라 라이브 룸 또는 크리에이터 채널 route로 분기된다.
|
||||||
- 최근 활동 `LIVE`의 `targetId == null && creatorId <= 0L` 입력에서 route가 생성되지 않는다.
|
- 최근 활동 `LIVE`의 `targetId == null && creatorId <= 0L` 입력에서 route가 생성되지 않는다.
|
||||||
- 최근 활동 `LIVE`의 라이브 룸 진입이 `LiveActionCoordinator`를 사용해 로그인·본인인증·성인 콘텐츠 보기 설정 정책을 우회하지 않는다.
|
- 최근 활동 `LIVE`의 라이브 룸 진입이 `LiveActionCoordinator`를 사용해 로그인·본인인증·성인 콘텐츠 보기 설정 정책을 우회하지 않는다.
|
||||||
- 현재 진행 중인 라이브가 0개이면 라이브 섹션과 `전체` 항목이 표시되지 않고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목이 표시된다.
|
- 현재 진행 중인 라이브가 0개이면 라이브 섹션과 `전체` 항목이 표시되지 않고, 1개 이상이면 최대 20개의 스크롤 목록과 오른쪽 끝에 고정된 `전체` 항목이 표시된다.
|
||||||
- 제외 대상인 `추천 필모그래피`, `또 다른 모습` 섹션이 화면에 생성되지 않는다.
|
- 제외 대상인 `추천 필모그래피`, `또 다른 모습` 섹션이 화면에 생성되지 않는다.
|
||||||
- 최근 응원이 많은 크리에이터 모두 팔로우 API success 후 버튼이 `모두 팔로우 완료`와 `ic_new_following` 상태로 변경된다.
|
- 최근 응원이 많은 크리에이터 모두 팔로우 API success 후 버튼이 `모두 팔로우 완료`와 `ic_new_following` 상태로 변경된다.
|
||||||
- 최근 응원이 많은 크리에이터의 기본 상태 모두 팔로우 버튼 터치 직후에는 확인창만 표시되고 API 호출 횟수는 `0`이다.
|
- 최근 응원이 많은 크리에이터의 기본 상태 모두 팔로우 버튼 터치 직후에는 확인창만 표시되고 API 호출 횟수는 `0`이다.
|
||||||
@@ -441,6 +442,7 @@ HomeRecommendation 화면
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 12. 검증 기록
|
## 12. 검증 기록
|
||||||
|
- 2026-08-03: 사용자 승인으로 현재 진행 중인 라이브의 `전체` 항목을 adapter 마지막 item에서 분리해 오른쪽 끝 고정 형제 뷰로 배치하는 요구사항을 확정했다. 고정 뷰가 라이브 목록을 가리지 않도록 수평 컨테이너에서 영역을 나누고, 빈 목록 섹션 숨김·라이브 최대 20개·기존 디자인·문구·클릭 목적지·로그인 가드·라이브 item 입장 흐름은 유지한다. 구현 검증은 Phase 19의 로직/local unit test와 resource merge·compile 기준으로 진행한다.
|
||||||
- 2026-07-31: 현재 진행 중인 라이브 `전체` 항목 상시 표시 요구사항을 TDD로 구현했다. 라이브 1개 입력의 adapter item 수가 기대 2, 실제 1로 실패하는 RED를 확인한 뒤 `HomeLiveAdapter`가 비어 있지 않은 입력에서 `전체` 항목을 추가하도록 최소 수정했다. focused `HomeMainFragmentLayoutTest` 74개와 전체 162 suites·1,199 tests, debug Kotlin compile, ktlint, diff 검증이 성공했으며 0개 숨김·20개 cap·기존 `전체` 디자인과 클릭 경로는 유지했다.
|
- 2026-07-31: 현재 진행 중인 라이브 `전체` 항목 상시 표시 요구사항을 TDD로 구현했다. 라이브 1개 입력의 adapter item 수가 기대 2, 실제 1로 실패하는 RED를 확인한 뒤 `HomeLiveAdapter`가 비어 있지 않은 입력에서 `전체` 항목을 추가하도록 최소 수정했다. focused `HomeMainFragmentLayoutTest` 74개와 전체 162 suites·1,199 tests, debug Kotlin compile, ktlint, diff 검증이 성공했으며 0개 숨김·20개 cap·기존 `전체` 디자인과 클릭 경로는 유지했다.
|
||||||
- 2026-07-31: 사용자 인터뷰에서 현재 진행 중인 라이브가 0개이면 기존처럼 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. `HomeLiveAdapter.submitItems()`의 기존 `items.size > MAX_VISIBLE_LIVE_COUNT` 조건과 `HomeMainFragmentLayoutTest`의 21개 초과 회귀 테스트를 확인했으며, 기존 디자인·클릭 목적지·로그인 가드·API 계약은 변경하지 않는 최소 범위를 PRD에 반영했다.
|
- 2026-07-31: 사용자 인터뷰에서 현재 진행 중인 라이브가 0개이면 기존처럼 섹션을 숨기고, 1개 이상이면 최대 20개 라이브 뒤에 `전체` 항목을 항상 표시하는 정책을 확정했다. `HomeLiveAdapter.submitItems()`의 기존 `items.size > MAX_VISIBLE_LIVE_COUNT` 조건과 `HomeMainFragmentLayoutTest`의 21개 초과 회귀 테스트를 확인했으며, 기존 디자인·클릭 목적지·로그인 가드·API 계약은 변경하지 않는 최소 범위를 PRD에 반영했다.
|
||||||
- 2026-07-31: PRD와 계획/TASK 보완 후 신규 범위의 placeholder·모순·요구사항 추적성을 자체 검토했다. `git status --short`로 변경 파일이 두 문서뿐임을 확인했고, placeholder 검색과 `git diff --check`는 출력 없이 성공했다. `./gradlew tasks --all`은 최초 sandbox 실행에서 사용자 Gradle cache lock 접근 제한으로 실패했으나 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료되어 계획에 기재한 Gradle task 이름의 유효성을 확인했다. production·test·resource 파일은 변경하지 않았다.
|
- 2026-07-31: PRD와 계획/TASK 보완 후 신규 범위의 placeholder·모순·요구사항 추적성을 자체 검토했다. `git status --short`로 변경 파일이 두 문서뿐임을 확인했고, placeholder 검색과 `git diff --check`는 출력 없이 성공했다. `./gradlew tasks --all`은 최초 sandbox 실행에서 사용자 Gradle cache lock 접근 제한으로 실패했으나 승인된 동일 명령 재실행은 `BUILD SUCCESSFUL`로 완료되어 계획에 기재한 Gradle task 이름의 유효성을 확인했다. production·test·resource 파일은 변경하지 않았다.
|
||||||
@@ -528,3 +530,11 @@ HomeRecommendation 화면
|
|||||||
- 현재 진행 중인 라이브가 1개 이상이면 라이브 개수와 관계없이 목록 마지막에 `전체` 항목을 표시한다.
|
- 현재 진행 중인 라이브가 1개 이상이면 라이브 개수와 관계없이 목록 마지막에 `전체` 항목을 표시한다.
|
||||||
- 라이브는 기존처럼 최대 20개까지 표시하므로 21개 이상일 때도 `라이브 20개 + 전체` 구성을 유지한다.
|
- 라이브는 기존처럼 최대 20개까지 표시하므로 21개 이상일 때도 `라이브 20개 + 전체` 구성을 유지한다.
|
||||||
- `전체` 항목의 디자인·클릭 목적지·로그인 가드와 라이브 item의 입장 흐름은 변경하지 않는다.
|
- `전체` 항목의 디자인·클릭 목적지·로그인 가드와 라이브 item의 입장 흐름은 변경하지 않는다.
|
||||||
|
|
||||||
|
### 2026-08-03 변경: 현재 진행 중인 라이브 `전체` 항목 오른쪽 고정
|
||||||
|
- 2026-07-31 변경의 `전체` 상시 표시 정책은 유지하되, adapter 마지막 item으로 배치하는 방식만 대체한다.
|
||||||
|
- `전체` 항목은 가로 스크롤 목록 밖의 형제 뷰로 분리해 오른쪽 끝에 고정하며, 라이브 목록의 스크롤 위치와 관계없이 표시한다.
|
||||||
|
- 라이브 목록과 `전체` 항목은 수평 컨테이너의 영역을 나누며 서로 겹치지 않는다.
|
||||||
|
- 라이브가 0개이면 기존처럼 섹션 전체를 숨기고, 1개 이상이면 라이브 목록을 최대 20개까지 표시한다.
|
||||||
|
- `전체` 항목의 기존 `58dp x 102dp` 크기, 검은 배경, 가운데 정렬, `Typography.Body5`, `soda_400` 문구 색상과 `HomeOnAirLiveActivity` 이동·로그인 가드를 유지한다.
|
||||||
|
- 라이브 item 순서·간격·입장 흐름과 추천/전체 라이브 API 계약은 변경하지 않는다.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Phase 19 현재 라이브 전체 고정 리뷰
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 19 / `P19-T1`, `P19-R1`, `P19-GATE` |
|
||||||
|
| 기준 | 현재 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-04 |
|
||||||
|
| 기준 문서 | `prd.md`, `plan-task.md` |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 목적과 범위
|
||||||
|
|
||||||
|
- `HomeLiveAdapter`가 라이브 item만 최대 20개 제공하는지 확인한다.
|
||||||
|
- 고정 `전체` 클릭이 기존 로그인 가드 경로를 유지하는지 확인한다.
|
||||||
|
- 초기 데이터 바인딩 전 고정 `전체`가 단독 노출되지 않는지 코드 흐름을 확인한다.
|
||||||
|
- 테스트 범위는 `app/src/test`의 로직/local unit test로 제한한다.
|
||||||
|
- UI/계측 테스트, 레이아웃 속성 테스트, 기기·에뮬레이터 조작, 스크린샷·시각 QA는 제외한다.
|
||||||
|
|
||||||
|
## 3. 검토 근거
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragment.kt`, `HomeLiveAdapter.kt`, `fragment_v2_main_home.xml`
|
||||||
|
- 테스트: `HomeMainFragmentLayoutTest.kt`, `HomeMainFragmentLoginGuardSourceTest.kt`
|
||||||
|
- 문서: Phase 19 요구사항·Task·Gate와 테스트 가이드
|
||||||
|
|
||||||
|
## 4. 발견 사항
|
||||||
|
|
||||||
|
### REV-P19-001 — 초기 로딩 중 고정 전체 단독 노출 가능성
|
||||||
|
|
||||||
|
- **심각도:** High
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **소유 Task:** `P19-R1`
|
||||||
|
|
||||||
|
`HomeRecommendationUiState.Loading`은 content를 bind하지 않으므로 `ll_home_live_section`의 XML 기본값이 `VISIBLE`이면 첫 데이터 바인딩 전 고정 `전체`만 노출될 수 있었다. 부모 섹션 기본값을 `GONE`으로 변경하고 기존 `bindLiveSection()`의 빈 목록 숨김·비어 있지 않은 목록 표시 로직은 유지했다.
|
||||||
|
|
||||||
|
저장소 정책상 XML visibility를 직접 assertion하는 레이아웃 테스트는 남기지 않았다. resource merge·Kotlin compile과 기존 로직 회귀로 검증했다.
|
||||||
|
|
||||||
|
## 5. 실행한 검증
|
||||||
|
|
||||||
|
| 명령 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| focused `HomeMainFragmentLayoutTest`, `HomeMainFragmentLoginGuardSourceTest` | 성공 |
|
||||||
|
| `:app:mergeDebugResources :app:compileDebugKotlin` | 성공 |
|
||||||
|
| `:app:testDebugUnitTest` | 첫 실행 기존 비결정적 1건 실패, 관련 클래스 격리 및 전체 재실행 성공 |
|
||||||
|
| `:app:ktlintCheck` | 이번 diff 밖 기존 package underscore 등으로 실패; 변경 파일 신규 오류 없음 |
|
||||||
|
| UI/계측 테스트 | 정책에 따라 작성·실행하지 않음 |
|
||||||
|
|
||||||
|
독립 최종 리뷰에서 adapter item-count 테스트에 남은 미사용 UI setup 3줄이 발견되어 제거했다. focused 로직 테스트와 `git diff --check` 재실행 후 재리뷰가 `APPROVE`로 종료됐다.
|
||||||
|
|
||||||
|
## 6. 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| adapter 경계와 라우팅 로직 | 충족 |
|
||||||
|
| 초기 로딩 노출 회귀 수정 | 충족 |
|
||||||
|
| UI 테스트 제거 | 충족 |
|
||||||
|
| 테스트 가이드 명확화 | 충족 |
|
||||||
|
|
||||||
|
**최종 결론:** `REV-P19-001` 수정 검증 완료 및 독립 재리뷰 승인. 전역 ktlint의 기존 실패는 Phase 19 범위 밖으로 분리한다.
|
||||||
@@ -285,7 +285,694 @@
|
|||||||
- 검증 기록:
|
- 검증 기록:
|
||||||
- 2026-07-12: RED로 `CreatorChannelCommunityDetailViewModelTest.유료 미구매 게시물은 작성자가 아니면 reaction 현황과 댓글을 숨긴다`를 추가했고, production 변경 전 `showReaction` 미구현 컴파일 실패를 확인했다. 이후 상세 UI 모델/상태와 Activity binding에 잠금 조건을 반영해 focused 테스트 GREEN을 확인했다.
|
- 2026-07-12: RED로 `CreatorChannelCommunityDetailViewModelTest.유료 미구매 게시물은 작성자가 아니면 reaction 현황과 댓글을 숨긴다`를 추가했고, production 변경 전 `showReaction` 미구현 컴파일 실패를 확인했다. 이후 상세 UI 모델/상태와 Activity binding에 잠금 조건을 반영해 focused 테스트 GREEN을 확인했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 15: 커뮤니티 구매 진입점·표시 상태·상세 메뉴 정리
|
||||||
|
|
||||||
|
**Phase 결과:** 홈과 커뮤니티 탭은 게시글 상태를 동일하게 표시하고 상세 이동만 수행하며, 구매와 권한별 관리/신고는 상세에서 수행한다.
|
||||||
|
|
||||||
|
**선행조건:** PRD `F0-20260810` 승인, 홈 Response의 `isPinned`/`isCommentAvailable` 서버 계약 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P15-T1`~`P15-T5`와 `P15-GATE` 완료, Task별 검증 기록 및 문서 최하단 `Verification Log` 누적.
|
||||||
|
|
||||||
|
**실행 제약:** 2026-08-10 사용자 지시에 따라 이번 계획 작성 단계에서는 구현과 Git 커밋을 수행하지 않는다. 이후 구현도 별도 사용자 요청 전에는 시작하지 않으며, 이 Phase에는 commit 단계를 두지 않는다.
|
||||||
|
|
||||||
|
#### Phase 15 파일 구조
|
||||||
|
|
||||||
|
- 홈 계약/표시:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
|
||||||
|
- `app/src/main/res/layout/view_feed_community.xml`
|
||||||
|
- 커뮤니티 탭:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_community_list.xml`
|
||||||
|
- 확인만 수행: `app/src/main/res/layout/item_creator_channel_community_grid.xml`
|
||||||
|
- 상세/결과 전파:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/CreatorChannelCommunityDetailActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/CreatorChannelCommunityDetailViewModel.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt`
|
||||||
|
- `app/src/main/res/layout/activity_creator_channel_community_detail.xml`
|
||||||
|
- 기존 테스트 수정:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChangeTest.kt`
|
||||||
|
|
||||||
|
#### Task 15.1 홈 Response와 게시글 표시 정책
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T1`:** 홈 커뮤니티 Response의 고정/댓글 가능 계약과 잠금·reaction 표시 조건을 순수 로직으로 고정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD `Home Response Contract`, `Home Community Requirements` 승인.
|
||||||
|
- **완료 증거:** 신규 DTO 필드 파싱 및 `FeedItem.Community` 표시 정책 focused test PASS.
|
||||||
|
- **범위 밖:** View binding, 커뮤니티 탭, 상세 메뉴.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeModelsTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 홈 JSON `isPinned: Boolean`, `isCommentAvailable: Boolean`, 기존 `price`, `existOrdered`.
|
||||||
|
- Produces:
|
||||||
|
|
||||||
|
`FeedItem.Community`에 추가할 member:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val isPinned: Boolean = false
|
||||||
|
val isCommentAvailable: Boolean = true
|
||||||
|
val hideReactionWhenLocked: Boolean = false
|
||||||
|
val isLocked: Boolean
|
||||||
|
val showReaction: Boolean
|
||||||
|
val showComment: Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
- `isLocked = price > 0 && !existOrdered`
|
||||||
|
- `showReaction = !hideReactionWhenLocked || !isLocked`
|
||||||
|
- `showComment = showReaction && isCommentAvailable`
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelHomeModelsTest`의 홈 JSON에 `isPinned=true`, `isCommentAvailable=false` 커뮤니티를 넣고 두 필드 보존을 검증한다.
|
||||||
|
- [x] **RED:** `FeedItemTest`에 잠금 숨김 opt-in 유료 미구매는 reaction/comment 모두 false, 기본값 유료 미구매는 기존 reaction/comment 표시 유지, 댓글 불가 무료 글은 reaction true/comment false, 고정 상태 보존 케이스를 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령을 실행해 신규 DTO/property 미존재로 인한 컴파일 또는 assertion 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeModelsTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedItemTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelCommunityPostResponse`에 `@SerializedName("isPinned")`, `@SerializedName("isCommentAvailable")` 필드를 추가하고 `FeedItem.Community`에 다른 화면의 기존 표시를 유지하는 기본값 및 위 계산 property를 최소 구현한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test가 PASS하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 신규 별도 presentation class를 만들지 않고 `FeedItem.Community` 계산 property만 유지한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): DTO/property 추가 전 focused test의 미존재 컴파일 RED를 확인했다. 구현 및 `P15-R3` 범위 보정 후 홈 JSON 신규 필드, 잠금 opt-in/default, 댓글 불가, 고정 상태 테스트가 최종 Gate에서 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.2 홈 커뮤니티 표시 연결
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T2`:** 홈 커뮤니티 item이 잠금·댓글 가능·고정 상태를 표시하고 card 전체 상세 이동 외의 액션을 제공하지 않는다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P15-T1` 완료.
|
||||||
|
- **완료 증거:** `P15-T1` 정책을 실제 bind에 사용하고 resource merge/Kotlin compile PASS.
|
||||||
|
- **범위 밖:** 커뮤니티 탭 item과 상세 메뉴.
|
||||||
|
- **TDD 예외 사유:** XML view 위치·visibility 자체를 직접 검증하는 테스트는 저장소 규칙상 작성하지 않는다. 표시 분기는 `P15-T1` 순수 로직 test로 검증한다.
|
||||||
|
- **대체 검증 방법:** View binding id 생성, resource merge, Kotlin compile, 기존 `FeedViewTest` 회귀를 실행한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/view_feed_community.xml`
|
||||||
|
- Verify: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt`(수정 없이 기존 회귀만 실행)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `FeedItem.Community.isPinned`, `showReaction`, `showComment`.
|
||||||
|
- Produces: `FeedCommunityView.bind(item)`의 공지/reaction 상태와 기존 `FeedCommunityView` root click 계약.
|
||||||
|
|
||||||
|
- [x] `CreatorChannelCommunityPostResponse.toFeedCommunityItem()`이 `isPinned`, `isCommentAvailable`을 전달하도록 수정한다.
|
||||||
|
- [x] `view_feed_community.xml`에 커뮤니티 탭과 같은 공지 row와 reaction row/comment icon 식별 id를 추가한다.
|
||||||
|
- [x] `FeedCommunityView.bind()`가 공지 row, reaction row, 댓글 icon/count, 하트 icon/count를 `FeedItem.Community` 계산 property로 bind하도록 수정한다.
|
||||||
|
- [x] `CreatorChannelHomeSectionAdapter.bindCommunities()`의 `row.setOnClickListener { onCommunityClick(community.postId) }`만 item 액션으로 유지한다.
|
||||||
|
- [x] 아래 명령으로 focused 회귀 및 compile을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedItemTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"
|
||||||
|
./gradlew :app:mergeDebugResources :app:compileDebugKotlin
|
||||||
|
```
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 홈 mapper/bind/root click을 정적 대조했고 `P15-R2`에서 reaction id 위치를 정정했다. `FeedItemTest`, `FeedViewTest`, resource merge, Kotlin compile이 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.3 커뮤니티 탭 상세 이동 전용화와 가격 UI 통일
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T3`:** 커뮤니티 탭 리스트/그리드 item 액션을 상세 이동 하나로 제한하고 잠금 가격 UI를 홈/상세 구조로 통일한다.
|
||||||
|
|
||||||
|
- **시작 조건:** 기존 list/grid `onPostClick` 상세 routing 계약 확인.
|
||||||
|
- **완료 증거:** play/owner-more callback 제거, list/grid root 상세 이동 유지, mapper/routing focused test 및 compile PASS.
|
||||||
|
- **범위 밖:** 상세 화면 내부 구매·메뉴 동작.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_community_list.xml`
|
||||||
|
- Verify only: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
|
||||||
|
- Verify only: `app/src/main/res/layout/item_creator_channel_community_grid.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `CreatorChannelCommunityPostUiModel.postId`, `price`, `isLocked`, `showOwnerTopPrice`.
|
||||||
|
- Produces: list/grid adapter constructor의 단일 item callback `onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit`.
|
||||||
|
|
||||||
|
- [x] **RED:** routing/source 계약 테스트에 Fragment/list adapter가 `onPlayClick`, `onOwnerMoreClick`, `toggleCommunityAudio`, `onCreatorChannelCommunityOwnerMoreClicked`를 갖지 않고 list/grid root가 `onPostClick`만 호출한다는 케이스를 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령으로 현재 하위 액션이 남아 있어 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityMapperTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** Fragment의 media player lifecycle/재생 함수와 Host owner-more callback, list adapter의 play/owner-more callback 및 더보기 view를 제거한다.
|
||||||
|
- [x] **GREEN:** UI model/mapper에서 이번 변경으로 미사용이 된 `audioUrl`, `showPlayButton`, `showOwnerMore`만 제거하고 작성자 상단 가격인 `showOwnerTopPrice`는 유지한다.
|
||||||
|
- [x] **GREEN:** 기존 Fragment/layout/source test에서 제거된 play/owner-more의 positive assertion만 삭제하거나 상세 이동 단일 callback의 negative assertion으로 교체한다. View 크기·간격·constraint·visibility 속성 assertion은 새로 추가하지 않는다.
|
||||||
|
- [x] **GREEN:** 리스트 잠금 금액의 compound drawable `TextView`를 그리드/상세와 같은 별도 `ImageView(ic_bar_cash)` + 금액 `TextView` capsule 구조로 변경한다.
|
||||||
|
- [x] **GREEN:** 그리드는 이미 `lock icon + cash icon + 금액 capsule` 구조와 root 상세 이동만 사용하므로 신규 production 변경 없이 계약 일치만 확인한다.
|
||||||
|
- [x] **GREEN 확인:** focused test, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin`을 실행한다.
|
||||||
|
- [x] **REFACTOR:** play/owner-more 제거로 생긴 import·binding·Host 구현 orphan만 삭제하고 다른 탭 코드는 정리하지 않는다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 하위 액션 제거 계약의 4개 RED 실패를 확인한 뒤 list/grid root `onPostClick`만 유지했다. mapper/layout focused test, resource merge, Kotlin compile과 최종 Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.4 상세 더보기 권한과 게시글 mutation
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T4`:** 상세 작성자 영역 우측 끝에서 권한별 관리/신고 메뉴를 제공하고 성공/실패 상태를 정확히 전파한다.
|
||||||
|
|
||||||
|
- **시작 조건:** 상세 DTO의 `creatorId`, `price`, `existOrdered`, `isPinned` 계약과 기존 레거시 dialog/API 확인.
|
||||||
|
- **완료 증거:** 더보기 노출 4개 권한 케이스, 고정/삭제/신고 API와 event focused test PASS, resource merge/compile PASS.
|
||||||
|
- **범위 밖:** 홈/커뮤니티 탭 새로고침 실행.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/CreatorChannelCommunityDetailViewModel.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/CreatorChannelCommunityDetailActivity.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/activity_creator_channel_community_detail.xml`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailViewModelTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityDetailReplyUiContractTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces UI model fields:
|
||||||
|
|
||||||
|
`CreatorChannelCommunityPostDetailUiModel`에 추가할 member:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val isOwner: Boolean
|
||||||
|
val showMore: Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
- `isOwner = creatorId == SharedPreferenceManager.userId`
|
||||||
|
- `showMore = isOwner || price == 0 || existOrdered`
|
||||||
|
- Produces ViewModel methods/events:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun updateCommunityPostFixed()
|
||||||
|
fun deleteCommunityPost()
|
||||||
|
fun reportCommunityPost(reason: String)
|
||||||
|
val postDeletedEventLiveData: LiveData<Boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Produces repository method:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun reportCommunityPost(postId: Long, reason: String, token: String)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED:** 상세 ViewModel test에 작성자 유료 미구매=true, 일반 무료=true, 일반 유료 구매=true, 일반 유료 미구매=false인 `showMore` 케이스를 추가한다.
|
||||||
|
- [x] **RED:** 고정 성공은 `isPinned` 반전 요청 후 상세 재조회와 `postChangedEventLiveData=true`, 삭제 성공은 `postDeletedEventLiveData=true`, 신고는 `ReportType.COMMUNITY_POST` repository 호출만 수행하는 케이스를 추가한다.
|
||||||
|
- [x] **RED:** UI 계약 test에 `R.id.btn_creator_channel_community_detail_more` resource와 Activity의 modify launcher/기존 BottomSheet·신고 dialog 연결 계약을 추가한다. 위치·constraint·visibility 값은 test에서 직접 검증하지 않는다.
|
||||||
|
- [x] **RED 확인:** 아래 명령으로 신규 fields/methods 미존재 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest --tests \
|
||||||
|
"kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityDetailViewModelTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `CreatorChannelRepository.reportCommunityPost()`가 `ReportRequest(ReportType.COMMUNITY_POST, reason, communityPostId = postId)`를 기존 `ReportRepository`에 위임하도록 추가한다.
|
||||||
|
- [x] **GREEN:** DetailViewModel의 고정은 `updateCommunityPostFixed(postId, !content.post.isPinned, authToken())`, 삭제는 `ModifyCommunityPostRequest(creatorCommunityId = postId, isActive = false)`를 JSON `text/plain` RequestBody로 변환한 `modifyCommunityPost(postImage = null, ...)`, 신고는 신규 report method를 호출한다. 각 mutation의 중복 요청을 막고 실패 시 기존 Toast event만 내보낸다.
|
||||||
|
- [x] **GREEN:** 신고 성공은 API message 또는 기존 `character_comment_report_submitted` 문구를 Toast event로 전달하고 게시글 변경 event는 내보내지 않는다.
|
||||||
|
- [x] **GREEN:** 상세 layout에서 `layout_creator_channel_community_detail_price`와 `tv_creator_channel_community_detail_purchased`를 제거하고, 작성자 영역 우측 끝에 `@drawable/ic_seemore_vertical`, `@string/read_more` content description을 사용하는 `btn_creator_channel_community_detail_more`를 추가한다. 닉네임/작성 시간의 end constraint는 이 버튼 start에 연결한다.
|
||||||
|
- [x] **GREEN:** Activity는 `post.showMore`로 더보기 버튼 visibility를 bind한다.
|
||||||
|
- [x] **GREEN:** Activity에서 `CreatorCommunityPostMenuBottomSheetDialog`를 열어 작성자는 수정/삭제/고정, 일반 사용자는 신고를 연결한다. 수정은 기존 `CreatorCommunityModifyActivity`, 삭제 확인은 `V2ModalDialog`, 신고 입력은 `CreatorCommunityReportDialog`를 재사용한다.
|
||||||
|
- [x] **GREEN:** 상세 paywall의 `PurchaseCommunityPostDialog -> purchaseCommunityPost()` 연결은 유지하고 홈/커뮤니티 탭에는 구매 호출부가 없음을 확인한다.
|
||||||
|
- [x] **GREEN:** 수정 Activity `RESULT_OK`와 고정 성공 시 상세를 재조회하고 상위 `RESULT_OK`를 설정하며, 삭제 event 수신 시 `RESULT_OK` 설정 후 상세를 종료한다.
|
||||||
|
- [x] **GREEN 확인:** focused test, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin`을 실행한다.
|
||||||
|
- [x] **REFACTOR:** 신규 menu abstraction 또는 별도 popup을 만들지 않고 기존 BottomSheet/Dialog를 직접 재사용한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 신규 fields/methods/resource 미존재 RED를 확인한 뒤 권한 truth table, 고정·삭제·신고 성공/실패·중복 guard, 기존 dialog 재사용과 결과 전파를 구현했다. 커뮤니티 focused test와 resource/compile Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.5 상위 새로고침 전파와 목록 메뉴 orphan 제거
|
||||||
|
|
||||||
|
**Goal 실행 `P15-T5`:** 상세 변경 결과가 홈·커뮤니티 탭을 함께 갱신하고 목록에 남은 관리 메뉴 코드를 제거한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P15-T3`, `P15-T4` 완료.
|
||||||
|
- **완료 증거:** 상세 `RESULT_OK -> CommunityChange.Updated -> 홈+커뮤니티 refresh` routing test PASS, 제거된 callback 관련 compile PASS.
|
||||||
|
- **범위 밖:** 신고 후 새로고침, 다른 creator channel 탭.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChangeTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `resolveCommunityActivityResult(CommunityActivityResultSource.Detail, RESULT_OK) == CommunityChange.Updated`.
|
||||||
|
- Produces: `CommunityChange.Updated` 처리 시 `homeActionDelegate?.refreshHome()`과 `refreshCreatorChannelCommunity()` 호출.
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelActivitySourceTest`에 상세 `Updated` 처리 블록이 홈/커뮤니티 refresh를 모두 호출한다는 routing 계약을 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령으로 현재 `Updated`가 커뮤니티만 refresh하고 목록 전용 change가 남아 있어 실패함을 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.community.action.CommunityChangeTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `handleCommunityChange(Updated)`가 홈과 커뮤니티를 함께 갱신하도록 변경한다.
|
||||||
|
- [x] **GREEN:** 목록 owner-more 제거로 미사용이 된 `communityPostModifyLauncher`, `onCreatorChannelCommunityOwnerMoreClicked`, 목록용 고정/삭제/dialog 함수와 관련 import/repository 의존을 `CreatorChannelActivity`에서 제거한다.
|
||||||
|
- [x] **GREEN:** 호출자가 사라진 `CommunityChange.Deleted`, `CommunityChange.PinChanged`를 제거하고 `CommunityChangeTest`의 삭제/고정 전용 케이스만 제거한다. `Write -> Created`, `Modify/Detail -> Updated`, 취소 -> `Ignored` 회귀는 유지한다.
|
||||||
|
- [x] **GREEN 확인:** focused test와 `./gradlew :app:compileDebugKotlin`을 실행한다.
|
||||||
|
- [x] **REFACTOR:** 다른 화면에서 사용 중인 `CommunityActionCommand.PostDetail`, `CommunityChange.Created/Updated`는 유지한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): routing RED 2건을 확인한 뒤 상세 `RESULT_OK`의 `Updated` 처리에서 홈·커뮤니티를 함께 갱신하고 목록 메뉴 orphan을 제거했다. Activity/action focused test와 Kotlin compile이 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.R1 목록 메뉴 제거 후 미사용 import 정리
|
||||||
|
|
||||||
|
**Goal 실행 `P15-R1`:** 확정 review 항목 `REV-P15-001`의 미사용 import를 제거해 Phase 15 style Gate를 회복한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P15-001` 확정, `P15-T5` 목록 메뉴/orphan 제거 완료.
|
||||||
|
- **대상 파일:** `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- **수정 범위:** `SharedPreferenceManager` 미사용 import 1줄 제거.
|
||||||
|
- **회귀 테스트:** `./gradlew :app:ktlintCheck`, Phase 15 Gate 전체.
|
||||||
|
- **완료 증거:** RED ktlint 정확한 1건 → import 제거 → ktlint/Gate PASS → review 보고서 수정 후 검증 누적.
|
||||||
|
- **범위 밖:** 다른 import 순서, 관련 없는 style 정리, `.editorconfig` deprecation 경고.
|
||||||
|
|
||||||
|
- [x] `./gradlew :app:ktlintCheck`에서 `CreatorChannelActivity.kt:29:1 Unused import`만 확정된 RED를 기록한다.
|
||||||
|
- [x] `SharedPreferenceManager` import 1줄만 제거한다.
|
||||||
|
- [x] `./gradlew :app:ktlintCheck`와 Phase 15 Gate를 재실행한다.
|
||||||
|
- [x] `plan-task.md` Verification Log와 `phase15-community-purchase-detail-menu-review.md`에 완료 증거를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 최초 Gate에서 `CreatorChannelActivity.kt:29:1 Unused import` RED를 확인하고 해당 import 1줄만 제거했다. 최종 `ktlintCheck`와 Phase 15 Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.R2 홈 reaction row binding 대상 정정
|
||||||
|
|
||||||
|
**Goal 실행 `P15-R2`:** 확정 review 항목 `REV-P15-002`의 잘못된 `ll_feed_community_reaction` id 위치를 정정해 유료 미구매 홈 item의 reaction UI만 숨긴다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P15-002` 확정, `P15-T2` 표시 정책 구현 완료.
|
||||||
|
- **대상 파일:** `app/src/main/res/layout/view_feed_community.xml`
|
||||||
|
- **수정 범위:** `ll_feed_community_reaction` id를 작성자 row에서 하트·댓글 reaction row로 이동.
|
||||||
|
- **회귀 검증:** `FeedItemTest`, `FeedViewTest`, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, Phase 15 Gate, XML tree 정적 재확인.
|
||||||
|
- **완료 증거:** 수정 전 XML tree에서 id가 프로필 row를 가리키는 근거 → id 1개 이동 → focused 회귀/Gate PASS → 리뷰 보고서 누적.
|
||||||
|
- **TDD 예외 사유:** 저장소 규칙상 View visibility·layout 속성을 직접 검증하는 신규 테스트를 추가하지 않는다. 표시 분기는 기존 `FeedItemTest`, binding/resource는 resource merge·compile·정적 XML tree 검토로 대체한다.
|
||||||
|
- **범위 밖:** 홈 card 간격·크기·제약, 다른 row 레이아웃.
|
||||||
|
|
||||||
|
- [x] 수정 전 `ll_feed_community_reaction` id가 프로필 row에 있고 실제 reaction row에는 id가 없는 근거를 기록한다.
|
||||||
|
- [x] id를 실제 reaction row로 이동한다.
|
||||||
|
- [x] focused 회귀, resource merge/compile, Phase 15 Gate를 재실행한다.
|
||||||
|
- [x] `plan-task.md` Verification Log와 review 보고서에 완료 증거를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 수정 전 id가 프로필 row에 연결되고 실제 reaction row에는 없음을 XML tree로 확인했다. id 1개를 실제 reaction row로 이동한 뒤 focused test, resource merge, Kotlin compile과 최종 Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.R3 공유 FeedItem reaction 정책 범위 정정
|
||||||
|
|
||||||
|
**Goal 실행 `P15-R3`:** 확정 review 항목 `REV-P15-003`의 공유 모델 회귀를 제거해 크리에이터 채널 홈에서만 유료 미구매 reaction을 숨기고 다른 `FeedItem.Community` 호출부는 기존 표시 동작을 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `REV-P15-003` 확정, `P15-T1`~`P15-T2` 표시 정책 구현 완료.
|
||||||
|
- **대상 파일:** `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`, `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`, `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt`, `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- **수정 범위:** `FeedItem.Community` 신규 정책은 기본값으로 기존 reaction을 유지하고, 크리에이터 채널 홈 mapper만 잠금 시 숨김을 명시하도록 제한.
|
||||||
|
- **회귀 테스트:** 잠금 item의 기본값은 reaction/comment 표시 유지, 크리에이터 채널 홈 opt-in은 reaction/comment 숨김, 기존 댓글 불가·고정 상태 테스트, Phase 15 Gate.
|
||||||
|
- **완료 증거:** 메인 홈 인기 커뮤니티 mapper와 공유 `FeedCommunityView` 호출 경로 확인 → 기본값 회귀 테스트 RED → 최소 opt-in 구현 → focused 회귀/Gate PASS → 리뷰 보고서 누적.
|
||||||
|
- **범위 밖:** 메인 홈 인기 커뮤니티의 이미지 잠금/가격 정책, 다른 feed UI 변경.
|
||||||
|
|
||||||
|
- [x] 메인 홈 인기 커뮤니티가 `price`, `existOrdered`를 전달하고 공유 `FeedCommunityView.bind()`를 사용해 기존 reaction까지 숨겨지는 호출 경로를 기록한다.
|
||||||
|
- [x] **RED:** 잠금 유료 `FeedItem.Community`가 신규 정책 기본값에서는 기존 reaction/comment 표시를 유지하고, opt-in에서는 둘 다 숨기는 회귀 테스트를 추가해 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** 신규 정책 기본값은 기존 표시를 유지하고 `CreatorChannelHomeSectionAdapter`만 잠금 reaction 숨김을 선택하도록 최소 수정한다.
|
||||||
|
- [x] focused 회귀와 Phase 15 Gate를 재실행한다.
|
||||||
|
- [x] `plan-task.md` Verification Log와 review 보고서에 완료 증거를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): `HomeRecommendationMappers -> FeedItem.Community -> HomePopularCommunityAdapter -> FeedCommunityView.bind()` 호출 경로로 공유 정책 회귀를 확정했다. `hideReactionWhenLocked` 미존재 컴파일 RED 후 기본값 false, 크리에이터 채널 홈 true로 제한했고 `FeedItemTest`, 메인 홈 mapper 회귀 테스트와 최종 Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Task 15.R4 메인 홈 회귀 테스트 파일 미사용 import 정리
|
||||||
|
|
||||||
|
**Goal 실행 `P15-R4`:** 확정 review 항목 `REV-P15-004`의 미사용 import를 제거해 Phase 15 style Gate를 회복한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P15-R3` 메인 홈 회귀 테스트 추가 후 `ktlintCheck` 실패 확정.
|
||||||
|
- **대상 파일:** `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
|
||||||
|
- **수정 범위:** 실제 사용처가 없는 `ColorDrawable` import 1줄 제거.
|
||||||
|
- **회귀 검증:** `./gradlew :app:ktlintCheck`, Phase 15 Gate, `git diff --check`.
|
||||||
|
- **완료 증거:** ktlint 정확한 unused import 1건 → import 제거 → ktlint/Gate PASS → 리뷰 보고서 누적.
|
||||||
|
- **범위 밖:** 같은 테스트 파일의 기존 구조·deprecated API 정리.
|
||||||
|
|
||||||
|
- [x] `ktlintCheck`의 `HomeMainFragmentLayoutTest.kt:6:1 Unused import` RED를 기록한다.
|
||||||
|
- [x] `ColorDrawable` import 1줄만 제거한다.
|
||||||
|
- [x] `ktlintCheck`와 Phase 15 Gate를 재실행한다.
|
||||||
|
- [x] `plan-task.md` Verification Log와 review 보고서에 완료 증거를 누적한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): 메인 홈 회귀 테스트 추가 후 lint 대상이 된 파일에서 사용처 없는 `ColorDrawable` import 1건을 확인해 제거했다. 최종 `ktlintCheck`와 전체 focused test Gate가 PASS했다.
|
||||||
|
|
||||||
|
#### Phase 15 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P15-GATE`:** 승인된 홈/탭/상세 사용자 흐름과 회귀 방지를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P15-T1`~`P15-T5` 완료.
|
||||||
|
- **완료 증거:** 아래 명령을 순차 실행해 모두 exit code 0을 확인하고 각 Task 검증 기록 및 `Verification Log`에 실제 결과를 누적한다.
|
||||||
|
- **범위 밖:** `app/src/androidTest`, `connectedDebugAndroidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA, Git commit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedItemTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeModelsTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.community.action.*"
|
||||||
|
./gradlew :app:testDebugUnitTest --tests \
|
||||||
|
"kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
./gradlew :app:compileDebugKotlin
|
||||||
|
./gradlew :app:ktlintCheck
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 홈 Response 신규 필드와 reaction/pin 정책, list/grid 상세 이동 전용화, 상세 더보기 권한·구매/관리/신고, 상세 변경 후 홈+커뮤니티 refresh 관련 focused test PASS. 리소스 병합·Kotlin 컴파일·ktlint·diff whitespace 검증 PASS.
|
||||||
|
|
||||||
|
#### Phase 15 실행 순서
|
||||||
|
|
||||||
|
```text
|
||||||
|
P15-T1 → P15-T2 → P15-T3 → P15-T4 → P15-T5 → P15-R1 → P15-R2 → P15-R3 → P15-R4 → P15-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Phase 15 계획 기록 — 2026-08-10
|
||||||
|
|
||||||
|
- 상태: 계획 완료 / 구현 미시작
|
||||||
|
- 무엇을: 승인된 PRD `F0-20260810`을 `P15-T1`~`P15-T5`, `P15-GATE`로 분해했다.
|
||||||
|
- 왜: 홈·커뮤니티 탭의 표시/액션 일관성과 상세 구매·메뉴·결과 전파를 구현 전에 검증 가능한 단위로 고정하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `git diff --check` — 문서 변경 whitespace 검증 PASS.
|
||||||
|
- `./gradlew tasks --all` — exit code 0, `BUILD SUCCESSFUL`.
|
||||||
|
- 남은 항목: Phase 15 구현 전체. 사용자 지시에 따라 이번 작업에서는 수행하지 않는다.
|
||||||
|
- 다음 행동: 사용자가 구현을 별도로 요청하면 `P15-T1` RED부터 순차 실행한다.
|
||||||
|
|
||||||
|
#### Phase 15 구현·검증 기록 — 2026-08-10
|
||||||
|
|
||||||
|
- 상태: 구현 및 검증 완료 / Git commit 미수행
|
||||||
|
- 무엇을: `P15-T1`~`P15-T5`를 구현하고 리뷰 확정 항목 `P15-R1`~`P15-R4`를 수정했다.
|
||||||
|
- 왜: 미체크 항목이 구현 누락이었음을 코드·테스트 대조로 확인했고, 구현 후 독립 리뷰와 Gate에서 실제 회귀·style 문제를 확인했기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- 홈 Response에 `isPinned`, `isCommentAvailable`을 연결하고 크리에이터 채널 홈만 유료 미구매 reaction 숨김을 opt-in했다.
|
||||||
|
- 홈·커뮤니티 탭 item 액션을 상세 이동으로 제한하고 상세에 구매·권한별 더보기·변경 결과 전파를 모았다.
|
||||||
|
- `REV-P15-001`~`REV-P15-004`를 각각 회귀 Task로 전환한 뒤 최소 수정했다.
|
||||||
|
- 전체 focused local unit/source contract test, resource merge, Kotlin compile, ktlint를 최종 working tree에서 재실행해 `BUILD SUCCESSFUL`을 확인했고 `git diff --check`, `git diff HEAD --check`는 출력 없이 PASS했다.
|
||||||
|
- 제외: 저장소 규칙과 사용자 지시에 따라 `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA, Git commit은 수행하지 않았다.
|
||||||
|
- 남은 항목: 실제 서버 Response 배포 상태와 기기 수동 UI 확인은 이번 자동 검증 범위 밖이다.
|
||||||
|
|
||||||
|
## Phase 16 — 홈·커뮤니티 리스트 권한별 더보기 메뉴
|
||||||
|
|
||||||
|
**Phase 결과:** 상세와 동일한 권한별 게시글 더보기 메뉴를 홈 커뮤니티 item과 커뮤니티 탭 리스트형에서 사용할 수 있고, 성공한 관리 작업은 두 목록에 함께 반영된다.
|
||||||
|
|
||||||
|
**선행조건:** 승인된 PRD `Home and Community List More Menu Requirements`, Phase 15 구현·검증 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P16-T1`~`P16-T3`, `P16-GATE` 완료와 Task별 검증 기록 및 `Verification Log` 누적.
|
||||||
|
|
||||||
|
### 범위
|
||||||
|
|
||||||
|
**포함**
|
||||||
|
|
||||||
|
- 홈 커뮤니티 item과 커뮤니티 탭 리스트형 작성자 영역 우측 끝 `ic_seemore_vertical` 버튼.
|
||||||
|
- 상세와 동일한 작성자/무료/구매 완료/유료 미구매 권한 truth table.
|
||||||
|
- 작성자 수정·삭제·고정/고정 해제, 일반 사용자 신고.
|
||||||
|
- 성공한 수정·삭제·고정/고정 해제 후 홈+커뮤니티 새로고침.
|
||||||
|
- card 본문 상세 이동과 더보기 메뉴 click 분리.
|
||||||
|
|
||||||
|
**제외**
|
||||||
|
|
||||||
|
- 커뮤니티 탭 썸네일형(그리드형) 더보기 버튼.
|
||||||
|
- 목록에서 유료 게시글 구매·오디오 재생.
|
||||||
|
- 상세 메뉴 구조 변경, 레거시 파일 수정, 신규 범용 coordinator/dependency.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA, Git commit.
|
||||||
|
|
||||||
|
### Task 16.1 공용 메뉴 권한 presentation 계약
|
||||||
|
|
||||||
|
**Goal 실행 `P16-T1`:** 홈과 커뮤니티 리스트가 동일한 메뉴 payload와 권한 계산을 사용하고 다른 공유 feed는 기존 표시를 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** PRD 권한 truth table 승인.
|
||||||
|
- **완료 증거:** 메뉴 권한 4개 케이스와 홈/커뮤니티 mapper focused test PASS.
|
||||||
|
- **범위 밖:** 버튼 layout/click, Activity menu/API 실행.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMenuItem.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeMapperTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItemTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class CreatorChannelCommunityMenuItem(
|
||||||
|
val postId: Long,
|
||||||
|
val creatorId: Long,
|
||||||
|
val currentUserId: Long,
|
||||||
|
val price: Int,
|
||||||
|
val existOrdered: Boolean,
|
||||||
|
val isPinned: Boolean
|
||||||
|
) {
|
||||||
|
val isOwner: Boolean
|
||||||
|
val showMore: Boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `isOwner = creatorId == currentUserId`
|
||||||
|
- `showMore = isOwner || price == 0 || existOrdered`
|
||||||
|
- `CreatorChannelCommunityPostUiModel`은 `menuItem: CreatorChannelCommunityMenuItem`을 제공한다.
|
||||||
|
- `CreatorChannelHomeSection.Communities`는 `currentMemberId: Long`을 함께 보존해 adapter가 같은 menu item을 만든다.
|
||||||
|
- `FeedItem.Community`는 `showMore: Boolean = false`를 추가해 다른 공유 feed 호출부의 기존 동작을 유지한다.
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelCommunityMapperTest`에 작성자 유료 미구매=true, 일반 무료=true, 일반 유료 구매=true, 일반 유료 미구매=false인 `menuItem.showMore`와 `isPinned` 보존 케이스를 추가한다.
|
||||||
|
- [x] **RED:** `CreatorChannelHomeMapperTest`에 `Communities.currentMemberId` 보존, `FeedItemTest`에 `showMore` 기본값 false 회귀를 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령을 실행해 신규 type/property 미존재 컴파일 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityMapperTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedItemTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** 위 `CreatorChannelCommunityMenuItem` 계산 property를 추가하고 커뮤니티 mapper/UI model과 홈 `Communities.currentMemberId`를 연결한다.
|
||||||
|
- [x] **GREEN:** `FeedItem.Community.showMore`를 기본값 false로 추가한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test가 PASS하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 별도 권한 service/factory를 만들지 않고 menu item 계산 property 하나만 공용 정책으로 유지한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): `menuItem`, `currentMemberId`, `showMore` 미존재로 `compileDebugUnitTestKotlin` RED 실패를 확인했다. `CreatorChannelCommunityMenuItem` 계산 property와 커뮤니티/홈 mapper 연결, `FeedItem.Community.showMore=false` 기본값을 최소 구현한 뒤 지정된 focused test 26개가 모두 PASS했다. 명세·코드 품질 독립 검토에서도 차단 이슈가 없음을 확인했다.
|
||||||
|
|
||||||
|
### Task 16.2 홈·커뮤니티 리스트 더보기 버튼과 callback
|
||||||
|
|
||||||
|
**Goal 실행 `P16-T2`:** 홈 커뮤니티 item과 커뮤니티 리스트형의 더보기 버튼이 card 상세 이동과 분리된 공용 menu callback을 호출한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P16-T1` 완료.
|
||||||
|
- **완료 증거:** 홈/리스트 callback routing focused test, resource merge, Kotlin compile PASS와 grid 무변경 확인.
|
||||||
|
- **범위 밖:** BottomSheet 표시와 mutation API.
|
||||||
|
- **TDD 예외 사유:** 버튼 위치·constraint·visibility 속성 자체는 저장소 규칙상 자동 테스트에서 직접 검증하지 않는다.
|
||||||
|
- **대체 검증 방법:** 권한 visibility는 `P16-T1` 순수 로직 test, callback은 click/source contract test, UI resource는 id 존재·resource merge·compile·정적 XML tree 대조로 검증한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/layout/view_feed_community.xml`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/item_creator_channel_community_list.xml`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
|
||||||
|
- Verify only: `app/src/main/res/layout/item_creator_channel_community_grid.xml`
|
||||||
|
- Verify only: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun FeedCommunityView.setOnMoreClick(listener: ((FeedItem.Community) -> Unit)?)
|
||||||
|
|
||||||
|
CreatorChannelHomeSectionAdapter(
|
||||||
|
onCommunityClick: (Long) -> Unit,
|
||||||
|
onCommunityMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
CreatorChannelCommunityListAdapter(
|
||||||
|
onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||||
|
onMoreClick: (CreatorChannelCommunityMenuItem) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **RED:** `FeedViewTest`에 `btn_feed_community_more` resource와 `setOnMoreClick`이 bound item만 전달하는 callback 케이스를 추가한다.
|
||||||
|
- [x] **RED:** `CreatorChannelCommunityFragmentLayoutTest`와 `CreatorChannelActivitySourceTest`에 홈/리스트 adapter → Fragment Host의 동일 menu callback, list root 상세 click 유지, grid menu callback 부재 계약을 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령으로 신규 resource/API/callback 미존재 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** `view_feed_community.xml` 작성자 row를 전체 폭으로 유지하고 우측 끝에 42dp `btn_feed_community_more`(`ic_seemore_vertical`, `read_more`)를 추가한다. `FeedCommunityView.bind()`는 `item.showMore`를 bind하고 별도 more click을 적용한다.
|
||||||
|
- [x] **GREEN:** 홈 adapter는 `Communities.currentMemberId`로 menu item을 만들고 `FeedItem.showMore`, `setOnMoreClick`, `onCommunityMoreClick`을 연결한다. HomeFragment Host로 같은 menu item을 전달한다.
|
||||||
|
- [x] **GREEN:** 커뮤니티 리스트 layout의 기존 top actions 끝에 42dp more 버튼을 추가하고 wrapper는 `showOwnerTopPrice || menuItem.showMore`일 때 표시한다. list adapter/Fragment Host에 `onMoreClick`을 연결한다.
|
||||||
|
- [x] **GREEN:** root `onCommunityClick`/`onPostClick`은 상세 이동을 유지하고 more 버튼은 메뉴 callback만 호출한다.
|
||||||
|
- [x] **GREEN:** grid layout/adapter에는 more id/callback을 추가하지 않는다.
|
||||||
|
- [x] **GREEN 확인:** focused test와 `./gradlew :app:mergeDebugResources :app:compileDebugKotlin`이 PASS하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 이번 Task가 만든 listener/import/binding 중복만 정리하고 공유 View의 다른 호출부 동작은 변경하지 않는다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): `btn_feed_community_more`, `btn_creator_channel_community_list_more`, `setOnMoreClick` 미존재로 `compileDebugUnitTestKotlin` RED 실패를 확인했다. 두 42dp 버튼과 홈/리스트의 동일 `CreatorChannelCommunityMenuItem` Host callback을 최소 연결한 뒤 지정된 focused test 119개가 모두 PASS했다. `:app:mergeDebugResources :app:compileDebugKotlin`도 PASS했으며 grid XML/adapter에는 more id·callback이 없음을 정적 확인했다.
|
||||||
|
|
||||||
|
### Task 16.3 Activity 공용 메뉴와 mutation 결과 전파
|
||||||
|
|
||||||
|
**Goal 실행 `P16-T3`:** 홈과 커뮤니티 리스트의 공용 callback이 상세와 같은 BottomSheet/Dialog/API를 실행하고 성공한 관리 작업만 두 목록을 갱신한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P16-T2` 완료, Phase 15 상세 메뉴/API 계약 유지.
|
||||||
|
- **완료 증거:** Activity source/routing focused test와 전체 community 회귀, compile PASS.
|
||||||
|
- **범위 밖:** 상세 ViewModel/Activity 메뉴 변경, 신고 후 목록 새로고침.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- Reuse: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
|
||||||
|
- Reuse: 레거시 `CreatorCommunityPostMenuBottomSheetDialog`, `CreatorCommunityReportDialog`, `CreatorCommunityModifyActivity`, `CreatorCommunityRepository`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChangeTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreatorChannelCommunityMoreClicked(item: CreatorChannelCommunityMenuItem)
|
||||||
|
|
||||||
|
private fun updateCreatorChannelCommunityPostFixed(item: CreatorChannelCommunityMenuItem)
|
||||||
|
private fun deleteCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem)
|
||||||
|
private fun reportCreatorChannelCommunityPost(item: CreatorChannelCommunityMenuItem, reason: String)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Activity는 `CreatorCommunityRepository`와 기존 `CreatorChannelRepository.reportCommunityPost()`를 주입해 재사용한다.
|
||||||
|
- 수정 launcher의 `RESULT_OK`, 삭제·고정 성공은 기존 `handleCommunityChange(CommunityChange.Updated)`로 홈+커뮤니티를 갱신한다.
|
||||||
|
- 신고 성공은 API message 또는 `character_comment_report_submitted` Toast만 표시하고 새로고침하지 않는다.
|
||||||
|
- `isCommunityPostMutating` 단일 guard로 고정·삭제·신고 중복 요청을 막고 성공/실패 모두 guard를 해제한다.
|
||||||
|
|
||||||
|
- [x] **RED:** `CreatorChannelActivitySourceTest`에 HomeFragment.Host와 CommunityFragment.Host의 동일 override 하나, 권한 guard, 기존 BottomSheet/Dialog/modify launcher 연결 계약을 추가한다.
|
||||||
|
- [x] **RED:** 같은 test에 고정 `!item.isPinned`, 삭제 `ModifyCommunityPostRequest(isActive=false)`, 신고 `reportCommunityPost`, 성공 `CommunityChange.Updated`, 신고 성공 no-refresh, 중복 guard 계약을 추가한다.
|
||||||
|
- [x] **RED 확인:** 아래 명령으로 Activity 공용 메뉴/mutation 연결 미존재 실패를 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.community.action.CommunityChangeTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **GREEN:** Activity에 modify launcher와 `CreatorCommunityPostMenuBottomSheetDialog`를 연결하고 `item.showMore`가 false면 menu를 시작하지 않는다.
|
||||||
|
- [x] **GREEN:** 작성자 menu는 수정·삭제·고정/고정 해제, 일반 사용자 menu는 `CreatorCommunityReportDialog` 신고만 연결한다. mutation method는 메뉴 역할과 `item.isOwner`를 다시 확인한다.
|
||||||
|
- [x] **GREEN:** 고정은 `updateCommunityPostFixed(item.postId, !item.isPinned, authToken())`, 삭제는 `ModifyCommunityPostRequest(creatorCommunityId = item.postId, isActive = false)` JSON `text/plain` 요청, 신고는 `CreatorChannelRepository.reportCommunityPost(item.postId, reason, authToken())`를 호출한다.
|
||||||
|
- [x] **GREEN:** 수정 복귀·고정·삭제 성공만 `CommunityChange.Updated`로 홈+커뮤니티를 갱신하고 실패/신고는 목록 상태를 변경하지 않는다.
|
||||||
|
- [x] **GREEN 확인:** focused test, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`, `./gradlew :app:compileDebugKotlin`이 PASS하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 상세 메뉴 코드를 옮기거나 신규 coordinator를 만들지 않고 Activity의 공용 목록 처리만 유지한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): Activity 공용 메뉴와 mutation source 계약 3개를 먼저 추가한 뒤 focused test에서 기존 100개는 통과하고 신규 3개가 `item.showMore` guard 및 mutation method/guard 미존재로 RED 실패함을 확인했다. 기존 BottomSheet/Dialog/modify launcher와 legacy/V2 repository를 Activity에 직접 연결한 뒤 focused test 103개, community wildcard 회귀 86개가 모두 PASS했다. `:app:compileDebugKotlin`도 PASS했으며 수정·고정·삭제 성공만 `CommunityChange.Updated`를 호출하고 신고 성공/모든 실패 경로에는 refresh가 없음을 source test로 확인했다.
|
||||||
|
- 리뷰 회귀 수정 검증 기록(2026-08-10): 게시물 작성자 권한은 채널 소유자 상태와 독립된 `item.isOwner`라는 PRD 계약에 맞춰 source test를 먼저 변경했고, 현재 `isCreatorChannelOwner()` 추가 조건 때문에 focused test 103개 중 권한 계약 3개가 RED 실패함을 확인했다. 수정·고정·삭제·신고 경로에서 채널 소유자 조건만 제거한 뒤 focused test 103개와 community wildcard 회귀 86개가 모두 PASS했고 `:app:compileDebugKotlin`도 PASS했다.
|
||||||
|
|
||||||
|
### Task 16.R1 Phase 완료 문서 정합성 복구
|
||||||
|
|
||||||
|
**Goal 실행 `P16-R1`:** 확정 review 항목 `REV-P16-001`에 따라 Phase 16 구현·Gate 완료 상태와 검증 증거를 문서에 누적한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P16-T1`~`P16-T3`, `P16-GATE` 실행 완료와 `REV-P16-001` 확정.
|
||||||
|
- **대상 파일:** `docs/20260708_커뮤니티_게시물_상세_댓글_답글/plan-task.md`, `reviews/phase16-home-community-list-more-menu-review.md`.
|
||||||
|
- **수정 범위:** 기존 계획 기록은 보존하고 구현·Gate 완료 기록, `Verification Log`, 리뷰 판정만 누적한다.
|
||||||
|
- **회귀 검증:** `git diff --check`, 최종 리뷰 재검토.
|
||||||
|
- **완료 증거:** 미시작 상태와 Gate 기록 누락 근거 → 완료 기록 누적 → diff check PASS → 5개 리뷰 lane 최종 판정.
|
||||||
|
- **범위 밖:** production/test 코드 변경, Gate 재실행을 위한 테스트 완화, Git commit.
|
||||||
|
|
||||||
|
- [x] Phase 16 구현·검증 완료 기록을 기존 계획 기록 뒤에 누적한다.
|
||||||
|
- [x] `Verification Log`에 focused test, resource merge, compile, ktlint, diff check 결과와 제외 범위를 누적한다.
|
||||||
|
- [x] Phase 16 리뷰 보고서에 `REV-P16-001` 수정 및 재검토 결과를 누적한다.
|
||||||
|
- [x] `git diff --check`와 최종 리뷰 재검토를 통과한다.
|
||||||
|
|
||||||
|
- 검증 기록(2026-08-10): Phase 16 완료 기록과 `Verification Log`를 누적한 뒤 `git diff --check`, `git diff HEAD --check`가 출력 없이 PASS했다. 최종 재검토에서 목표·실행 QA·코드 품질·보안·맥락 5개 lane이 모두 PASS했으며 추가 차단 이슈가 없음을 확인했다.
|
||||||
|
|
||||||
|
### Phase 16 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P16-GATE`:** 권한별 버튼·메뉴·mutation·새로고침과 grid 제외, 기존 상세/구매 회귀를 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P16-T1`~`P16-T3` 완료.
|
||||||
|
- **완료 증거:** 아래 명령의 exit code 0, 정적 권한/클릭 대조, Task별 검증 기록과 `Verification Log` 누적.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 test 완화, 기기 UI 조작, Git commit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedItemTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
|
||||||
|
./gradlew :app:testDebugUnitTest \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*" \
|
||||||
|
--tests "kr.co.vividnext.sodalive.v2.community.action.*"
|
||||||
|
./gradlew :app:mergeDebugResources
|
||||||
|
./gradlew :app:compileDebugKotlin
|
||||||
|
./gradlew :app:ktlintCheck
|
||||||
|
git diff --check
|
||||||
|
git diff HEAD --check
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 홈·커뮤니티 리스트형은 동일 권한 메뉴를 제공하고 card root 상세 이동을 유지한다. grid에는 메뉴가 없고, 미구매 유료 일반 사용자는 버튼이 숨겨진다. 수정·삭제·고정 성공은 홈+커뮤니티를 갱신하며 신고/실패는 갱신하지 않는다. 기존 상세 구매·메뉴 focused 회귀와 resource/compile/style/whitespace Gate가 모두 PASS한다.
|
||||||
|
|
||||||
|
### Phase 16 실행 순서
|
||||||
|
|
||||||
|
```text
|
||||||
|
P16-T1 → P16-T2 → P16-T3 → P16-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 16 계획 기록 — 2026-08-10
|
||||||
|
|
||||||
|
- 상태: 계획 완료 / 구현 미시작
|
||||||
|
- 무엇을: 승인된 홈·커뮤니티 리스트형 권한별 더보기 후속 요구를 `P16-T1`~`P16-T3`, `P16-GATE`로 분해했다.
|
||||||
|
- 왜: 공용 권한 계산 → UI callback → Activity mutation 순으로 계약을 분리해 grid 제외와 상세 이동 회귀를 검증하기 위해서다.
|
||||||
|
- 남은 항목: 사용자 계획 검토 승인 후 `P16-T1` RED부터 순차 실행한다.
|
||||||
|
- Git commit: 사용자 지시에 따라 수행하지 않는다.
|
||||||
|
|
||||||
|
### Phase 16 구현·검증 기록 — 2026-08-10
|
||||||
|
|
||||||
|
- 상태: 구현 및 Gate 완료 / Git commit 미수행
|
||||||
|
- 무엇을: `P16-T1`~`P16-T3`를 구현하고 리뷰 확정 항목 `REV-P16-001`을 `P16-R1`로 수정했다.
|
||||||
|
- 왜: 홈과 커뮤니티 리스트형에서 상세과 동일한 권한별 메뉴를 제공하고, 성공한 관리 작업만 양쪽 목록에 반영하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 공용 `CreatorChannelCommunityMenuItem`으로 작성자/가격/구매 권한과 고정 상태를 전달했다.
|
||||||
|
- 홈·리스트의 더보기 click을 card 상세 click과 분리하고 grid는 기존 상세 이동만 유지했다.
|
||||||
|
- 기존 BottomSheet/Dialog/repository를 Activity에서 재사용하고 수정·삭제·고정 성공만 `CommunityChange.Updated`로 전파했다.
|
||||||
|
- 최초 독립 명세 검토에서 채널 소유자 조건이 작성자 권한을 제한하는 문제를 확인해 `item.isOwner` 계약으로 회귀 수정했다.
|
||||||
|
- 최종 Gate의 focused test, resource merge, Kotlin compile, ktlint, 두 diff check가 모두 통과했다.
|
||||||
|
- 제외: `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA, 실제 서버 mutation, 별도 worktree, Git commit.
|
||||||
|
- 남은 항목: 없음.
|
||||||
|
|
||||||
## Verification Log
|
## Verification Log
|
||||||
|
- 2026-08-11: 사용자 요청에 따라 Phase 15·16 완료 체크를 PRD, production, test와 다시 역대조했다. 실제 Task 체크박스는 모두 완료 상태였고 체크 누락·미구현 항목은 없었다. Phase 15 홈 Response/공유 feed 회귀 test, Phase 16 홈·Feed·Activity focused test, community/action focused test와 필터 없는 전체 `:app:testDebugUnitTest`가 모두 `BUILD SUCCESSFUL`로 PASS했다. `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check`, `git diff HEAD --check`도 PASS했다. 구매 호출은 상세에만 있고, 홈·리스트 root/more 분리와 grid 제외, 권한별 메뉴, 성공 관리 mutation의 홈+커뮤니티 갱신, 신고·실패 no-refresh, 공유 `FeedItem.Community` 기본값 보존을 정적으로 확인했다. 독립 재리뷰도 Critical/Important/Minor 이슈 없음과 승인 가능으로 판정했다. 새 확정 결함이 없어 억지로 회귀 수정 Task나 production/test 변경을 추가하지 않았고 Git commit도 수행하지 않았다.
|
||||||
|
- 2026-08-10: Phase 16 `P16-T1`~`P16-T3`와 `P16-GATE`를 완료했다. 최종 working tree에서 홈·Feed·Activity focused test와 community/action 회귀 test가 `BUILD SUCCESSFUL`로 PASS했고, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`도 모두 PASS했다. `git diff --check`, `git diff HEAD --check`는 출력 없이 통과했다. 허용된 실행 QA 재검증에서도 P0 시나리오 11/11과 focused 128개, community/action 96개가 PASS했다. 리뷰에서 확인된 작성자 권한 추가 제한은 `item.isOwner` 기준으로 회귀 수정했고, 완료 문서 누락 `REV-P16-001`은 `P16-R1`로 누적했다. 저장소 규칙과 사용자 지시에 따라 `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA, 실제 서버 mutation, 별도 worktree, Git commit은 수행하지 않았다. `.editorconfig disabled_rules` 및 Gradle deprecated feature 경고는 기존 비차단 경고로 유지했다.
|
||||||
|
- 2026-08-10: Phase 15 문서 대조·구현·독립 재리뷰를 완료했다. 미구현 체크리스트는 `P15-T1`~`P15-T5`로 구현했고, 리뷰에서 `REV-P15-001` 미사용 import, `REV-P15-002` reaction id 오배치, `REV-P15-003` 공유 피드 reaction 정책 범위 회귀, `REV-P15-004` 테스트 파일 미사용 import를 확정해 `P15-R1`~`P15-R4`로 수정했다. 최종 working tree에서 홈/공유 피드·커뮤니티 상세/action·상위 Activity focused test, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 PASS했고 `git diff --check`, `git diff HEAD --check`는 출력 없이 PASS했다. 수정 후 독립 재리뷰에서도 새 Critical/Important/Minor 이슈가 없음을 확인했다. `.editorconfig disabled_rules` 및 기존 test deprecated API 경고는 실패가 아닌 기존 경고이며 범위 밖으로 유지했다. Git commit은 수행하지 않았다.
|
||||||
|
- 2026-08-10: 승인된 후속 PRD와 Phase 15 구현 계획만 작성했다. placeholder·요구사항 충돌을 자체 검토했고 `git diff --check`, `./gradlew tasks --all`이 통과했다. 사용자 지시에 따라 production/test 코드 구현과 Git commit은 수행하지 않았다.
|
||||||
- 2026-07-12: Phase 14 유료 미구매 상세 게시글 reaction/comment 숨김 변경을 완료했다. RED는 `CreatorChannelCommunityDetailViewModelTest.유료 미구매 게시물은 작성자가 아니면 reaction 현황과 댓글을 숨긴다`의 `showReaction` 미구현 컴파일 실패로 확인했고, 구현 후 focused 테스트, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 PASS했다. 실제 화면 직접 조작은 Activity가 내부 진입 경로와 데이터 계정에 의존해 이번 검증에서는 단위/리소스/컴파일/스타일 검증으로 대체했다.
|
- 2026-07-12: Phase 14 유료 미구매 상세 게시글 reaction/comment 숨김 변경을 완료했다. RED는 `CreatorChannelCommunityDetailViewModelTest.유료 미구매 게시물은 작성자가 아니면 reaction 현황과 댓글을 숨긴다`의 `showReaction` 미구현 컴파일 실패로 확인했고, 구현 후 focused 테스트, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`, `./gradlew :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 PASS했다. 실제 화면 직접 조작은 Activity가 내부 진입 경로와 데이터 계정에 의존해 이번 검증에서는 단위/리소스/컴파일/스타일 검증으로 대체했다.
|
||||||
|
|
||||||
- 구현 전 문서 작성 단계에서는 Gradle 검증을 실행하지 않았다.
|
- 구현 전 문서 작성 단계에서는 Gradle 검증을 실행하지 않았다.
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
- 사용자는 게시글에 좋아요를 누르고, 댓글과 답글을 작성/수정/삭제하고 싶어 한다.
|
- 사용자는 게시글에 좋아요를 누르고, 댓글과 답글을 작성/수정/삭제하고 싶어 한다.
|
||||||
- 댓글 작성이 불가능한 게시글에서는 댓글 영역과 입력 영역이 노출되면 안 된다.
|
- 댓글 작성이 불가능한 게시글에서는 댓글 영역과 입력 영역이 노출되면 안 된다.
|
||||||
- 조회용 신규 v2 API 계약과 Figma 기반 UI 요구사항이 별도 문서로 정의되어야 한다.
|
- 조회용 신규 v2 API 계약과 Figma 기반 UI 요구사항이 별도 문서로 정의되어야 한다.
|
||||||
|
- 2026-08-10 후속 확인에서 홈 커뮤니티 섹션의 유료 미구매 게시글에 하트·댓글 UI가 노출되고, 홈 Response에 고정/댓글 가능 여부가 없어 커뮤니티 탭·상세와 표시 상태가 달라지는 문제가 확인됐다.
|
||||||
|
- 커뮤니티 탭의 유료 미구매 가격 UI와 구매 진입점이 홈·상세와 일치하지 않고, 상세 구매 결과가 커뮤니티 탭에만 반영되어 홈 섹션은 구매 전 상태로 남는다.
|
||||||
|
- 게시글 관리/신고 메뉴가 상세에 없고 목록 item에 별도 액션이 남아 있어, 목록은 상세 이동만 담당하고 구매·관리 액션은 상세에 모으는 정리가 필요하다.
|
||||||
|
- 2026-08-10 구현 확인 후 상세에만 둔 권한별 더보기 메뉴를 홈 커뮤니티 item과 커뮤니티 탭 리스트형에서도 바로 사용할 수 있어야 한다는 후속 요구가 확정됐다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,6 +38,11 @@
|
|||||||
- 하트 터치 시 좋아요 API를 호출하고, `isLiked`에 따라 `ic_feed_community_heart_fill`/`ic_feed_community_heart` icon을 표시한다.
|
- 하트 터치 시 좋아요 API를 호출하고, `isLiked`에 따라 `ic_feed_community_heart_fill`/`ic_feed_community_heart` icon을 표시한다.
|
||||||
- 댓글 작성이 불가능한 게시글(`isCommentAvailable == false`)이면 댓글 목록과 하단 입력 영역을 숨긴다.
|
- 댓글 작성이 불가능한 게시글(`isCommentAvailable == false`)이면 댓글 목록과 하단 입력 영역을 숨긴다.
|
||||||
- 하단 댓글/답글 입력 영역은 화면 하단에 고정하고 키보드 표시 여부에 따라 함께 올라오게 한다.
|
- 하단 댓글/답글 입력 영역은 화면 하단에 고정하고 키보드 표시 여부에 따라 함께 올라오게 한다.
|
||||||
|
- 크리에이터 채널 홈 Response의 커뮤니티 게시글에 `isPinned`, `isCommentAvailable`을 추가하고 홈 섹션의 고정·댓글 표시 상태에 반영한다.
|
||||||
|
- 홈과 커뮤니티 탭의 게시글 card 본문 터치는 상세 이동만 수행하고, 별도 더보기 버튼은 권한별 관리/신고 메뉴를 제공한다. 유료 게시글 구매는 상세에서만 시작한다.
|
||||||
|
- 커뮤니티 탭 리스트형·썸네일형의 유료 미구매 가격 UI를 홈·상세와 같은 잠금/가격 표현으로 통일한다.
|
||||||
|
- 상세·홈·커뮤니티 리스트형 게시글 작성자 영역 우측 끝에 `ic_seemore_vertical` 버튼을 표시하고 작성자/구매 권한에 따라 관리 또는 신고 메뉴를 제공한다.
|
||||||
|
- 상세 구매·수정·삭제·고정 결과가 크리에이터 채널 홈과 커뮤니티 탭에 모두 반영되도록 결과 전파 범위를 확장한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,6 +54,8 @@
|
|||||||
- 레거시 커뮤니티/댓글 관련 파일을 직접 수정하지 않는다.
|
- 레거시 커뮤니티/댓글 관련 파일을 직접 수정하지 않는다.
|
||||||
- 크리에이터 채널의 다른 탭 UI나 공통 스크롤 구조를 재설계하지 않는다.
|
- 크리에이터 채널의 다른 탭 UI나 공통 스크롤 구조를 재설계하지 않는다.
|
||||||
- v2 패키지 내부 popup dialog는 `kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog`를 사용한다.
|
- v2 패키지 내부 popup dialog는 `kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog`를 사용한다.
|
||||||
|
- 레거시 커뮤니티 게시글 목록의 구매 동작을 변경하지 않는다.
|
||||||
|
- 커뮤니티 게시글 화면 공통화를 위한 신규 framework, coordinator 또는 범용 abstraction을 추가하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,11 +75,96 @@
|
|||||||
- 사용자는 내가 쓴 댓글/답글을 수정하거나 삭제하고 싶다.
|
- 사용자는 내가 쓴 댓글/답글을 수정하거나 삭제하고 싶다.
|
||||||
- 채널 크리에이터는 자신의 채널 댓글/답글을 삭제해 관리하고 싶다.
|
- 채널 크리에이터는 자신의 채널 댓글/답글을 삭제해 관리하고 싶다.
|
||||||
- 사용자는 댓글이 불가능한 게시글에서는 입력창 없이 게시글만 보고 싶다.
|
- 사용자는 댓글이 불가능한 게시글에서는 입력창 없이 게시글만 보고 싶다.
|
||||||
|
- 사용자는 홈과 커뮤니티 탭에서 게시글을 터치하면 다른 중간 동작 없이 상세로 이동하고 싶다.
|
||||||
|
- 사용자는 미구매 유료 게시글의 하트·댓글 정보를 홈에서 보지 않고 상세에서 구매하고 싶다.
|
||||||
|
- 사용자는 고정 게시글을 홈 커뮤니티 섹션에서도 구분하고 싶다.
|
||||||
|
- 작성자는 상세에서 자신의 게시글을 수정·삭제·고정/고정 해제하고 싶다.
|
||||||
|
- 일반 사용자는 무료 또는 구매 완료 게시글 상세에서 게시글을 신고하고 싶다.
|
||||||
|
- 작성자는 홈 또는 커뮤니티 리스트에서 자신의 게시글을 수정·삭제·고정/고정 해제하고 싶다.
|
||||||
|
- 일반 사용자는 홈 또는 커뮤니티 리스트의 무료·구매 완료 게시글을 신고하고 싶다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Core Features
|
## 7. Core Features
|
||||||
|
|
||||||
|
### F0-20260810. 구매 진입점·표시 상태·권한별 메뉴 후속 정리
|
||||||
|
홈·커뮤니티 탭은 게시글 상태를 일관되게 표시하고 card 본문에서 상세로 이동한다. 구매는 상세에서만 수행하며, 권한별 게시글 관리/신고 메뉴는 상세·홈·커뮤니티 리스트형에서 제공한다.
|
||||||
|
|
||||||
|
#### Home Response Contract
|
||||||
|
크리에이터 채널 홈의 `communities`와 `notices`가 사용하는 게시글 Response에 상세와 같은 필드를 추가한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class CreatorChannelCommunityPostResponse(
|
||||||
|
// 기존 필드 유지
|
||||||
|
@SerializedName("isPinned") val isPinned: Boolean,
|
||||||
|
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Home Community Requirements
|
||||||
|
- `isPinned == true`이면 커뮤니티 탭과 같은 고정/공지 표시를 노출한다.
|
||||||
|
- `price > 0 && existOrdered == false`인 유료 미구매 게시글은 하트 icon·좋아요 수·댓글 icon·댓글 수 reaction UI 전체를 숨긴다.
|
||||||
|
- 무료 또는 구매 완료 게시글은 하트 icon과 좋아요 수를 표시한다.
|
||||||
|
- 무료 또는 구매 완료 게시글에서 `isCommentAvailable == true`이면 댓글 icon과 댓글 수를 표시하고, `false`이면 댓글 icon과 댓글 수만 숨긴다.
|
||||||
|
- 게시글 card 본문 터치 액션은 상세 이동만 제공한다.
|
||||||
|
- 홈 커뮤니티 item에서 구매 dialog와 오디오 재생은 시작하지 않는다.
|
||||||
|
- 권한 조건을 만족하는 홈 커뮤니티 item은 작성자 영역 우측 끝의 더보기 버튼으로 관리/신고 메뉴를 시작한다.
|
||||||
|
|
||||||
|
#### Community Tab Requirements
|
||||||
|
- 리스트형과 썸네일형 card 본문 터치 액션은 상세 이동만 제공한다.
|
||||||
|
- 리스트형 item의 오디오 재생은 제거 상태를 유지하고, 권한 조건을 만족하는 item의 작성자 영역 우측 끝에는 더보기 버튼을 제공한다.
|
||||||
|
- 썸네일형(그리드형)은 작성자 영역이 없으므로 더보기 버튼을 제공하지 않고 상세 이동만 유지한다.
|
||||||
|
- 유료 미구매 게시글 item에서는 구매 dialog를 열지 않는다.
|
||||||
|
- 리스트형과 썸네일형의 유료 미구매 가격 UI를 홈/상세와 같은 `lock icon + cash icon + 금액 capsule` 구조로 통일한다.
|
||||||
|
- 리스트형·썸네일형의 기존 reaction 숨김, 댓글 가능 여부, 고정 표시 정책은 유지한다.
|
||||||
|
|
||||||
|
#### Detail Purchase and Badge Requirements
|
||||||
|
- 유료 미구매 게시글의 paywall을 터치한 경우에만 기존 `PurchaseCommunityPostDialog`를 표시한다.
|
||||||
|
- 상세 작성자 영역 우측 상단에 표시하던 유료 금액 badge를 제거한다.
|
||||||
|
- 유료 구매 완료 시 표시하던 구매 완료 badge를 제거한다.
|
||||||
|
- paywall 내부의 잠금/가격 표시는 구매 전 구매 진입점이므로 유지한다.
|
||||||
|
- 구매 성공 시 상세를 다시 조회하고 상위 화면에 변경 결과를 반환한다.
|
||||||
|
- 구매 실패 시 잠금 상태를 유지하고 기존 오류 Toast 정책을 따른다.
|
||||||
|
|
||||||
|
#### Detail More Menu Requirements
|
||||||
|
- `ic_seemore_vertical` 버튼은 title bar가 아니라 게시글 작성자 영역의 우측 끝에 표시한다.
|
||||||
|
- 작성자는 무료/유료 및 구매 여부와 관계없이 더보기 버튼을 표시한다.
|
||||||
|
- 일반 사용자는 무료 게시글 또는 구매 완료 유료 게시글에서 더보기 버튼을 표시한다.
|
||||||
|
- 일반 사용자의 유료 미구매 게시글에서는 더보기 버튼을 숨긴다.
|
||||||
|
- 작성자 메뉴는 기존 `CreatorCommunityPostMenuBottomSheetDialog`를 재사용해 수정, 삭제, 고정/고정 해제를 제공한다.
|
||||||
|
- 일반 사용자 메뉴는 같은 BottomSheet의 신고 메뉴를 제공하고 기존 `CreatorCommunityReportDialog`를 재사용한다.
|
||||||
|
- 수정은 기존 `CreatorCommunityModifyActivity`, 삭제/고정은 기존 `CreatorCommunityRepository` API를 호출한다.
|
||||||
|
- 신고는 기존 `ReportType.COMMUNITY_POST` 흐름을 재사용한다.
|
||||||
|
|
||||||
|
#### Home and Community List More Menu Requirements
|
||||||
|
- 홈 커뮤니티 item과 커뮤니티 탭 리스트형 item의 작성자 영역 우측 끝에 `ic_seemore_vertical` 버튼을 표시한다.
|
||||||
|
- 작성자 판정은 게시글 `creatorId == SharedPreferenceManager.userId`로 통일한다.
|
||||||
|
- 작성자는 무료/유료 및 구매 여부와 관계없이 더보기 버튼을 표시한다.
|
||||||
|
- 일반 사용자는 무료 게시글 또는 구매 완료 유료 게시글에서 더보기 버튼을 표시한다.
|
||||||
|
- 일반 사용자의 유료 미구매 게시글에서는 더보기 버튼을 숨긴다.
|
||||||
|
- 작성자 메뉴는 기존 `CreatorCommunityPostMenuBottomSheetDialog`를 재사용해 수정, 삭제, 고정/고정 해제를 제공한다.
|
||||||
|
- 일반 사용자 메뉴는 같은 BottomSheet의 신고 메뉴를 제공하고 기존 `CreatorCommunityReportDialog`를 재사용한다.
|
||||||
|
- 수정은 기존 `CreatorCommunityModifyActivity`, 삭제/고정은 기존 `CreatorCommunityRepository`, 신고는 기존 `ReportType.COMMUNITY_POST` 흐름을 재사용한다.
|
||||||
|
- 더보기 버튼 터치는 card의 상세 이동 click으로 전파하지 않고 메뉴만 표시한다.
|
||||||
|
- 커뮤니티 탭 썸네일형(그리드형)은 이번 후속 범위에서 제외한다.
|
||||||
|
|
||||||
|
#### Change Propagation Requirements
|
||||||
|
- 구매·수정·삭제·고정/고정 해제 성공 시 상세는 `RESULT_OK`를 반환한다.
|
||||||
|
- 수정 복귀와 고정/고정 해제 성공 시 상세를 다시 조회한다.
|
||||||
|
- 삭제 성공 시 `RESULT_OK`를 설정하고 상세를 종료한다.
|
||||||
|
- `CreatorChannelActivity`가 상세의 `RESULT_OK`를 받으면 홈 탭과 커뮤니티 탭을 모두 새로고침한다.
|
||||||
|
- 홈·커뮤니티 리스트 메뉴의 수정·삭제·고정/고정 해제 성공도 홈 탭과 커뮤니티 탭을 모두 새로고침한다.
|
||||||
|
- 신고 결과는 게시글 구매/내용/고정 상태를 바꾸지 않으므로 홈·커뮤니티 탭 새로고침을 발생시키지 않는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 작성자 응답의 `existOrdered == false`는 작성자의 관리 권한을 제한하지 않는다.
|
||||||
|
- 서버가 유료 미구매 홈 item에 reaction count를 내려줘도 UI에는 표시하지 않는다.
|
||||||
|
- `isCommentAvailable == false`인 무료/구매 완료 홈 item은 좋아요만 표시하고 댓글 UI는 숨긴다.
|
||||||
|
- 상세 메뉴 mutation이 실패하면 상세 상태를 임의 변경하거나 성공 결과를 반환하지 않는다.
|
||||||
|
- 홈·커뮤니티 리스트 메뉴 mutation이 실패하면 현재 목록 상태를 임의 변경하거나 성공 새로고침을 실행하지 않는다.
|
||||||
|
- 더보기 버튼이 숨겨진 유료 미구매 일반 사용자 item은 card 본문 상세 이동만 가능하다.
|
||||||
|
- 다른 화면에서 사용하는 `FeedItem.Community`는 신규 필드 기본값으로 기존 표시 동작을 유지한다.
|
||||||
|
|
||||||
### F0. 2026-07-12 후속 변경사항
|
### F0. 2026-07-12 후속 변경사항
|
||||||
크리에이터 커뮤니티 상세의 오디오/구매 동작을 기존 레거시 목록 UI와 동일한 사용성으로 보정한다.
|
크리에이터 커뮤니티 상세의 오디오/구매 동작을 기존 레거시 목록 UI와 동일한 사용성으로 보정한다.
|
||||||
|
|
||||||
@@ -107,7 +203,7 @@ Figma `290:9243`, `290:9266` 기준으로 게시글 본문과 좋아요를 표
|
|||||||
- 본문(`content`), 이미지(`imageUrl`), 오디오(`audioUrl`)를 표시한다.
|
- 본문(`content`), 이미지(`imageUrl`), 오디오(`audioUrl`)를 표시한다.
|
||||||
- 오디오 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
|
- 오디오 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
|
||||||
- 유료 게시글(`price > 0`)이고 미구매(`existOrdered == false`)이면 Figma paywall(잠금 + 가격) 표시를 따른다.
|
- 유료 게시글(`price > 0`)이고 미구매(`existOrdered == false`)이면 Figma paywall(잠금 + 가격) 표시를 따른다.
|
||||||
- 유료 게시글을 구매한 경우(`existOrdered == true`) Figma 기준 `구매완료` badge를 표시한다.
|
- 유료 게시글 구매 완료 badge는 2026-08-10 후속 요구사항에 따라 표시하지 않는다.
|
||||||
- 하트 터치 시 좋아요 API(`communityPostLike(postId)`)를 호출한다.
|
- 하트 터치 시 좋아요 API(`communityPostLike(postId)`)를 호출한다.
|
||||||
- `isLiked == true`이면 `ic_feed_community_heart_fill`, `false`이면 `ic_feed_community_heart`를 표시한다.
|
- `isLiked == true`이면 `ic_feed_community_heart_fill`, `false`이면 `ic_feed_community_heart`를 표시한다.
|
||||||
- 좋아요 수(`likeCount`)를 하트 옆에 표시한다.
|
- 좋아요 수(`likeCount`)를 하트 옆에 표시한다.
|
||||||
@@ -211,6 +307,9 @@ Figma `290:9266` 기준으로 댓글 불가 상태를 처리한다.
|
|||||||
|
|
||||||
## 8. UX / UI Expectations
|
## 8. UX / UI Expectations
|
||||||
- 게시글 item 터치부터 상세 진입까지 자연스럽게 이어진다.
|
- 게시글 item 터치부터 상세 진입까지 자연스럽게 이어진다.
|
||||||
|
- 홈과 커뮤니티 탭의 게시글 card 본문은 상세 이동만 수행하고, 별도 더보기 버튼은 권한별 관리/신고 메뉴만 제공한다. 구매·재생은 제공하지 않는다.
|
||||||
|
- 홈·커뮤니티 탭 리스트형·썸네일형·상세의 유료 미구매 가격은 동일한 잠금/가격 capsule 구조로 표시한다.
|
||||||
|
- 상세·홈·커뮤니티 리스트형 `ic_seemore_vertical` 버튼은 게시글 작성자 영역 우측 끝에 정렬하고 닉네임·작성 시간과 겹치지 않게 한다.
|
||||||
- 커뮤니티 상세 상단 게시글 영역은 목록 카드처럼 rounded 배경을 깔지 않고, 게시글과 댓글 영역 및 댓글 사이에는 Figma `290:9243` 기준 1dp border를 표시한다.
|
- 커뮤니티 상세 상단 게시글 영역은 목록 카드처럼 rounded 배경을 깔지 않고, 게시글과 댓글 영역 및 댓글 사이에는 Figma `290:9243` 기준 1dp border를 표시한다.
|
||||||
- 하트 상태와 좋아요 수는 터치 즉시 반응한다(낙관적 업데이트 후 실패 시 롤백).
|
- 하트 상태와 좋아요 수는 터치 즉시 반응한다(낙관적 업데이트 후 실패 시 롤백).
|
||||||
- 입력 영역은 키보드 있음/없음 상태를 기준으로 자연스럽게 대응한다.
|
- 입력 영역은 키보드 있음/없음 상태를 기준으로 자연스럽게 대응한다.
|
||||||
@@ -229,18 +328,30 @@ Figma `290:9266` 기준으로 댓글 불가 상태를 처리한다.
|
|||||||
- 커뮤니티 댓글/답글 신고는 현재 전용 신고 API/레거시 흐름이 없어 이번 구현 범위에서 제외하고, 기존 레거시 메뉴2의 실제 동작인 채널 크리에이터 삭제만 재사용한다.
|
- 커뮤니티 댓글/답글 신고는 현재 전용 신고 API/레거시 흐름이 없어 이번 구현 범위에서 제외하고, 기존 레거시 메뉴2의 실제 동작인 채널 크리에이터 삭제만 재사용한다.
|
||||||
- 신규 문자열은 `values`, `values-en`, `values-ja`에 추가하거나 기존 문자열을 재사용한다.
|
- 신규 문자열은 `values`, `values-en`, `values-ja`에 추가하거나 기존 문자열을 재사용한다.
|
||||||
- 오디오 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
|
- 오디오 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
|
||||||
|
- 홈 커뮤니티 표시는 기존 `FeedCommunityView`와 `FeedItem.Community`를 기본값 기반으로 최소 확장하며 다른 feed 호출부의 기존 동작을 유지한다.
|
||||||
|
- 상세·홈·커뮤니티 리스트형 게시글 메뉴는 기존 `CreatorCommunityPostMenuBottomSheetDialog`, `CreatorCommunityReportDialog`, `CreatorCommunityModifyActivity`, `CreatorCommunityRepository`를 호출해 사용하고 레거시 파일은 수정하지 않는다.
|
||||||
|
- 홈·커뮤니티 리스트형 메뉴 실행과 mutation은 `CreatorChannelActivity`의 공용 처리 흐름을 사용하고 화면별 중복 mutation 구현이나 신규 범용 coordinator를 추가하지 않는다.
|
||||||
|
- UI 자동 테스트는 Response/mapper/presentation/routing/result 전파 로직만 검증하고 View 크기·간격·constraint·visibility 속성을 직접 검증하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Metrics
|
## 10. Metrics
|
||||||
- 별도 분석 이벤트 추가는 이번 범위에서 정의하지 않는다.
|
- 별도 분석 이벤트 추가는 이번 범위에서 정의하지 않는다.
|
||||||
- 기능 성공 기준은 게시글 상세 진입, 댓글/답글 조회, 좋아요/댓글/답글 작성·수정·삭제가 정상 동작하고 목록에 반영되는 것이다.
|
- 기능 성공 기준은 게시글 상세 진입, 댓글/답글 조회, 좋아요/댓글/답글 작성·수정·삭제가 정상 동작하고 목록에 반영되는 것이다.
|
||||||
|
- 2026-08-10 후속 성공 기준은 홈 Response 신규 필드가 고정/댓글 UI에 반영되고, 미구매 유료 홈 item reaction이 숨겨지며, card 본문은 상세 이동만 수행하고 권한별 더보기 메뉴가 상세·홈·커뮤니티 리스트형에 표시되며, 구매·메뉴 결과가 홈과 커뮤니티 탭에 반영되는 것이다.
|
||||||
- UI 완료 기준은 Figma `290:9243`, `290:9266`, `669:40462`, `669:40468`의 주요 layout 구조, 하트/전송 버튼 상태, 댓글 불가 처리, 입력 영역 동작이 구현 화면에 반영되었는지 확인되는 것이다.
|
- UI 완료 기준은 Figma `290:9243`, `290:9266`, `669:40462`, `669:40468`의 주요 layout 구조, 하트/전송 버튼 상태, 댓글 불가 처리, 입력 영역 동작이 구현 화면에 반영되었는지 확인되는 것이다.
|
||||||
- 자동 검증 기준은 조회 API 계약(page/size 기본값, 응답 매핑), 하트 icon/전송 버튼 상태 계약, 댓글 불가 게시글의 댓글/입력 영역 숨김 계약을 source/resource/ViewModel 테스트로 확인하는 것이다.
|
- 자동 검증 기준은 조회 API 계약(page/size 기본값, 응답 매핑), 하트 icon/전송 버튼 상태 계약, 댓글 불가 게시글의 댓글/입력 영역 숨김 계약과 홈 신규 Response 필드, 잠금·댓글·핀 표시 조건, 상세·홈·커뮤니티 리스트형 더보기 권한, 결과 전파를 local unit test로 확인하는 것이다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Open Questions & Decisions
|
## 11. Open Questions & Decisions
|
||||||
|
- 결정(2026-08-10, 목록 더보기 후속): 상세와 동일한 권한별 더보기 메뉴를 홈 커뮤니티 item과 커뮤니티 탭 리스트형에 적용한다. 작성자 영역이 없는 커뮤니티 그리드형은 제외한다.
|
||||||
|
- 결정(2026-08-10, 목록 click): card 본문은 상세 이동, 더보기 버튼은 메뉴 표시로 분리하며 더보기 touch는 상세 이동으로 전파하지 않는다.
|
||||||
|
- 결정(2026-08-10, 구현 구조): 홈·커뮤니티 리스트형은 `CreatorChannelActivity`의 공용 메뉴/mutation 흐름을 사용하고 기존 상세 메뉴 구현은 유지한다.
|
||||||
|
- 결정(2026-08-10, 더보기 권한): 작성자는 구매 여부와 관계없이 관리 메뉴를 표시하고, 일반 사용자는 무료 또는 구매 완료 게시글에서만 신고 메뉴를 표시한다.
|
||||||
|
- 결정(2026-08-10, 가격 UI): 커뮤니티 탭 리스트형과 썸네일형 모두 홈/상세의 잠금/가격 capsule 표현을 따른다.
|
||||||
|
- 결정(2026-08-10, 홈 고정 표시): 홈 Response의 `isPinned`를 홈 커뮤니티 item 고정 표시에 사용한다.
|
||||||
|
- 결정(2026-08-10, 문서 연속성): 상세 구매 후속 요구사항을 포함하는 기존 문서에 이번 홈/탭/상세 정리 범위를 이어서 기록한다.
|
||||||
- 결정(재사용 vs 복사): 답글 목록 화면 UI는 팬Talk 상세와 동일하지만 조회 API가 다르고, 현재 팬Talk 상세 화면은 v2에 미구현이다. 따라서 이번 범위에서는 **커뮤니티 전용 답글 화면으로 신규 구현하되, layout XML과 댓글/답글 item view(ViewHolder)만 데이터/콜백 주입형으로 설계해 재사용 가능하게 둔다.** ViewModel/Repository/데이터 모델/화면 컨트롤러는 커뮤니티 전용으로 분리한다.
|
- 결정(재사용 vs 복사): 답글 목록 화면 UI는 팬Talk 상세와 동일하지만 조회 API가 다르고, 현재 팬Talk 상세 화면은 v2에 미구현이다. 따라서 이번 범위에서는 **커뮤니티 전용 답글 화면으로 신규 구현하되, layout XML과 댓글/답글 item view(ViewHolder)만 데이터/콜백 주입형으로 설계해 재사용 가능하게 둔다.** ViewModel/Repository/데이터 모델/화면 컨트롤러는 커뮤니티 전용으로 분리한다.
|
||||||
- 근거: 아직 팬Talk 상세라는 두 번째 사용처가 없어 지금 데이터 계층까지 공통 추상화하면 단일 사용처를 위한 추측성 추상화가 된다. UI 리소스/item view만 공용화하면 추후 팬Talk 상세 구현 시 동일 UI를 그대로 재사용할 수 있다.
|
- 근거: 아직 팬Talk 상세라는 두 번째 사용처가 없어 지금 데이터 계층까지 공통 추상화하면 단일 사용처를 위한 추측성 추상화가 된다. UI 리소스/item view만 공용화하면 추후 팬Talk 상세 구현 시 동일 UI를 그대로 재사용할 수 있다.
|
||||||
- 남은 확인: 없음.
|
- 남은 확인: 없음.
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# Phase 15 커뮤니티 구매·상세 메뉴 리뷰 보고서
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 15 / `P15-T1`~`P15-T5`, `P15-GATE` |
|
||||||
|
| 기준 commit 또는 working tree | `02cd583f` 기반 미커밋 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-10 |
|
||||||
|
| 리뷰어 | Codex, 독립 재검토 `phase15_review` |
|
||||||
|
| 기준 문서 | `prd.md` F0-20260810, `plan-task.md` Phase 15 |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- Phase 15의 미체크 항목이 단순 체크 누락인지 미구현인지 확인한다.
|
||||||
|
- PRD의 홈 Response, 유료 미구매 표시, 상세 메뉴 권한, 상위 새로고침 요구사항을 코드·테스트·Gate와 대조한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: Phase 15 파일 구조에 나열된 홈, 커뮤니티 탭, 상세, 상위 Activity, action 파일
|
||||||
|
- 테스트: Phase 15 focused local unit/source contract 테스트
|
||||||
|
- 문서: `prd.md` F0-20260810, `plan-task.md` Phase 15
|
||||||
|
- 수동 검증: 코드/diff 정적 검토
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA
|
||||||
|
- 서버 Response 실제 배포 여부와 외부 API 운영 상태
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 보안·데이터 손실, 핵심 흐름 불능 |
|
||||||
|
| High | 확정 PRD/API 계약 위반 또는 주요 회귀 |
|
||||||
|
| Medium | 제한 조건의 기능·접근성·복구 문제 |
|
||||||
|
| Low | 유지보수성, 문서 정합성, style Gate 문제 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
### 문서와 코드
|
||||||
|
|
||||||
|
- 요구사항: PRD `Home Response Contract`, `Home Community Requirements`, `Community Tab Requirements`, `Detail Purchase and Badge Requirements`, `Detail More Menu Requirements`, `Change Propagation Requirements`
|
||||||
|
- 계획: `P15-T1`~`P15-T5`, `P15-GATE`
|
||||||
|
- 코드/테스트: Phase 15 파일 구조와 각 Task에 명시된 파일
|
||||||
|
|
||||||
|
### 실행 환경
|
||||||
|
|
||||||
|
```text
|
||||||
|
OS: Darwin 25.0.0 x86_64
|
||||||
|
Java: OpenJDK 17.0.15
|
||||||
|
Android/Gradle: 저장소 wrapper 및 debug variant
|
||||||
|
민감 환경 값: 기록하지 않음
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행한 검증
|
||||||
|
|
||||||
|
| 명령 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 홈 focused unit test | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| 커뮤니/action focused unit test | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `CreatorChannelActivitySourceTest` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:mergeDebugResources` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:compileDebugKotlin` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:ktlintCheck` | 실패 | `CreatorChannelActivity.kt:29:1 Unused import` 1건 |
|
||||||
|
| `git diff --check` | 성공 | 출력 없음 |
|
||||||
|
| 수정 후 전체 focused test | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| 수정 후 resource/compile/ktlint Gate | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| 수정 후 `git diff --check`, `git diff HEAD --check` | 성공 | 출력 없음 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P15-001` | Low | 수정 완료 | 목록 메뉴 제거 후 미사용 import가 style Gate를 막음 | `P15-R1` | 미사용 import 1줄 제거 및 Gate 재검증 |
|
||||||
|
| `REV-P15-002` | High | 수정 완료 | 홈 reaction id가 작성자 row에 연결됨 | `P15-R2` | reaction id를 실제 하트·댓글 row로 이동 및 Gate 재검증 |
|
||||||
|
| `REV-P15-003` | High | 수정 완료 | 공유 FeedItem 잠금 정책이 다른 홈 화면 reaction을 숨김 | `P15-R3` | 크리에이터 채널 홈만 잠금 숨김 정책을 선택하고 공유 기본값 회귀 검증 |
|
||||||
|
| `REV-P15-004` | Low | 수정 완료 | 메인 홈 회귀 테스트 파일의 미사용 import가 style Gate를 막음 | `P15-R4` | 미사용 import 1줄 제거 및 Gate 재검증 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P15-001 — 목록 메뉴 제거 후 미사용 import가 style Gate를 막음
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** `P15-T5` orphan 제거, `P15-GATE`
|
||||||
|
- **소유 Task:** `P15-R1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
`CreatorChannelActivity`의 목록 고정/삭제 로직을 제거한 후 `SharedPreferenceManager` import가 남아 `ktlintCheck`가 실패했다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 코드: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt:29`
|
||||||
|
- 검증: `./gradlew :app:ktlintCheck` 결과 `Unused import (no-unused-imports)`
|
||||||
|
- 문서: `plan-task.md` `P15-T5`, `P15-GATE`
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
기능 동작에는 영향이 없으나 Phase 15 style Gate를 통과할 수 없다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
`SharedPreferenceManager` import 1줄만 제거하고 `ktlintCheck`와 Phase 15 Gate를 재실행한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-10 — 기능 테스트·resource merge·compile은 통과했고 ktlint 정확한 1건으로 확정했다.
|
||||||
|
- 2026-08-10 — `SharedPreferenceManager` import 1줄 제거 후 최종 `ktlintCheck`와 Phase 15 Gate가 PASS했다.
|
||||||
|
|
||||||
|
### REV-P15-002 — 홈 reaction id가 작성자 row에 연결됨
|
||||||
|
|
||||||
|
- **심각도:** High
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** PRD `Home Community Requirements`, `P15-T2`
|
||||||
|
- **소유 Task:** `P15-R2`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
`FeedCommunityView.bind()`는 `ll_feed_community_reaction`에 `item.showReaction`을 적용하지만, XML의 해당 id가 프로필·닉네임·작성 시간 row에 붙어 있다. 실제 하트·댓글 row에는 id가 없다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- XML: `view_feed_community.xml:36-41`의 `ll_feed_community_reaction` 아래에 `iv_feed_community_profile`이 있음
|
||||||
|
- XML: `view_feed_community.xml:154-195`의 실제 reaction row에 id가 없음
|
||||||
|
- 코드: `FeedCommunityView.kt:51`, `FeedCommunityView.kt:81`
|
||||||
|
|
||||||
|
**재현 또는 검증 절차**
|
||||||
|
|
||||||
|
1. `price > 0 && existOrdered == false`인 홈 item의 `showReaction == false`를 확인한다.
|
||||||
|
2. `FeedCommunityView.bind()`가 `ll_feed_community_reaction.isVisible = false`를 적용한다.
|
||||||
|
3. XML tree에서 해당 id가 작성자 row를 가리키므로 작성자 row가 숨겨진다.
|
||||||
|
4. 실제 reaction row는 자신의 visibility binding을 받지 않아 하트·댓글 UI가 남는다.
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
유료 미구매 홈 item이 PRD와 반대로 작성자 영역을 숨기고 reaction UI를 노출한다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
`ll_feed_community_reaction` id를 작성자 row에서 실제 하트·댓글 row로 이동한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-10 — XML tree와 binding 대상을 대조해 PRD 2번을 재현하는 실제 결함으로 확정했다.
|
||||||
|
- 2026-08-10 — id를 실제 하트·댓글 reaction row로 이동하고 focused/resource/compile 및 최종 Gate PASS를 확인했다.
|
||||||
|
|
||||||
|
### REV-P15-003 — 공유 FeedItem 잠금 정책이 다른 홈 화면 reaction을 숨김
|
||||||
|
|
||||||
|
- **심각도:** High
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** PRD `Edge Cases`, `Constraints & Dependencies`, `P15-T1`~`P15-T2`
|
||||||
|
- **소유 Task:** `P15-R3`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
`FeedItem.Community.showReaction = !isLocked`와 공유 `FeedCommunityView.bind()` 조합이 크리에이터 채널 홈뿐 아니라 메인 홈 인기 커뮤니티에도 적용된다. 메인 홈 mapper도 `price`, `existOrdered`를 전달하므로 유료 미구매 item의 기존 reaction 표시가 함께 사라진다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 공유 정책: `FeedItem.kt`의 `showReaction`, `showComment`
|
||||||
|
- 다른 호출부: `HomeRecommendationMappers.kt:84-103`이 `price`, `existOrdered`를 전달
|
||||||
|
- 공유 View 사용: `HomePopularCommunityAdapter.kt:49`의 `view.bind(item.item)`
|
||||||
|
- 문서: PRD `다른 화면에서 사용하는 FeedItem.Community는 신규 필드 기본값으로 기존 표시 동작을 유지한다.`
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
요구 범위 밖인 메인 홈 인기 커뮤니티의 유료 미구매 item 표시가 변경되어 기존 화면 회귀가 발생한다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
공유 모델의 신규 정책 기본값은 기존 reaction 표시를 유지하고, 크리에이터 채널 홈 mapper만 잠금 reaction 숨김을 명시적으로 선택한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-10 — 전체 `FeedItem.Community` production 생성자와 공유 View 호출부를 추적해 실제 범위 확장 결함으로 확정했다.
|
||||||
|
- 2026-08-10 — 기본값은 기존 reaction 표시를 유지하고 크리에이터 채널 홈만 opt-in하도록 수정했다. 공유 모델·메인 홈 mapper 회귀 테스트와 최종 Gate가 PASS했다.
|
||||||
|
|
||||||
|
### REV-P15-004 — 메인 홈 회귀 테스트 파일의 미사용 import가 style Gate를 막음
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** `P15-R3`, `P15-GATE`
|
||||||
|
- **소유 Task:** `P15-R4`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
메인 홈 mapper 회귀 테스트 추가로 `HomeMainFragmentLayoutTest`가 다시 lint 대상이 되면서 사용처가 없는 `ColorDrawable` import 1건이 확인됐다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 코드: `HomeMainFragmentLayoutTest.kt:6`
|
||||||
|
- 검증: `./gradlew :app:ktlintCheck` 결과 `Unused import`
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
기능 동작에는 영향이 없으나 Phase 15 style Gate가 실패한다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
사용처가 없는 `ColorDrawable` import 1줄만 제거한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-10 — `rg`로 import 외 사용처가 없음을 확인해 정확한 style Gate 결함으로 확정했다.
|
||||||
|
- 2026-08-10 — `ColorDrawable` import 1줄 제거 후 `ktlintCheck`와 최종 focused test Gate가 PASS했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
- `plan-task.md` Phase 15에 `Task 15.R1` / goal `P15-R1`, `Task 15.R2` / goal `P15-R2`, `Task 15.R3` / goal `P15-R3`, `Task 15.R4` / goal `P15-R4`를 추가했다.
|
||||||
|
- 각 항목의 대상 파일, 최소 수정 범위, 회귀 검증, 완료 증거를 명시했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | PRD/plan·production/test/diff 대조 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P15-001`~`REV-P15-004` 확정 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `P15-R1`~`P15-R4` |
|
||||||
|
| 보류 항목 기록 | 해당 없음 | 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | `P15-R1`~`P15-R4` 수정 후 최종 Gate PASS |
|
||||||
|
|
||||||
|
**최종 결론:** 수정 검증 완료
|
||||||
|
|
||||||
|
**남은 항목:** 없음. 기기·에뮬레이터 수동 UI 확인과 실제 서버 Response 배포 상태는 리뷰 범위 밖이다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- `REV-P15-001`: `CreatorChannelActivity`의 미사용 `SharedPreferenceManager` import 1줄을 제거했다.
|
||||||
|
- `REV-P15-002`: `ll_feed_community_reaction` id를 프로필 row에서 실제 하트·댓글 row로 이동했다.
|
||||||
|
- `REV-P15-003`: `FeedItem.Community.hideReactionWhenLocked` 기본값을 `false`로 두고 크리에이터 채널 홈 mapper만 `true`를 전달했다. 메인 홈 인기 커뮤니티 잠금 item의 기존 reaction 표시 회귀 테스트를 추가했다.
|
||||||
|
- `REV-P15-004`: `HomeMainFragmentLayoutTest`의 미사용 `ColorDrawable` import 1줄을 제거했다.
|
||||||
|
- 최종 전체 focused test: `BUILD SUCCESSFUL`.
|
||||||
|
- 최종 `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`: `BUILD SUCCESSFUL`.
|
||||||
|
- 최종 `git diff --check`, `git diff HEAD --check`: 출력 없이 PASS.
|
||||||
|
- 정적 재검토: 구매 호출은 상세에만 존재하고, 홈·커뮤니티 item은 상세 이동만 수행하며, 상세 배지 제거·권한별 메뉴·홈+커뮤니티 refresh 계약이 유지됨을 확인했다.
|
||||||
|
- 독립 재검토: Critical/Minor 없음, Important 1건(`REV-P15-003`)을 수정 반영했다.
|
||||||
|
- 수정 후 독립 최종 재검토: `REV-P15-001`~`REV-P15-004` 반영을 확인했고 새 Critical/Important/Minor 이슈 없음으로 판정했다.
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Phase 16 홈·커뮤니티 리스트 더보기 메뉴 리뷰 보고서
|
||||||
|
|
||||||
|
## 1. 리뷰 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 리뷰 대상 | Phase 16 / `P16-T1`~`P16-T3`, `P16-GATE` |
|
||||||
|
| 기준 commit 또는 working tree | 현재 `main` 미커밋 working tree |
|
||||||
|
| 리뷰 일자 | 2026-08-10 |
|
||||||
|
| 리뷰어 | Sisyphus, 독립 목표·QA·품질·보안·맥락 검토 |
|
||||||
|
| 기준 문서 | `prd.md` F0-20260810, `plan-task.md` Phase 16 |
|
||||||
|
| 리뷰 상태 | 수정 검증 완료 |
|
||||||
|
|
||||||
|
## 2. 리뷰 목적과 범위
|
||||||
|
|
||||||
|
### 목적
|
||||||
|
|
||||||
|
- 홈과 커뮤니티 리스트형의 권한별 더보기 메뉴가 PRD와 일치하는지 확인한다.
|
||||||
|
- 완료 체크박스와 production/test/Gate/문서 기록이 일치하는지 확인한다.
|
||||||
|
|
||||||
|
### 포함 범위
|
||||||
|
|
||||||
|
- 코드: Phase 16 공용 권한 모델, 홈·리스트 callback, Activity 메뉴·mutation
|
||||||
|
- 테스트: Phase 16 focused local unit/source contract 테스트
|
||||||
|
- 문서: `prd.md` F0-20260810, `plan-task.md` Phase 16
|
||||||
|
- 수동 검증: 코드·XML·diff 정적 검토
|
||||||
|
|
||||||
|
### 제외 범위
|
||||||
|
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA
|
||||||
|
- 실제 서버 Response와 API mutation, Git commit, 별도 worktree
|
||||||
|
|
||||||
|
## 3. 판정 기준
|
||||||
|
|
||||||
|
| 심각도 | 기준 |
|
||||||
|
|---|---|
|
||||||
|
| Blocker | 보안·데이터 손실, 핵심 흐름 불능 |
|
||||||
|
| High | 확정 PRD/API 계약 위반 또는 주요 회귀 |
|
||||||
|
| Medium | 제한 조건의 기능·접근성·복구 문제 |
|
||||||
|
| Low | 유지보수성, 문서 정합성, style Gate 문제 |
|
||||||
|
|
||||||
|
## 4. 검토한 근거
|
||||||
|
|
||||||
|
- 요구사항: PRD `Home and Community List More Menu Requirements`, `Change Propagation Requirements`
|
||||||
|
- 계획: `P16-T1`~`P16-T3`, `P16-GATE`
|
||||||
|
- 코드/테스트: Phase 16 Task에 명시된 파일과 focused test
|
||||||
|
- 실행 환경: macOS, 저장소 Gradle wrapper, debug variant
|
||||||
|
|
||||||
|
| 명령 또는 검증 | 결과 | 핵심 증거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 홈·Feed·Activity focused test | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| community/action focused test | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:mergeDebugResources` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:compileDebugKotlin` | 성공 | `BUILD SUCCESSFUL` |
|
||||||
|
| `./gradlew :app:ktlintCheck` | 성공 | 기존 `disabled_rules` 경고만 존재 |
|
||||||
|
| `git diff --check`, `git diff HEAD --check` | 성공 | 출력 없음 |
|
||||||
|
|
||||||
|
## 5. 발견 사항 요약
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `REV-P16-001` | Low | 수정 완료 | Phase 16 구현·Gate 완료 상태가 문서에 누락됨 | `P16-R1` | 완료 기록과 Verification Log 누적 |
|
||||||
|
|
||||||
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
|
### REV-P16-001 — Phase 16 구현·Gate 완료 상태가 문서에 누락됨
|
||||||
|
|
||||||
|
- **심각도:** Low
|
||||||
|
- **상태:** 수정 완료
|
||||||
|
- **관련 요구사항:** Phase 16 완료 조건, `P16-GATE`
|
||||||
|
- **소유 Task:** `P16-R1`
|
||||||
|
|
||||||
|
**관찰 내용**
|
||||||
|
|
||||||
|
모든 Phase 16 Task와 Gate가 통과했지만 계획 기록은 `구현 미시작`으로 남고 `Verification Log`에 Gate 결과가 없었다.
|
||||||
|
|
||||||
|
**근거**
|
||||||
|
|
||||||
|
- 문서: `plan-task.md` Phase 16 계획 기록과 `Verification Log`
|
||||||
|
- 검증: 최종 5개 리뷰 중 목표·코드 품질·맥락 검토가 동일 문서 불일치를 차단 이슈로 판정
|
||||||
|
|
||||||
|
**영향**
|
||||||
|
|
||||||
|
기능 동작에는 영향이 없지만 문서상 완료 조건을 충족하지 못해 Phase 완료 판정을 내릴 수 없다.
|
||||||
|
|
||||||
|
**권장 조치**
|
||||||
|
|
||||||
|
기존 계획 기록은 보존하고 구현·Gate 완료 기록과 `Verification Log`를 누적한 뒤 최종 리뷰를 재실행한다.
|
||||||
|
|
||||||
|
**판정 기록**
|
||||||
|
|
||||||
|
- 2026-08-10 — 코드와 Gate는 통과했으나 완료 문서가 실제 상태와 달라 확정했다.
|
||||||
|
- 2026-08-10 — 기존 계획 기록을 보존하고 Phase 16 구현·Gate 완료 기록과 `Verification Log`를 누적했다.
|
||||||
|
|
||||||
|
## 7. 확정 항목의 plan·goal 전환
|
||||||
|
|
||||||
|
- `plan-task.md`에 `Task 16.R1` / goal `P16-R1`을 추가했다.
|
||||||
|
|
||||||
|
## 8. 리뷰 종료 판정
|
||||||
|
|
||||||
|
| 판정 항목 | 결과 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 리뷰 범위 전체 확인 | 충족 | PRD/plan·production/test/diff 대조 |
|
||||||
|
| 후보 항목 판정 완료 | 충족 | `REV-P16-001` 확정 |
|
||||||
|
| 확정 항목 plan 반영 | 충족 | `P16-R1` |
|
||||||
|
| 보류 항목 기록 | 해당 없음 | 없음 |
|
||||||
|
| 검증 명령과 결과 기록 | 충족 | Phase 16 구현 기록과 `Verification Log` 누적 |
|
||||||
|
|
||||||
|
**최종 결론:** 수정 검증 완료
|
||||||
|
|
||||||
|
**남은 항목:** 없음. 실제 서버 권한 검증과 기기 UI 확인은 리뷰 범위 밖이다.
|
||||||
|
|
||||||
|
## 9. 수정 후 검증 기록
|
||||||
|
|
||||||
|
- 수정 전: Phase 16 계획 기록은 구현 미시작이며 `Verification Log`에 Gate 결과가 없었다.
|
||||||
|
- 1차 수정: 기존 계획 기록 뒤에 구현·Gate 완료 기록을 추가하고 `Verification Log`에 최종 자동 검증과 제외 범위를 누적했다.
|
||||||
|
- 최종 검증: `git diff --check`, `git diff HEAD --check`가 출력 없이 PASS했다.
|
||||||
|
- 목표·제약 검토: PASS, 차단 이슈 없음.
|
||||||
|
- 실행 QA: PASS, 허용된 P0 시나리오 11/11과 focused 128개, community/action 96개 PASS.
|
||||||
|
- 코드 품질 검토: PASS, 차단 이슈 없음.
|
||||||
|
- 보안 검토: PASS, 클라이언트 범위의 Critical/High 이슈 없음. 서버 소유권·rate limit 검증은 저장소 밖이라 확인하지 못했다.
|
||||||
|
- 맥락 검토: PASS, 추가 요구사항·호출자·Git 이력 충돌 없음.
|
||||||
|
|
||||||
|
### 2차 독립 재점검 — 2026-08-11
|
||||||
|
|
||||||
|
- 문서 체크: `plan-task.md`의 실제 Task 체크박스는 모두 완료 상태이며, 체크 누락 또는 미구현 항목이 없음을 production/test와 역대조했다.
|
||||||
|
- 요구사항 대조: 더보기 권한 truth table, 홈·리스트 root/more click 분리, grid 제외, 작성자 관리/일반 사용자 신고, 성공 관리 작업의 홈+커뮤니티 갱신, 신고·실패 no-refresh가 PRD와 일치했다.
|
||||||
|
- 공유 회귀 대조: `FeedItem.Community.hideReactionWhenLocked=false`, `showMore=false` 기본값과 크리에이터 채널 홈의 명시적 opt-in을 확인해 다른 공유 feed의 기존 표시를 보존함을 확인했다.
|
||||||
|
- 자동 검증: Phase 15·16 focused test, community/action focused test, 전체 `:app:testDebugUnitTest`, resource merge, Kotlin compile, ktlint, 두 diff check가 모두 PASS했다.
|
||||||
|
- 독립 리뷰: Critical/Important/Minor 이슈 없음, 승인 가능 판정. 새 확정 항목이 없어 회귀 수정 Task와 production/test 수정은 추가하지 않았다.
|
||||||
307
docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv
Normal file
307
docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
,KR,JP,,,,
|
||||||
|
,홈,ホーム,,,,
|
||||||
|
,콘텐츠,コンテンツ,,,,
|
||||||
|
,대화,トーク,,,,
|
||||||
|
,마이,マイページ,,,,
|
||||||
|
,추천,おすすめ,,,,
|
||||||
|
,랭킹,ランキング,,,,
|
||||||
|
,팔로잉,フォロー中,,,,
|
||||||
|
,전체,すべて,,,,
|
||||||
|
,방금 활동한 크리에이터,最近活動したクリエイター,,,,
|
||||||
|
,최근 데뷔한 크리에이터,新着クリエイター,,,,
|
||||||
|
,처음부터 함께 성장!,今から推せる,,,,
|
||||||
|
,크리에이터와 이야기를 나눠요!,クリエイターと話す,,,,
|
||||||
|
,추천 필모그래피,おすすめフィルモグラフィー,,,,
|
||||||
|
,또 다른 모습,写真集,,,,
|
||||||
|
,{{장르}}의 크리에이터,{{ジャンル}}のクリエイター,,,,
|
||||||
|
,최근 응원이 많은 크리에이터,最近応援の多いクリエイター,,,,
|
||||||
|
,인기 커뮤니티,人気コミュニティ,,,,
|
||||||
|
,주간 인기,週間人気,,,,
|
||||||
|
,지금 뜨는 중,急上昇,,,,
|
||||||
|
,남성 인기,男性人気,,,,
|
||||||
|
,여성 인기,女性人気,,,,
|
||||||
|
,최근 대화,最近のトーク,,,,
|
||||||
|
,이달의 스케줄,今月のスケジュール,,,,
|
||||||
|
,최근 소식,最新情報,,,,
|
||||||
|
,{{크리에이터 이름}}님이 이번 주 랭킹 n위에 올랐어요!,{{크리에이터 이름}}が今週のランキングでn位にランクイン!,,,,
|
||||||
|
,아직 팔로우한 크리에이터가 없어요. 관심 있는 크리에이터를 팔로우해 보세요.,"まだフォローしているクリエイターがいません。
|
||||||
|
気になるクリエイターをフォローしてみましょう。",,,,
|
||||||
|
,찾아보기,探す,,,,
|
||||||
|
,오직 보이스온에서만!,VOICE ON限定,,,,
|
||||||
|
,새로 올라온 오디오,新着オーディオ,,,,
|
||||||
|
,추천 시리즈,おすすめシリーズ,,,,
|
||||||
|
,무료 오디오,無料オーディオ,,,,
|
||||||
|
,포인트 오디오,ポイントオーディオ,,,,
|
||||||
|
,최근 댓글이 많은 오디오,最近コメントの多いオーディオ,,,,
|
||||||
|
,{{키워드}}의 오디오,{{キーワード}}のオーディオ,,,,
|
||||||
|
,추천 오디오,おすすめオーディオ,,,,
|
||||||
|
,매출,売上,,,,
|
||||||
|
,판매량,販売数,,,,
|
||||||
|
,댓글수,コメント数,,,,
|
||||||
|
,좋아요,いいね,,,,
|
||||||
|
,오디오,オーディオ,,,,
|
||||||
|
,시리즈,シリーズ,,,,
|
||||||
|
,오리지널,オリジナル,,,,
|
||||||
|
,무료,無料,,,,
|
||||||
|
,포인트,ポイント,,,,
|
||||||
|
,화보,フォト,,,,
|
||||||
|
,최신순,最新順,,,,
|
||||||
|
,추천순,おすすめ順,,,,
|
||||||
|
,인기순,人気順,,,,
|
||||||
|
,높은 가격순,高い順,,,,
|
||||||
|
,낮은 가격순,安い順,,,,
|
||||||
|
,소장순,保有順,,,,
|
||||||
|
,팔로우,フォロー,,,,
|
||||||
|
,팔로워,フォロワー,,,,
|
||||||
|
,대화하기,トークする,,,,
|
||||||
|
,DM 보내기,DMを送る,,,,
|
||||||
|
,라이브,ライブ,,,,
|
||||||
|
,커뮤니티,コミュニティ,,,,
|
||||||
|
,팬Talk,ファントーク,,,,
|
||||||
|
,후원,サポート,,,,
|
||||||
|
,팬레터,ファンレター,,,,
|
||||||
|
,후원하기,サポートする,,,,
|
||||||
|
,공지,お知らせ,,,,
|
||||||
|
,스케줄,スケジュール,,,,
|
||||||
|
,전체보기,すべて見る,,,,
|
||||||
|
,전체 오디오의 n%를 소장하고 있어요.,全オーディオのn%を所持しています。,,,,
|
||||||
|
,전체 화보의 n%를 소장하고 있어요.,全フォトのn%を所持しています。,,,,
|
||||||
|
,전체소장,全話購入,,,,
|
||||||
|
,완결,完結,,,,
|
||||||
|
,연재,連載,,,,
|
||||||
|
,소장중,保有中,,,,
|
||||||
|
,리스트형,リスト,,,,
|
||||||
|
,썸네일형,グリッド,,,,
|
||||||
|
,구매완료,購入済み,,,,
|
||||||
|
,삭제,削除,,,,
|
||||||
|
,신고,通報,,,,
|
||||||
|
,후원 랭킹,サポートランキング,,,,
|
||||||
|
,"크리에이터가 라이브를 준비 중입니다.
|
||||||
|
기대해 주세요!","クリエイターがライブを準備しています。
|
||||||
|
お楽しみに!",,,,
|
||||||
|
,"크리에이터가 오디오를 준비 중입니다.
|
||||||
|
기대해 주세요!","クリエイターがオーディオを準備しています。
|
||||||
|
お楽しみに!",,,,
|
||||||
|
,"크리에이터가 시리즈를 준비 중입니다.
|
||||||
|
기대해 주세요!","クリエイターがシリーズを準備しています。
|
||||||
|
お楽しみに!",,,,
|
||||||
|
,"크리에이터가 화보를 준비 중입니다.
|
||||||
|
기대해 주세요!","クリエイターがフォトを準備しています。
|
||||||
|
お楽しみに!",,,,
|
||||||
|
,"크리에이터가 커뮤니티를 준비 중입니다.
|
||||||
|
기대해 주세요!","クリエイターがコミュニティを準備しています。
|
||||||
|
お楽しみに!",,,,
|
||||||
|
,"아직 응원이 없습니다.
|
||||||
|
처음으로 크리에이터를 응원해 보세요!","まだファンレターがありません。
|
||||||
|
最初のファンレターを送ってみましょう!",,,,
|
||||||
|
,응원 남기기,ファンレターを送る,,,,
|
||||||
|
,"아직 후원이 없습니다.
|
||||||
|
처음으로 크리에이터를 후원해 보세요!","まだサポートがありません。
|
||||||
|
最初のサポートをしてみましょう!",,,,
|
||||||
|
,당신의 첫 응원을 기다리고 있어요!,あなたの最初のファンレターを待っています!,,,,
|
||||||
|
,댓글,コメント,,,,
|
||||||
|
,취소,キャンセル,,,,
|
||||||
|
,보내기,送信,,,,
|
||||||
|
,입력 종료,入力終了,,,,
|
||||||
|
,"입력을 종료할까요?
|
||||||
|
지금 나가면 입력한 내용이 저장되지 않아요.","入力を終了しますか?
|
||||||
|
終了すると、入力内容は保存されません。",,,,
|
||||||
|
,종료,終了,,,,
|
||||||
|
,계속 작성,続ける,,,,
|
||||||
|
,응원 삭제,ファンレターを削除,,,,
|
||||||
|
,"응원을 삭제할까요?
|
||||||
|
삭제 시 크리에이터의 답글도 같이 사라져요.","ファンレターを削除しますか?
|
||||||
|
削除すると、クリエイターからの返信も削除されます。",,,,
|
||||||
|
,오늘,今日,,,,
|
||||||
|
,지금,今,,,,
|
||||||
|
,지난,終了,,,,
|
||||||
|
,"아직 대화가 없어요.
|
||||||
|
크리에이터와 이야기를 시작해 보세요.","まだ会話がありません。
|
||||||
|
クリエイターと話してみましょう。",,,,
|
||||||
|
,"아직 나눈 AI 채팅이 없어요.
|
||||||
|
크리에이터와 대화를 시작해 보세요.","まだAIチャットがありません。
|
||||||
|
クリエイターと会話を始めてみましょう。",,,,
|
||||||
|
,"아직 나눈 대화가 없어요.
|
||||||
|
좋아하는 크리에이터에게 메세지를 보내보세요.","まだメッセージがありません。
|
||||||
|
好きなクリエイターにメッセージを送ってみましょう。",,,,
|
||||||
|
,크리에이터의 이름을 입력하세요,クリエイター名を入力,,,,
|
||||||
|
,대화 시작,トーク開始,,,,
|
||||||
|
,{{크리에이터이름}}님과 AI 채팅 혹은 DM을 시작해 볼까요?,{{クリエイター名}}とAIチャットまたはDMを始めてみませんか?,,,,
|
||||||
|
,AI 채팅하기,AIチャット,,,,
|
||||||
|
,채팅 설정,チャット設定,,,,
|
||||||
|
,이미지 보관함,ギャラリー,,,,
|
||||||
|
,배경 이미지 설정,背景設定,,,,
|
||||||
|
,채팅 초기화,チャットを初期化,,,,
|
||||||
|
,신규 크리에이터,新着クリエイター,,,,
|
||||||
|
,나의 크리에이터,マイクリエイター,,,,
|
||||||
|
,AI 캐릭터 채팅,AIキャラクターチャット,,,,
|
||||||
|
,팔로우 해제,フォロー解除,,,,
|
||||||
|
,"팔로우를 해제할까요?
|
||||||
|
{{크리에이터이름}}의 소식을 받을 수 없어요.","{{크리에이터이름}}からの最新情報を受け取れなくなります。
|
||||||
|
フォローを解除しますか?",,,,
|
||||||
|
,해제하기,解除する,,,,
|
||||||
|
,검색어를 입력하세요,キーワードを入力,,,,
|
||||||
|
,전체삭제,すべて削除,,,,
|
||||||
|
,최근 검색,最近の検索,,,,
|
||||||
|
,인기 검색어,急上昇ワード,,,,
|
||||||
|
,AI 채팅 인기 검색 키워드,AIチャット急上昇ワード,,,,
|
||||||
|
,시리즈 인기 검색 키워드,シリーズ急上昇ワード,,,,
|
||||||
|
,오디오 인기 검색 키워드,オーディオ急上昇ワード,,,,
|
||||||
|
,키워드,キーワード,,,,
|
||||||
|
,검색 결과,検索結果,,,,
|
||||||
|
,정확도순,関連度順,,,,
|
||||||
|
,많이 찾는 크리에이터,人気のクリエイター,,,,
|
||||||
|
,"관련 크리에이터가 없어요.
|
||||||
|
다른 검색어로 다시 찾아보세요.","該当するクリエイターがいません。
|
||||||
|
別のキーワードで検索してください。",,,,
|
||||||
|
,"관련 시리즈가 없어요.
|
||||||
|
다른 검색어로 다시 찾아보세요.","該当するシリーズがありません。
|
||||||
|
別のキーワードで検索してください。",,,,
|
||||||
|
,"관련 화보가 없어요.
|
||||||
|
다른 검색어로 다시 찾아보세요.","該当するフォトがありません。
|
||||||
|
別のキーワードで検索してください。",,,,
|
||||||
|
,"관련 키워드가 없어요.
|
||||||
|
다른 검색어로 다시 찾아보세요.","該当するキーワードがありません。
|
||||||
|
別のキーワードで検索してください。",,,,
|
||||||
|
,라이브 시작하기,ライブを開始,,,,
|
||||||
|
,오디오 올리기,オーディオを投稿,,,,
|
||||||
|
,커뮤니티 글 올리기,コミュニティ投稿,,,,
|
||||||
|
,이메일을 입력해 주세요,メールアドレスを入力してください,,,,
|
||||||
|
,이메일,メールアドレス,,,,
|
||||||
|
,이용약관,利用規約,,,,
|
||||||
|
,개인정보수집 및 이용동의,個人情報の収集・利用への同意,,,,
|
||||||
|
,올바른 이메일을 입력해 주세요,正しいメールアドレスを入力してください,,,,
|
||||||
|
,다음으로,次へ,,,,
|
||||||
|
,이미 가입된 이메일이에요,登録済みのメールアドレスです,,,,
|
||||||
|
,비밀번호를 입력해 주세요,パスワードを入力してください,,,,
|
||||||
|
,비밀번호,パスワード,,,,
|
||||||
|
,"8자 이상 (영문 대/소문자, 특수문자 포함)",8文字以上(英大文字・小文字・記号を含む),,,,
|
||||||
|
,완료,完了,,,,
|
||||||
|
,"8자 이상, 영문 대/소문자와 특수문자를 포함해 주세요",8文字以上、英大文字・小文字・記号を含めてください,,,,
|
||||||
|
,이메일로 로그인하기,メールアドレスでログイン,,,,
|
||||||
|
,로그인,ログイン,,,,
|
||||||
|
,가입되지 않은 이메일이에요,登録されていないメールアドレスです,,,,
|
||||||
|
,입력한 비밀번호를 다시 확인해 주세요,パスワードをもう一度確認してください,,,,
|
||||||
|
,인증코드를 입력해 주세요,認証コードを入力してください,,,,
|
||||||
|
,{email}로 인증코드를 보냈어요,{email}に認証コードを送信しました,,,,
|
||||||
|
,인증코드,認証コード,,,,
|
||||||
|
,0:00 후에 인증코드 다시 보내기,0:00後に認証コードを再送する,,,,
|
||||||
|
,인증 가능한 시간을 초과했어요,認証可能時間を超過しました,,,,
|
||||||
|
,인증코드 다시 보내기,認証コードを再送する,,,,
|
||||||
|
,올바른 인증코드를 입력해 주세요,正しい認証コードを入力してください,,,,
|
||||||
|
,새 비밀번호를 입력해 주세요,新しいパスワードを入力してください,,,,
|
||||||
|
,기존 비밀번호와 다르게 설정해 주세요,現在のパスワードとは異なるものを設定してください,,,,
|
||||||
|
,비밀번호 설정 완료!,パスワード設定完了!,,,,
|
||||||
|
,"비밀번호 설정을 완료했어요.
|
||||||
|
다시 로그인해 주세요.","パスワードを設定しました。
|
||||||
|
もう一度ログインしてください。",,,,
|
||||||
|
,재설정 종료,再設定を終了,,,,
|
||||||
|
,"지금 나가면 현재 진행중인 재설정이 종료되고,
|
||||||
|
처음부터 다시 진행해야 해요.","終了すると、進行中の再設定が終了し、
|
||||||
|
最初からやり直す必要があります。",,,,
|
||||||
|
,계속 재설정,再設定を続ける,,,,
|
||||||
|
,AI 채팅,AIチャット,,,,
|
||||||
|
,전체 크리에이터,すべてのクリエイター,,,,
|
||||||
|
,필수,必須,,,,
|
||||||
|
,가입한 이메일을 입력해 주세요,"登録メールアドレスを
|
||||||
|
入力してください",,,,
|
||||||
|
,존재하지 않는 이메일이에요,登録されていないメールアドレスです,,,,
|
||||||
|
,모두 팔로우하기,すべてフォロー,,,,
|
||||||
|
,대여중,レンタル中,,,,
|
||||||
|
,데뷔,デビュー,,,,
|
||||||
|
,라이브 총 진행 수,ライブ配信回数,,,,
|
||||||
|
,라이브 누적 진행 시간,ライブ累計配信時間,,,,
|
||||||
|
,라이브 누적 참여자,ライブ累計参加者数,,,,
|
||||||
|
,고정 해제,固定を解除,,,,
|
||||||
|
,수정하기,編集,,,,
|
||||||
|
,삭제하기,削除,,,,
|
||||||
|
,크리에이터에게만 보여요.,クリエイターにのみ表示されます。,,,,
|
||||||
|
,답글쓰기,返信する,,,,
|
||||||
|
,신고하기,通報する,,,,
|
||||||
|
,댓글을 입력하세요,コメントを入力,,,,
|
||||||
|
,크리에이터에게 응원의 메세지를 보내주세요!,クリエイターへファンレターを送りましょう!,,,,
|
||||||
|
,답글을 삭제할까요?,返信を削除しますか?,,,,
|
||||||
|
,답글 삭제,返信を削除,,,,
|
||||||
|
,크리에이터,クリエイター,,,,
|
||||||
|
,시리즈 자세히 보기,シリーズの詳細を見る,,,,
|
||||||
|
,소개,紹介,,,,
|
||||||
|
,장르,ジャンル,,,,
|
||||||
|
,연령 제한,年齢制限,,,,
|
||||||
|
,소장,購入,,,,
|
||||||
|
,페이지 수,ページ数,,,,
|
||||||
|
,발매일,発売日,,,,
|
||||||
|
,크리에이터의 다른 화보,このクリエイターの他のフォト,,,,
|
||||||
|
,{{화보이름 12자 ...}}와 비슷한 화보,「{{화보이름 12자 ...}}」に似たフォト,,,,
|
||||||
|
,[Premium Photo]의 다른 화보,「Premium Photo」の他のフォト,,,,
|
||||||
|
,소장하기,購入する,,,,
|
||||||
|
,스크랩,クリップ,,,,
|
||||||
|
,"마음에 드는 페이지를 스크랩하여
|
||||||
|
감상해 보세요!","お気に入りのページをクリップして
|
||||||
|
楽しみましょう!",,,,
|
||||||
|
,마지막 페이지,最後のページ,,,,
|
||||||
|
,"{{메거진 이름 12자 초과시...}}은 어떠셨나요?
|
||||||
|
후기를 댓글로 남겨보세요.","「{{雑誌名...}}」はいかがでしたか
|
||||||
|
感想をコメントでお聞かせください。",,,,
|
||||||
|
,댓글 쓰기,コメントを書く,,,,
|
||||||
|
,첫 페이지에요.,最初のページです。,,,,
|
||||||
|
,오래된순,古い順,,,,
|
||||||
|
,보관함,ライブラリ,,,,
|
||||||
|
,구매,購入,,,,
|
||||||
|
,재생목록,プレイリスト,,,,
|
||||||
|
,최근,最近,,,,
|
||||||
|
,제목순,タイトル順,,,,
|
||||||
|
,발매순,発売日順,,,,
|
||||||
|
,대여,レンタル,,,,
|
||||||
|
,업로드순,アップロード順,,,,
|
||||||
|
,"구매한 오디오가 없어요.
|
||||||
|
관심있는 오디오를 구매해 보세요!","購入したオーディオはありません。
|
||||||
|
気になるオーディオを購入してみましょう!",,,,
|
||||||
|
,"구매한 화보가 없어요.
|
||||||
|
관심있는 화보를 구매해 보세요!","購入したフォトはありません。
|
||||||
|
気になるフォトを購入してみましょう!",,,,
|
||||||
|
,"구매한 커뮤니티 글이 없어요.
|
||||||
|
좋아하는 크리에이터의 글을 구매해 보세요!","購入したコミュニティ投稿はありません。
|
||||||
|
お気に入りのクリエイターの投稿を購入してみましょう!",,,,
|
||||||
|
,나중에 들을 오디오,あとで聴く,,,,
|
||||||
|
,"좋아요한 오디오가 없어요.
|
||||||
|
마음에 드는 오디오에 좋아요를 남겨 보세요!","「いいね」したオーディオはありません。
|
||||||
|
お気に入りのオーディオに「いいね」してみましょう!",,,,
|
||||||
|
,"좋아요한 화보가 없어요.
|
||||||
|
마음에 드는 화보에 좋아요를 남겨 보세요!","「いいね」したフォトはありません。
|
||||||
|
お気に入りのフォトに「いいね」してみましょう!",,,,
|
||||||
|
,"좋아요한 커뮤니티 글이 없어요.
|
||||||
|
마음에 드는 글에 좋아요를 남겨 보세요!","「いいね」した投稿はありません。
|
||||||
|
お気に入りの投稿に「いいね」してみましょう!",,,,
|
||||||
|
,"최근 본 콘텐츠가 없어요.
|
||||||
|
새로운 콘텐츠를 만나보세요!","最近見たコンテンツはありません。
|
||||||
|
新しいコンテンツを見つけましょう!",,,,
|
||||||
|
,"최근 본 오디오가 없어요.
|
||||||
|
새로운 오디오를 만나보세요!","最近聴いたオーディオはありません。
|
||||||
|
新しいオーディオを見つけましょう!",,,,
|
||||||
|
,"최근 본 시리즈가 없어요.
|
||||||
|
새로운 시리즈를 만나보세요!","最近見たシリーズはありません。
|
||||||
|
しいシリーズを見つけましょう!",,,,
|
||||||
|
,"최근 본 화보가 없어요.
|
||||||
|
새로운 화보를 만나보세요!","最近見たフォトはありません。
|
||||||
|
新しいフォトを見つけましょう!",,,,
|
||||||
|
,충전하기,チャージ,,,,
|
||||||
|
,현재 보유한 캔,現在のカン,,,,
|
||||||
|
,사용할 캔,使用するカン,,,,
|
||||||
|
,사용할 포인트,使用するポイント,,,,
|
||||||
|
,*포인트 일부를 차감하여 대여합니다.,*ポイントを使用してレンタルします。,,,,
|
||||||
|
,캔이 부족해요! 충전 후 이용해 주세요.,カンが不足しています。チャージしてご利用ください。,,,,
|
||||||
|
,소장이 완료되었어요!,購入が完了しました!,,,,
|
||||||
|
,대여가 완료되었어요!,レンタルが完了しました!,,,,
|
||||||
|
,구매에 실패했어요. 다시 시도해 주세요.,購入に失敗しました。もう一度お試しください。,,,,
|
||||||
|
,오늘 하루 보지 않기,今日一日は表示しない,,,,
|
||||||
|
,닫기,閉じる,,,,
|
||||||
|
,콘텐츠 보호를 위해 캡처를 지원하지 않아요.,コンテンツ保護のため、スクリーンショットはご利用いただけません。,,,,
|
||||||
|
,답글을 입력하세요,返信を入力,,,,
|
||||||
|
,전체 인기,人気ランキング,,,,
|
||||||
|
,현재 보유,所持数,,,,
|
||||||
|
,활동,アクティビティ,,,,
|
||||||
|
,익명,匿名,,,,
|
||||||
|
,DM 확인하기,DMを確認,,,,
|
||||||
|
,예정,予定,,,,
|
||||||
|
,대화 찾기,トークを探す,,,,
|
||||||
|
,{요일}요 인기 시리즈,{요일}曜人気シリーズ,,,,
|
||||||
|
428
docs/20260811_일본어_string_resource_반영/plan-task.md
Normal file
428
docs/20260811_일본어_string_resource_반영/plan-task.md
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
# 일본어 string resource 반영 Goal 실행형 구현 계획
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 계획 작성 완료 |
|
||||||
|
| 작성일 | `2026-08-11` |
|
||||||
|
| 요구사항 기준 | `docs/20260811_일본어_string_resource_반영/prd.md` |
|
||||||
|
| 매핑표 | `docs/20260811_일본어_string_resource_반영/resource-mapping.md` |
|
||||||
|
| API 기준 | API 변경 없음 |
|
||||||
|
| 현재 Phase | 전체 완료 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
`kr_jp_string_resource.csv`의 일본어 문구를 resource key 단위로 안전하게 확정한 뒤 Android `values-ja/strings.xml`에 최소 반영한다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 완료 | `2/2` | 완료 | 없음 |
|
||||||
|
| 2 | 완료 | `1/1` | 완료 | 없음 |
|
||||||
|
|
||||||
|
- 동시에 하나의 미완료 goal만 운용한다.
|
||||||
|
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다.
|
||||||
|
- 후속 수정은 기존 완료 Task를 다시 열지 않고 신규 회귀 수정 Task로 추가한다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv` 기준 KR/JP 쌍 분석.
|
||||||
|
- `app/src/main/res/values/strings.xml`의 KR 값과 resource key 후보 대조.
|
||||||
|
- `app/src/main/res/values-ja/strings.xml`의 기존 JP 값과 CSV JP 값 차이 확인.
|
||||||
|
- `docs/20260811_일본어_string_resource_반영/resource-mapping.md` 생성과 상태 갱신.
|
||||||
|
- 확정된 key에 한해 `app/src/main/res/values-ja/strings.xml` 수정.
|
||||||
|
- Android resource XML 빌드 검증.
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- `app/src/main/res/values/strings.xml` 한국어 원문 수정.
|
||||||
|
- `app/src/main/res/values-en/strings.xml` 영어 리소스 수정.
|
||||||
|
- layout, Kotlin, API, navigation, ViewModel 변경.
|
||||||
|
- 신규 dependency 추가.
|
||||||
|
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷 QA.
|
||||||
|
- CSV JP 문구를 임의 재번역하는 작업. JP 오탈자 의심 항목은 CSV 값을 그대로 쓰고 최종 리포트에 확인 필요로 기록한다.
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- 기술 스택: Android Gradle 프로젝트, XML string resource.
|
||||||
|
- 리소스 파일: `app/src/main/res/values/strings.xml`, `app/src/main/res/values-ja/strings.xml`.
|
||||||
|
- 기준 CSV: `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv`.
|
||||||
|
- CSV에는 resource key가 없으므로 한국어 값이 정확히 일치하는 후보만 자동 매핑 대상으로 삼는다.
|
||||||
|
- `values/strings.xml`에 exact match 또는 Android placeholder/suffix 대응 한국어 리소스가 없으면 신규 key를 만들지 않고 `제외`한다.
|
||||||
|
- 같은 KR 문구가 여러 resource key에 있으면 모든 후보를 기록하고, 기존 JP가 CSV JP와 다르면 CSV JP를 우선 반영한다.
|
||||||
|
- JP 오탈자 의심 항목은 key가 있으면 CSV 값을 그대로 반영하고 최종 리포트에 기록한다. key가 없으면 반영하지 않는다.
|
||||||
|
- `{{...}}`, `{...}`, `n%` 치환 문구는 Android `%1$s`, `%1$d`, suffix resource와 대응되는 경우 후보로 포함한다.
|
||||||
|
- `resource-mapping.md`의 `<br>`은 표시용이며 `values-ja/strings.xml`에는 literal `<br>`이 아니라 `\n`으로 반영한다.
|
||||||
|
- `%1$s`, `%1$d`, `%%`, `\n`, XML escape, `@string/...` 참조를 깨지 않는다.
|
||||||
|
- 로직 변경이 없으므로 신규 unit test는 기본 작성하지 않는다.
|
||||||
|
- 검증은 `./gradlew :app:assembleDebug`를 기본으로 하고, 리소스 lint 확인이 필요하면 `./gradlew :app:lintDebug`를 추가한다.
|
||||||
|
|
||||||
|
## Task TDD 작성 규칙
|
||||||
|
|
||||||
|
이번 계획의 Phase 1은 read-only 대조와 문서 확정이므로 TDD 예외를 적용한다. Phase 2는 XML 리소스 수정 작업이며 로직 unit test 대상이 아니므로 RED/GREEN 테스트 대신 XML 빌드 검증을 완료 증거로 사용한다.
|
||||||
|
|
||||||
|
## Phase 1 매핑 확정
|
||||||
|
|
||||||
|
**Phase 결과:** CSV KR/JP 행이 resource key 후보, 반영 대상, 보류 대상, 제외 대상으로 분류된다.
|
||||||
|
|
||||||
|
**선행조건:** `prd.md`의 `DEC-001` 확정.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`, `P1-T2`, `P1-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 1.1 CSV와 한국어 resource 후보 대조
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** CSV KR 문구와 `values/strings.xml`의 한국어 resource key 후보를 대조한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `kr_jp_string_resource.csv`, `app/src/main/res/values/strings.xml` 존재.
|
||||||
|
- **완료 증거:** 후보 대조 결과가 `resource-mapping.md`에 기록되고, 중복 KR 문구가 별도 표시된다.
|
||||||
|
- **범위 밖:** `values-ja/strings.xml` 수정, CSV 문구 수정.
|
||||||
|
- **TDD 예외 사유:** production 동작을 변경하지 않는 read-only 대조 Task다.
|
||||||
|
- **대체 검증 방법:** CSV KR 값과 `values/strings.xml` string body를 대조하고, exact match 수와 중복/미매칭 예시를 Progress에 기록한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Read: `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv`
|
||||||
|
- Read: `app/src/main/res/values/strings.xml`
|
||||||
|
- Create: `docs/20260811_일본어_string_resource_반영/resource-mapping.md`
|
||||||
|
- Modify: `docs/20260811_일본어_string_resource_반영/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: CSV `KR`, `JP` 컬럼과 Android `<string name="...">...</string>` 값.
|
||||||
|
- Produces: `P1-T2`가 사용할 `resource-mapping.md`의 KR 문구별 resource key 후보 목록.
|
||||||
|
|
||||||
|
- [x] CSV의 `KR`, `JP` 행을 파싱한다.
|
||||||
|
- [x] `values/strings.xml`의 `<string>` 값을 key와 함께 추출한다.
|
||||||
|
- [x] CSV KR과 한국어 resource 값을 exact match로 대조한다.
|
||||||
|
- [x] 같은 KR 문구가 여러 key에 매칭되는 항목을 중복 후보로 표시한다.
|
||||||
|
- [x] 매칭되지 않은 CSV KR 행은 신규 key 추정 없이 미매칭 후보로 기록한다.
|
||||||
|
- [x] `resource-mapping.md`를 생성하고 `CSV 행`, `KR`, `JP`, `resource key 후보`, `기존 JA`, `상태`, `비고` 컬럼으로 후보 목록을 기록한다.
|
||||||
|
- [x] Progress에 exact match 수, 중복 후보 수, 미매칭 후보 수와 대표 예시를 남긴다.
|
||||||
|
|
||||||
|
#### Task 1.2 일본어 기존값 충돌과 보류 항목 확정
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** `values-ja/strings.xml`의 기존 JP 값과 CSV JP 값을 대조해 반영/보류/제외 결정을 확정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료.
|
||||||
|
- **완료 증거:** 확정 반영 목록과 보류 목록이 `resource-mapping.md`에 기록되고, 요약이 `plan-task.md` Progress 또는 Decision Log에 남는다.
|
||||||
|
- **범위 밖:** 확정되지 않은 key 반영, 일본어 재번역.
|
||||||
|
- **TDD 예외 사유:** production 동작을 변경하지 않는 read-only 대조와 의사결정 Task다.
|
||||||
|
- **대체 검증 방법:** 기존 JP 값, CSV JP 값, placeholder 차이, 줄바꿈 차이, 오탈자 의심 항목을 대조한다.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Read: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Modify: `docs/20260811_일본어_string_resource_반영/resource-mapping.md`
|
||||||
|
- Modify: `docs/20260811_일본어_string_resource_반영/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `P1-T1`이 만든 `resource-mapping.md`의 resource key 후보 목록.
|
||||||
|
- Produces: `P2-T1`이 수정할 확정 key/value 목록.
|
||||||
|
|
||||||
|
- [x] 후보 key마다 기존 `values-ja/strings.xml` 값을 확인한다.
|
||||||
|
- [x] CSV JP와 기존 JP가 같은 항목은 변경 없음으로 분류한다.
|
||||||
|
- [x] CSV JP와 기존 JP가 다른 항목은 변경 후보로 분류한다.
|
||||||
|
- [x] KR placeholder와 JP placeholder가 다른 항목은 보류한다.
|
||||||
|
- [x] CSV JP 오탈자 의심 항목은 key가 있으면 그대로 반영하고, key가 없으면 제외한 뒤 Decision Log에 사용자 확인 필요로 남긴다.
|
||||||
|
- [x] `@string/...` 참조는 참조를 풀어야 하는 명확한 근거가 없으면 보류한다.
|
||||||
|
- [x] 반영 대상 key, 보류 대상 key, 제외 대상 key를 `resource-mapping.md`에 기록하고 요약을 Progress에 남긴다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] `P1-T1`, `P1-T2`의 체크박스와 완료 증거가 모두 충족됐다.
|
||||||
|
- [x] `resource-mapping.md`에 확정 반영 대상 key 목록이 존재한다.
|
||||||
|
- [x] 보류 항목은 이유와 후속 결정 주체가 기록됐다.
|
||||||
|
- [x] 확정되지 않은 항목을 Phase 2에 넘기지 않았다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** 매핑 결과가 PRD 정책과 충돌하지 않는지 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1`, `P1-T2` 완료.
|
||||||
|
- **완료 증거:** 아래 대조 기준 통과와 Progress 기록.
|
||||||
|
- **범위 밖:** Gate 통과를 위한 임의 번역, CSV 수정, resource 수정.
|
||||||
|
|
||||||
|
검증 기준:
|
||||||
|
|
||||||
|
- [x] 확정 반영 목록의 모든 key는 `values/strings.xml`에 존재한다.
|
||||||
|
- [x] 확정 반영 목록의 모든 key는 `values-ja/strings.xml`에 존재하거나 신규 추가 필요로 명시됐다.
|
||||||
|
- [x] 확정 반영 목록의 JP 값은 CSV JP에서 온 값이다.
|
||||||
|
- [x] `resource-mapping.md`의 모든 행은 `반영`, `보류`, `제외`, `변경 없음` 중 하나의 상태를 가진다.
|
||||||
|
- [x] placeholder와 줄바꿈 형식이 Android XML에 맞게 표현될 수 있다.
|
||||||
|
- [x] 보류 항목은 Phase 2 수정 목록에서 제외됐다.
|
||||||
|
|
||||||
|
## Phase 2 일본어 resource 반영
|
||||||
|
|
||||||
|
**Phase 결과:** 확정된 key만 `values-ja/strings.xml`에 반영되고 Android resource 빌드가 통과한다.
|
||||||
|
|
||||||
|
**선행조건:** `P1-GATE` 완료.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P2-T1`, `P2-GATE` 완료, 검증 기록 누적.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 2.1 확정 key를 values-ja에 반영
|
||||||
|
|
||||||
|
**Goal 실행 `P2-T1`:** Phase 1에서 확정된 key만 `app/src/main/res/values-ja/strings.xml`에 반영한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-GATE` 완료와 확정 반영 key 목록 존재.
|
||||||
|
- **완료 증거:** `values-ja/strings.xml` diff가 확정 key에만 한정되고 XML 빌드 검증이 통과한다.
|
||||||
|
- **범위 밖:** 한국어/영어 리소스 수정, 확정되지 않은 보류 항목 반영, 레이아웃/코드 수정.
|
||||||
|
- **TDD 예외 사유:** Android XML string resource 수정이며 로직 unit test 대상이 아니다.
|
||||||
|
- **대체 검증 방법:** resource XML diff 검토, placeholder 대조, Gradle assemble/lint 명령 실행.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- Reference: `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv`
|
||||||
|
- Reference: `docs/20260811_일본어_string_resource_반영/resource-mapping.md`
|
||||||
|
- Modify: `docs/20260811_일본어_string_resource_반영/plan-task.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `P1-GATE`에서 승인된 `resource-mapping.md`의 `반영` 상태 key/value 목록.
|
||||||
|
- Produces: Android 일본어 locale에서 사용할 확정 JP string resource.
|
||||||
|
|
||||||
|
- [x] **RED 대체 확인:** 수정 전 `./gradlew :app:assembleDebug`를 실행해 baseline resource 상태를 확인한다.
|
||||||
|
- [x] **GREEN:** 확정 key만 `values-ja/strings.xml`에서 CSV JP 값으로 수정한다.
|
||||||
|
- [x] **GREEN 확인:** `./gradlew :app:assembleDebug`를 실행해 Android resource 빌드가 통과하는지 확인한다.
|
||||||
|
- [x] **REFACTOR:** 이번 Task가 만든 XML escape, placeholder, 줄바꿈 문제만 정리하고 `./gradlew :app:assembleDebug`를 다시 실행한다.
|
||||||
|
- [x] 필요 시 `./gradlew :app:lintDebug`를 실행하고 결과를 Progress에 기록한다.
|
||||||
|
|
||||||
|
### 완료 조건
|
||||||
|
|
||||||
|
- [x] `values-ja/strings.xml` diff가 확정 key에만 한정됐다.
|
||||||
|
- [x] `%1$s`, `%1$d`, `%%`, `\n`, XML escape가 깨지지 않았다.
|
||||||
|
- [x] 보류 항목은 반영되지 않았다.
|
||||||
|
- [x] Gradle resource 빌드 검증 결과가 Progress에 기록됐다.
|
||||||
|
|
||||||
|
### 검증 방법
|
||||||
|
|
||||||
|
#### Phase 2 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P2-GATE`:** 일본어 resource 반영 결과와 회귀 위험을 최종 판정한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P2-T1` 완료.
|
||||||
|
- **완료 증거:** 아래 명령과 Expected 통과, Progress에 실제 결과 누적.
|
||||||
|
- **범위 밖:** Gate 실패와 무관한 기능 수정, UI/계측 테스트 실행.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** exit code 0, Android resource XML 파싱 성공, debug APK assemble 성공. 2026-08-11 최종 실행 결과 `BUILD SUCCESSFUL in 10s`.
|
||||||
|
|
||||||
|
필요 시 추가 검증:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:lintDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** exit code 0 또는 이번 resource 변경과 무관한 기존 lint 이슈만 보고. 2026-08-11에는 XML string resource 변경이며 `assembleDebug`와 XML parse가 통과해 실행하지 않았다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | 없음 | 아니요 | CSV 또는 한국어 resource 형식 문제를 Progress에 기록 |
|
||||||
|
| 2 | `P1-T2` | `P1-T1` | 아니요 | 보류 항목을 사용자 확인 대상으로 분리 |
|
||||||
|
| 3 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 확정되지 않은 항목을 Phase 2에서 제외 |
|
||||||
|
| 4 | `P2-T1` | `P1-GATE` | 아니요 | 실패 key를 보류로 되돌리고 Decision Log에 기록 |
|
||||||
|
| 5 | `P2-GATE` | `P2-T1` | 아니요 | 실패 원인 key만 회귀 수정 Task로 분리 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 -> P1-T2 -> P1-GATE -> P2-T1 -> P2-GATE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- 확정되지 않은 CSV 행을 resource key에 임의 매핑하지 않는다.
|
||||||
|
- 같은 KR 문구가 여러 key에 있을 때 전체를 자동 덮어쓰지 않는다.
|
||||||
|
- CSV JP 문구를 임의 재번역하지 않는다.
|
||||||
|
- `values/strings.xml`, `values-en/strings.xml`, layout, Kotlin 파일을 수정하지 않는다.
|
||||||
|
- 신규 dependency를 추가하지 않는다.
|
||||||
|
- test를 삭제, skip, 완화하지 않는다.
|
||||||
|
- 기기·에뮬레이터 UI 조작, 스크린샷 QA, `connectedDebugAndroidTest`를 실행하지 않는다.
|
||||||
|
|
||||||
|
## 의사결정 및 중단 규칙
|
||||||
|
|
||||||
|
- PRD와 plan-task가 충돌하면 PRD Decision Log를 우선하고 plan-task를 먼저 갱신한다.
|
||||||
|
- CSV JP에 오탈자 의심이 있으면 임의 수정하지 않는다. key가 있으면 그대로 반영하고 최종 리포트에 기록하며, key가 없으면 제외한다.
|
||||||
|
- placeholder가 맞지 않으면 해당 key는 보류하고 사용자 확인 대상으로 남긴다.
|
||||||
|
- `values-ja/strings.xml`에 key가 없으면 신규 추가 여부를 사용자에게 확인한다.
|
||||||
|
- Gradle 검증 실패가 발생하면 실패한 resource key만 확인하고 관련 없는 기능 수정은 하지 않는다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
|
||||||
|
|
||||||
|
### 문서 작성 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: PRD와 계획문서 초안을 작성하고, 매핑표 산출물을 `resource-mapping.md` 별도 파일로 만들도록 보정했다.
|
||||||
|
- 왜: 사용자가 CSV 기반 일본어 string resource 반영 전에 모호한 사항을 확인하고 PRD/계획문서만 생성하라고 요청했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `docs/sample/sample-prd.md` 확인 — PRD 구조 확인.
|
||||||
|
- `docs/sample/sample-plan-task.md` 확인 — plan-task 구조 확인.
|
||||||
|
- `docs/agent-guides/work-plan-docs.md` 확인 — 문서 위치와 작성 규칙 확인.
|
||||||
|
- `docs/agent-guides/build-test-style.md` 확인 — Android Gradle 검증 명령 확인.
|
||||||
|
- `app/src/main/res/values/strings.xml`, `app/src/main/res/values-ja/strings.xml` 일부 확인 — CSV에 key가 없고 동일 KR 문구가 반복되는 위험 확인.
|
||||||
|
- 남은 항목: 실제 매핑 확정과 resource 반영은 아직 수행하지 않음.
|
||||||
|
- 다음 행동: 사용자가 구현을 요청하면 `P1-T1`부터 실행한다.
|
||||||
|
|
||||||
|
### 문서 보정 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 매핑표 저장 위치를 `docs/20260811_일본어_string_resource_반영/resource-mapping.md`로 명시했다.
|
||||||
|
- 왜: 매핑 행이 많아 `plan-task.md` Progress에만 남기면 검토와 승인 추적이 어렵다.
|
||||||
|
- 어떻게:
|
||||||
|
- `prd.md` — 매핑표 경로, 처리 흐름, 요구사항 수용 기준, Decision Log에 `resource-mapping.md`를 반영.
|
||||||
|
- `plan-task.md` — Task 1.1에서 `resource-mapping.md`를 생성하고 Task 1.2에서 상태를 갱신하도록 Files, 완료 증거, 체크박스, Gate를 보정.
|
||||||
|
- 남은 항목: 실제 `resource-mapping.md` 생성과 매핑 행 작성은 `P1-T1` 실행 시 수행.
|
||||||
|
- 다음 행동: 사용자가 구현을 요청하면 `P1-T1`부터 실행한다.
|
||||||
|
|
||||||
|
### P1-T1 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: CSV KR/JP와 `values/strings.xml`의 한국어 resource 값을 exact match로 대조해 `resource-mapping.md` 1차 후보 목록을 생성했다.
|
||||||
|
- 왜: CSV에 Android resource key가 없어 일본어 반영 전에 안전한 key 후보를 확정해야 한다.
|
||||||
|
- 어떻게:
|
||||||
|
- `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv` — Python `csv` 모듈로 multiline CSV를 파싱했다.
|
||||||
|
- `app/src/main/res/values/strings.xml` — XML `<string>` 값을 key와 함께 추출했다.
|
||||||
|
- `docs/20260811_일본어_string_resource_반영/resource-mapping.md` — exact 후보 231행, 중복 KR 후보 164행, 미매칭 후보 130행을 기록했다.
|
||||||
|
- 남은 항목: `values-ja/strings.xml` 기존값 대조와 `반영/보류/제외/변경 없음` 상태 확정.
|
||||||
|
- 다음 행동: `P1-T2`에서 기존 JA 값과 CSV JP 값을 대조한다.
|
||||||
|
|
||||||
|
### P1-T2 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `resource-mapping.md` 후보를 `values-ja/strings.xml` 기존값과 대조해 상태를 확정했다.
|
||||||
|
- 왜: 확정되지 않은 중복·placeholder·alias·오탈자 의심 항목을 일본어 리소스에 반영하지 않기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `app/src/main/res/values-ja/strings.xml` — 후보 key의 기존 JA 값을 추출했다.
|
||||||
|
- `resource-mapping.md` — 최초 기준으로 `반영` 33행, `변경 없음` 34행, `보류` 294행으로 갱신했다.
|
||||||
|
- 백그라운드 탐색 결과 — 중복 KR, alias, placeholder, multiline/escape, JP 이상 사례가 현재 보류 규칙과 일치함을 확인했다.
|
||||||
|
- 남은 항목: `P1-GATE`에서 모든 행의 상태와 Phase 2 대상 제한을 확인한다.
|
||||||
|
- 다음 행동: `P1-GATE`를 실행한다.
|
||||||
|
|
||||||
|
### P1-T2 2차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 사용자 추가 지시에 따라 한국어 리소스가 없는 행을 `제외`로 확정하고, JP 오탈자 의심 항목 처리 기준을 바꿨다.
|
||||||
|
- 왜: 한국어 리소스가 없는 CSV 행은 신규 key를 만들지 않고 반영 대상에서 제외해야 하며, JP 오탈자 의심은 임의 보정하지 않고 확인 가능하게 남겨야 한다.
|
||||||
|
- 어떻게:
|
||||||
|
- `resource-mapping.md` — 추가 지시 전 기준으로 `반영` 33행, `변경 없음` 34행, `보류` 164행, `제외` 130행으로 갱신했다.
|
||||||
|
- `최근 본 시리즈가 없어요... / しいシリーズを見つけましょう!` — JP 오탈자 의심이지만 한국어 resource exact match가 없어 `제외`로 유지하고 최종 리포트 확인 대상으로 남긴다.
|
||||||
|
- 남은 항목: Phase 2에서 `반영` 상태 33개 key만 적용한다.
|
||||||
|
- 다음 행동: 이미 적용한 `values-ja/strings.xml` 33개 key가 새 기준과 일치하는지 재검증한다.
|
||||||
|
|
||||||
|
### P1-T2 3차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `{{...}}`, `{...}`, `n%` 치환 후보와 중복 KR CSV JP 우선 정책을 반영해 매핑표를 재계산했다.
|
||||||
|
- 왜: 사용자가 placeholder 형태 문구도 앱 내 변경 가능 리소스로 고려하고, 중복 KR의 기존 JP가 다르면 CSV JP를 우선하라고 지시했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `resource-mapping.md` — 최초 추가 지시 기준으로 `반영` 34행, `변경 없음` 200행, `제외` 127행으로 갱신했다.
|
||||||
|
- placeholder/suffix 후보 — `{{장르}}의 크리에이터`는 `home_recommendation_section_genre_creator_suffix`, `{{크리에이터 이름}}...랭킹...`은 `screen_home_following_creator_ranking_news_message`, `전체 오디오의 n%...`은 `creator_channel_audio_owned_rate_message`로 대응했다.
|
||||||
|
- 중복 KR 후보 — 기존 JP가 CSV JP와 다른 key는 `반영`으로 분류했다.
|
||||||
|
- `<br>` — 매핑표 표시용으로만 사용하고 XML 적용 값은 `\n`으로 유지했다.
|
||||||
|
- 남은 항목: 새 기준에 따라 `values-ja/strings.xml`을 재검증한다.
|
||||||
|
- 다음 행동: `P2-T1` 재검증과 Gate를 다시 실행한다.
|
||||||
|
|
||||||
|
### P1-GATE 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `resource-mapping.md`의 상태와 확정 반영 key의 KR/JA 리소스 존재 여부를 검증했다.
|
||||||
|
- 왜: Phase 2에서 보류 항목을 실수로 반영하지 않도록 반영 대상을 33개 key로 제한하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `resource-mapping.md` — 전체 361행이 `반영`, `보류`, `제외`, `변경 없음` 중 허용 상태인지 확인했다.
|
||||||
|
- `app/src/main/res/values/strings.xml`, `app/src/main/res/values-ja/strings.xml` — `반영` 33개 key가 양쪽 리소스에 존재함을 확인했다.
|
||||||
|
- Gate 결과 — 최초 기준 `rows=361 apply=33 hold=294 unchanged=34`, 추가 지시 반영 후 `apply=33 hold=164 unchanged=34 excluded=130`.
|
||||||
|
- 남은 항목: `P2-T1`에서 `반영` 상태 33개 key만 `values-ja/strings.xml`에 적용한다.
|
||||||
|
- 다음 행동: `P2-T1`을 실행한다.
|
||||||
|
|
||||||
|
### P2-T1 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: `resource-mapping.md`의 `반영` 상태 33개 key만 `app/src/main/res/values-ja/strings.xml`에 CSV JP 값으로 반영했다.
|
||||||
|
- 왜: 확정 key에 한정해 일본어 리소스를 갱신하고, 보류/제외 항목의 오반영을 막기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:assembleDebug` — 수정 전 baseline 성공, `BUILD SUCCESSFUL in 45s`.
|
||||||
|
- `app/src/main/res/values-ja/strings.xml` — 최종 기준 확정 34개 key를 CSV JP 또는 Android placeholder/suffix 변환값으로 수정했다.
|
||||||
|
- `./gradlew :app:assembleDebug` — 수정 후 GREEN 확인 성공, `BUILD SUCCESSFUL in 11s`.
|
||||||
|
- `resource-mapping.md` 대조 — 추가 지시 전 `반영=33 변경 없음=34 보류=164 제외=130`, 이후 최종 `반영=34 변경 없음=200 제외=127`.
|
||||||
|
- `./gradlew :app:assembleDebug` — REFACTOR 대체 확인 성공, `BUILD SUCCESSFUL in 2s`.
|
||||||
|
- `./gradlew :app:lintDebug` — 실행하지 않음. 이번 변경은 XML string resource 치환이고 `assembleDebug`가 resource XML 파싱과 packaging을 검증했으며, 추가 lint 실행이 새 증거를 만들 필요가 없다고 판단했다.
|
||||||
|
- 남은 항목: `P2-GATE`에서 diff 범위와 최종 빌드 상태를 확인한다.
|
||||||
|
- 다음 행동: `P2-GATE`를 실행한다.
|
||||||
|
|
||||||
|
### P2-GATE 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: 일본어 resource 반영 결과와 회귀 범위를 최종 검증했다.
|
||||||
|
- 왜: 확정 key만 반영됐고 한국어/영어 리소스나 코드가 수정되지 않았음을 확인하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew :app:assembleDebug` — 리뷰 블로커 수정 후 최종 성공, `BUILD SUCCESSFUL in 10s`.
|
||||||
|
- `python3` XML parse — `app/src/main/res/values-ja/strings.xml` 파싱 성공, `xml_parse_ok escaped_newlines_ok no_literal_br`.
|
||||||
|
- `git diff -- app/src/main/res/values/strings.xml app/src/main/res/values-en/strings.xml` — 출력 없음, 한국어/영어 리소스 변경 없음.
|
||||||
|
- `git diff --name-only` — 변경 범위는 `app/src/main/res/values-ja/strings.xml`, `prd.md`, `plan-task.md`, `resource-mapping.md`로 확인했다. 추가 지시 반영 후에도 동일하다.
|
||||||
|
- `resource-mapping.md` 최종 상태 — `반영=67 변경 없음=167 제외=127`, 모든 `반영` 값이 `values-ja`와 일치함.
|
||||||
|
- 남은 항목: reviewer gate와 최종 보고.
|
||||||
|
- 다음 행동: 변경 범위 리뷰를 요청한다.
|
||||||
|
|
||||||
|
### 리뷰 블로커 수정 1차 실행 — 2026-08-11
|
||||||
|
|
||||||
|
- 상태: 완료
|
||||||
|
- 무엇을: reviewer가 지적한 매핑 상태 불일치와 XML 물리 개행 문제를 수정했다.
|
||||||
|
- 왜: HEAD 기준 실제 diff와 `resource-mapping.md` 상태가 일치해야 하고, `<br>` 대응 값은 XML에서 물리 개행이 아니라 `\n` escape로 저장되어야 한다.
|
||||||
|
- 어떻게:
|
||||||
|
- `resource-mapping.md` — HEAD 기준으로 재분류해 `반영` 67행, `변경 없음` 167행, `제외` 127행으로 갱신했다.
|
||||||
|
- `app/src/main/res/values-ja/strings.xml` — string body 내부 물리 개행을 `\n` escape로 정규화했다.
|
||||||
|
- 검증 — `xml_parse_ok escaped_newlines_ok no_literal_br`, `apply_values_match`, `./gradlew :app:assembleDebug` 최종 `BUILD SUCCESSFUL in 10s`.
|
||||||
|
- 남은 항목: 재리뷰 통과와 최종 보고.
|
||||||
|
- 다음 행동: reviewer gate를 다시 실행한다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-11 | `DEC-001` | 확정 | CSV KR/JP 쌍은 resource key 매핑표를 먼저 확정한 뒤 반영한다. | CSV에는 `string name`이 없고 중복 KR 문구가 존재한다. | `P1-T1`, `P1-T2`, `P2-T1` |
|
||||||
|
| 2026-08-11 | `DEC-002` | 확정 | 이번 실행에서는 `prd.md`, `plan-task.md`만 생성하고 resource XML은 수정하지 않는다. | 사용자 요청 범위가 문서 생성으로 한정됐다. | 전체 |
|
||||||
|
| 2026-08-11 | `DEC-003` | 확정 | 매핑표는 `docs/20260811_일본어_string_resource_반영/resource-mapping.md` 별도 파일로 생성한다. | 검토 대상 행이 많고 반영/보류/제외 상태 추적이 필요하다. | `P1-T1`, `P1-T2`, `P1-GATE` |
|
||||||
|
| 2026-08-11 | `DEC-004` | 확정 | 한국어 resource exact match가 없는 CSV 행은 `제외`한다. | 사용자 추가 지시. | `P1-T2`, `P1-GATE` |
|
||||||
|
| 2026-08-11 | `DEC-005` | 확정 | JP 오탈자 의심 항목은 key가 있으면 CSV 값을 그대로 반영하고 최종 리포트에 기록한다. key가 없으면 제외한다. | 사용자 추가 지시와 “한국어 리소스가 없는 것은 반영하지 않는다” 조건. | `P2-T1`, 최종 보고 |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `ISSUE-001` | Medium | 후보 | CSV에 resource key가 없어 동일 KR 문구를 자동 매핑하면 오반영 위험이 있다. | `P1-T1` | exact match 후보와 중복 후보를 분리한다. |
|
||||||
|
| `ISSUE-002` | Low | 후보 | 기존 `values-ja/strings.xml`에 `フォロー를解除`처럼 혼합 언어 의심 문구가 있다. | `P1-T2` | CSV 매핑 후보와 일치하면 변경 후보로 분류한다. |
|
||||||
|
| `ISSUE-003` | Low | 후보 | CSV JP에 `しいシリーズを見つけましょう!`처럼 오탈자 의심 문구가 있다. | `P1-T2` | key가 있으면 그대로 반영하고 최종 리포트에 기록한다. 현재 해당 CSV 행은 한국어 resource exact match가 없어 제외됐다. |
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- 2026-08-11 문서 생성 검증: `prd.md`, `plan-task.md`를 CSV 파일이 있는 `docs/20260811_일본어_string_resource_반영/` 폴더에 생성했다. 코드와 resource XML은 수정하지 않았다.
|
||||||
|
- 2026-08-11 문서 보정 검증: 매핑표 저장 위치를 `resource-mapping.md`로 명시했다. 실제 매핑표 파일은 `P1-T1` 실행 시 생성한다.
|
||||||
|
- 2026-08-11 통합 검증: `resource-mapping.md` 최종 기준 `반영` 67개 key만 `values-ja/strings.xml`에 적용했고, `./gradlew :app:assembleDebug`가 최종 `BUILD SUCCESSFUL in 10s`로 통과했다. 한국어/영어 리소스 diff는 없다.
|
||||||
|
|
||||||
|
## 최종 보고 형식
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
구현 결과: Phase 1 매핑 확정과 Phase 2 일본어 resource 반영 완료
|
||||||
|
|
||||||
|
- 변경: `app/src/main/res/values-ja/strings.xml`의 확정 resource key만 CSV JP 값 또는 Android placeholder/suffix 변환값으로 수정
|
||||||
|
- 결정: `DEC-001` 매핑표 먼저 확정 후 확정 key만 반영
|
||||||
|
- 검증:
|
||||||
|
- `./gradlew :app:assembleDebug` — 성공 또는 실패와 핵심 원인 기록
|
||||||
|
- 남은 항목: 보류된 문구와 사용자 확인 필요 항목 기록
|
||||||
|
- 문서: `docs/20260811_일본어_string_resource_반영/prd.md`, `docs/20260811_일본어_string_resource_반영/plan-task.md`, `docs/20260811_일본어_string_resource_반영/resource-mapping.md`
|
||||||
|
```
|
||||||
172
docs/20260811_일본어_string_resource_반영/prd.md
Normal file
172
docs/20260811_일본어_string_resource_반영/prd.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# 일본어 string resource 반영 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 기준 확정 |
|
||||||
|
| 작성일 | `2026-08-11` |
|
||||||
|
| 최종 수정일 | `2026-08-11` |
|
||||||
|
| 대상 제품 | SodaLive Android 일본어 string resource |
|
||||||
|
| 작성자·결정권자 | 사용자, Sisyphus |
|
||||||
|
| 기준 CSV | `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv` |
|
||||||
|
| 매핑표 | `docs/20260811_일본어_string_resource_반영/resource-mapping.md` |
|
||||||
|
| 관련 구현 계획 | `docs/20260811_일본어_string_resource_반영/plan-task.md` |
|
||||||
|
| 관련 review | 없음 |
|
||||||
|
|
||||||
|
### 요구사항 상태
|
||||||
|
|
||||||
|
| 상태 | 의미 | 구현 처리 |
|
||||||
|
|---|---|---|
|
||||||
|
| 확정 | CSV와 사용자 결정으로 구현 기준이 정해짐 | `plan-task.md`의 Task와 완료 증거로 추적 |
|
||||||
|
| 미결 | 제품 문구 또는 매핑 판단이 더 필요함 | 임의 반영하지 않고 매핑 확정 단계에서 확인 |
|
||||||
|
| 제외 | 이번 작업에서 반영하지 않음 | Decision Log에 제외 이유 기록 |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
`kr_jp_string_resource.csv`의 KR/JP 쌍을 기준으로 Android 일본어 리소스 `app/src/main/res/values-ja/strings.xml` 반영 범위를 확정한다. CSV에는 Android `string name` 키가 없으므로, 한국어 원문과 `app/src/main/res/values/strings.xml` 값을 대조한 결과를 `resource-mapping.md`에 저장하고 resource key 매핑을 먼저 확정한 뒤 일본어 값을 반영한다. `{{...}}`, `{...}`, `n%`처럼 앱에서 치환되는 문구는 Android placeholder 또는 suffix resource와 대응되는 경우 후보로 포함한다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 일본어 리소스에는 CSV 기준 문구와 다른 번역, 미반영 번역, 오탈자 의심 항목이 섞여 있을 수 있다.
|
||||||
|
|
||||||
|
- CSV는 `KR`, `JP` 컬럼만 제공하고 Android resource key를 제공하지 않는다.
|
||||||
|
- `홈`, `콘텐츠`, `전체`, `라이브`, `댓글`, `삭제`처럼 같은 KR 문구가 여러 resource key에 반복된다.
|
||||||
|
- 기존 `values-ja/strings.xml` 값과 CSV 값이 다를 때 무조건 덮어쓰면 의도하지 않은 화면 문구가 바뀔 수 있다.
|
||||||
|
|
||||||
|
문제를 해결했다는 판단은 확정된 KR 문구/resource key 매핑에 한해 `values-ja/strings.xml`이 CSV JP 값을 반영하고, XML 리소스 빌드 검증이 통과하는 것으로 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- CSV 기준 일본어 문구를 Android 일본어 리소스에 안전하게 반영한다.
|
||||||
|
- 중복 KR 문구는 resource key 단위로 매핑을 확정한 뒤 반영해 오반영을 막는다.
|
||||||
|
- 반영 대상, 제외 대상, 확인 필요 대상을 문서에 남겨 후속 검토가 가능하게 한다.
|
||||||
|
|
||||||
|
### 3.2 품질 목표
|
||||||
|
|
||||||
|
- `values-ja/strings.xml`의 XML escape, placeholder, 줄바꿈 형식을 Android resource 규칙에 맞게 유지한다.
|
||||||
|
- `%1$s`, `%1$d`, `%%`, `\n`, `@string/...` 참조를 깨지 않는다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않았으므로 UI/계측 테스트, 기기 조작, 스크린샷 QA는 수행하지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- `app/src/main/res/values/strings.xml`의 한국어 원문 수정.
|
||||||
|
- `app/src/main/res/values-en/strings.xml` 수정.
|
||||||
|
- 레이아웃, 코드, 화면 흐름, API 계약 수정.
|
||||||
|
- CSV 자체 번역 품질을 임의로 재번역하거나 새 일본어 문구를 창작하는 작업.
|
||||||
|
- Android UI/계측 테스트 추가 또는 `connectedDebugAndroidTest` 실행.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 일본어 사용자 | 앱 화면에서 자연스럽고 CSV 기준과 일치하는 일본어 문구 확인 | 일본어 locale로 앱 사용 | Android |
|
||||||
|
| Android 개발자 | 확정된 매핑만 안전하게 리소스에 반영 | resource key 대조, XML 수정, 로컬 검증 | Android Gradle 프로젝트 |
|
||||||
|
|
||||||
|
권한 정책 변경은 없다. 인증, API, 데이터 소유권, 민감정보 저장 정책은 이번 범위 밖이다.
|
||||||
|
|
||||||
|
## 6. 핵심 처리 흐름
|
||||||
|
|
||||||
|
1. `kr_jp_string_resource.csv`의 KR/JP 행을 읽는다.
|
||||||
|
2. `app/src/main/res/values/strings.xml`에서 KR 문구와 일치하는 resource key 후보를 찾는다.
|
||||||
|
3. 같은 KR 문구가 여러 key에 매칭되면 모두 `resource-mapping.md`에 후보로 기록하고, 기존 JP가 CSV JP와 다르면 CSV JP를 우선 반영한다.
|
||||||
|
4. `app/src/main/res/values-ja/strings.xml`의 기존 JP 값을 확인한다.
|
||||||
|
5. 확정된 key만 CSV JP 값으로 수정한다.
|
||||||
|
6. XML escape, placeholder, 줄바꿈, 빌드 리소스 검증을 실행한다.
|
||||||
|
|
||||||
|
## 7. 기능 요구사항
|
||||||
|
|
||||||
|
### 7.1 매핑 확정
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `I18N-MAP-001` | 확정 | CSV 행은 resource key 없이 제공된 KR/JP 쌍으로 취급한다. | key 추정 없이 `values/strings.xml` 값과 일치하는 후보만 매핑 대상으로 삼는다. | `P1-T1` |
|
||||||
|
| `I18N-MAP-002` | 확정 | 동일 KR 문구가 여러 resource key에 있으면 모든 후보를 기록하고 CSV JP를 우선한다. | 중복 후보 중 기존 JP가 CSV JP와 다르면 해당 key에 CSV JP를 반영한다. | `P1-T1`, `P1-T2`, `P2-T1` |
|
||||||
|
| `I18N-MAP-003` | 확정 | CSV JP와 기존 JP가 다른 항목은 변경 후보로 분리한다. | 변경 전 기존 값, CSV 값, 반영 결정이 `resource-mapping.md`에 기록된다. | `P1-T2` |
|
||||||
|
| `I18N-MAP-004` | 확정 | CSV JP의 오탈자 의심 항목은 제품 문구를 임의 보정하지 않는다. | KR resource key가 있으면 CSV JP를 그대로 반영하고 최종 리포트에 확인 필요로 기록한다. KR resource key가 없으면 반영하지 않는다. | `P1-T2`, `P2-T1` |
|
||||||
|
| `I18N-MAP-005` | 확정 | `values/strings.xml`에 exact 또는 placeholder/suffix 대응 한국어 리소스가 없으면 반영하지 않는다. | 해당 CSV 행은 신규 key 추정 없이 `resource-mapping.md`에서 `제외`로 분류한다. | `P1-T2`, `P1-GATE` |
|
||||||
|
| `I18N-MAP-006` | 확정 | CSV의 `{{...}}`, `{...}`, `n%` 치환 문구는 앱 내 변경 가능한 리소스로 고려한다. | Android `%1$s`, `%1$d`, suffix resource와 안전하게 대응되는 경우 후보로 기록하고 Android 형식 값으로 반영한다. | `P1-T2`, `P2-T1` |
|
||||||
|
| `I18N-MAP-007` | 확정 | 매핑표의 `<br>`은 표시용이며 XML에 그대로 반영하지 않는다. | `values-ja/strings.xml`에는 줄바꿈을 `\n`으로 저장하고 literal `<br>`이 없어야 한다. | `P2-T1`, `P2-GATE` |
|
||||||
|
|
||||||
|
### 7.2 리소스 반영
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `I18N-RES-001` | 확정 | 반영 대상은 `app/src/main/res/values-ja/strings.xml`로 한정한다. | 한국어, 영어, layout, Kotlin 파일 변경 없이 일본어 리소스만 수정한다. | `P2-T1` |
|
||||||
|
| `I18N-RES-002` | 확정 | Android string placeholder 형식을 보존한다. | KR의 `%1$s`, `%1$d`, `%%`에 대응되는 placeholder가 JP에도 보존된다. | `P2-T1`, `P2-GATE` |
|
||||||
|
| `I18N-RES-003` | 확정 | CSV 줄바꿈은 Android XML에서 `\n`으로 표현한다. | 실제 줄바꿈 또는 CSV quote가 XML을 깨지 않는다. | `P2-T1`, `P2-GATE` |
|
||||||
|
| `I18N-RES-004` | 확정 | `@string/...` 참조 값은 CSV 직접 문구와 매칭되더라도 참조 의도를 먼저 확인한다. | 참조를 문자열로 풀어야 하는 근거가 없으면 기존 참조를 보존한다. | `P1-T2`, `P2-T1` |
|
||||||
|
|
||||||
|
## 8. API 계약
|
||||||
|
|
||||||
|
API 변경은 없다. 서버 계약, request/response, endpoint, 인증 header는 이번 범위 밖이다.
|
||||||
|
|
||||||
|
## 9. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- 비밀값, 토큰, URL, 사용자 개인정보를 문서나 로그에 추가하지 않는다.
|
||||||
|
- `BuildConfig` 값은 출력, 로그, Toast, 문서 예시에 포함하지 않는다.
|
||||||
|
- CSV와 resource XML 외 외부 데이터 소스는 사용하지 않는다.
|
||||||
|
|
||||||
|
## 10. 성능과 품질 요구사항
|
||||||
|
|
||||||
|
- 신규 dependency를 추가하지 않는다.
|
||||||
|
- 리소스 XML 파싱과 빌드가 통과해야 한다.
|
||||||
|
- 기본 검증 명령은 `./gradlew :app:assembleDebug`, 필요 시 `./gradlew :app:lintDebug`다.
|
||||||
|
- 로직 변경이 없으므로 신규 unit test는 기본적으로 작성하지 않는다.
|
||||||
|
|
||||||
|
## 11. 성공 기준
|
||||||
|
|
||||||
|
### 11.1 기능 수용 기준
|
||||||
|
|
||||||
|
- [x] CSV KR/JP 행 중 `values/strings.xml`과 매칭되는 후보가 resource key 단위로 식별된다. (`I18N-MAP-001`)
|
||||||
|
- [x] 중복 KR 문구와 기존 JP 충돌 항목이 `resource-mapping.md`에서 반영/보류/제외 중 하나로 분류된다. (`I18N-MAP-002`, `I18N-MAP-003`)
|
||||||
|
- [x] 확정된 key만 `values-ja/strings.xml`에 반영된다. (`I18N-RES-001`)
|
||||||
|
- [x] Android resource placeholder와 줄바꿈 형식이 보존된다. (`I18N-RES-002`, `I18N-RES-003`)
|
||||||
|
|
||||||
|
### 11.2 추적성 완료 기준
|
||||||
|
|
||||||
|
- [x] 모든 확정 요구사항이 `plan-task.md`의 Task와 완료 증거로 연결된다.
|
||||||
|
- [x] 확인 필요 문구는 임의 반영하지 않고 Decision Log 또는 발견된 문제에 남긴다.
|
||||||
|
- [x] 제외 항목은 제외 이유와 후속 조건을 기록한다.
|
||||||
|
|
||||||
|
## 12. Open Questions
|
||||||
|
|
||||||
|
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 시점 | 영향 Goal |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `OQ-001` | 확정 | CSV에 key가 없을 때 반영 정책 | 매핑표 먼저 확정 후 확정 key만 반영 | 사용자 | 2026-08-11 | `P1-T1`, `P1-T2` |
|
||||||
|
| `OQ-002` | 확정 | CSV JP 오탈자 의심 항목을 그대로 반영할지 제품 문구로 보정할지 | KR resource key가 있으면 CSV JP 그대로 반영하고 최종 리포트에 기록 | 사용자 | 2026-08-11 | `P2-T1` |
|
||||||
|
| `OQ-003` | 확정 | 한국어 리소스가 없는 CSV 행을 신규 key로 추가할지 여부 | exact 또는 placeholder/suffix 대응 key가 없으면 신규 key를 만들지 않고 제외 | 사용자 | 2026-08-11 | `P1-T2`, `P1-GATE` |
|
||||||
|
| `OQ-004` | 확정 | 중복 KR 후보의 기존 JP가 CSV JP와 다를 때 처리 | CSV JP 우선 반영 | 사용자 | 2026-08-11 | `P2-T1` |
|
||||||
|
| `OQ-005` | 확정 | 매핑표 `<br>`의 XML 반영 방식 | literal `<br>`이 아니라 `\n` 줄바꿈으로 반영 | 사용자 | 2026-08-11 | `P2-T1`, `P2-GATE` |
|
||||||
|
|
||||||
|
## 13. 요구사항 추적표
|
||||||
|
|
||||||
|
| 요구사항 범위 | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||||
|
|---|---:|---|---|---|
|
||||||
|
| `I18N-MAP-001~007` | 1 | `P1-T1`, `P1-T2`, `P1-GATE` | `resource-mapping.md` 후보 목록 확인 | 사용자 매핑 승인 |
|
||||||
|
| `I18N-RES-001~004` | 2 | `P2-T1`, `P2-GATE` | `./gradlew :app:assembleDebug`, 필요 시 `./gradlew :app:lintDebug` | 일본어 locale 화면 확인은 사용자 명시 요청 시 별도 수행 |
|
||||||
|
|
||||||
|
## 14. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-08-11 | `DEC-001` | 확정 | CSV KR/JP 쌍은 resource key 매핑표를 먼저 확정한 뒤 반영한다. | CSV에 key가 없고 동일 KR 문구가 여러 resource key에 반복된다. | `I18N-MAP-001~003`, `P1-T1`, `P1-T2` |
|
||||||
|
| 2026-08-11 | `DEC-002` | 확정 | 이번 요청에서는 PRD와 계획문서만 생성하고 실제 string resource는 수정하지 않는다. | 사용자 요청: “모호한 것은 물어보고 prd와 계획문서만 생성한다.” | 전체 |
|
||||||
|
| 2026-08-11 | `DEC-003` | 확정 | 문서는 CSV 파일이 있는 `docs/20260811_일본어_string_resource_반영/` 폴더에 생성한다. | 사용자 요청: “문서 생성은 csv파일이 있는 폴더에 생성한다.” | 전체 |
|
||||||
|
| 2026-08-11 | `DEC-004` | 확정 | 매핑표는 `resource-mapping.md` 별도 파일로 생성한다. | 매핑 행이 많고 반영/보류/제외 검토가 필요해 `plan-task.md` Progress에만 누적하면 추적성이 낮다. | `I18N-MAP-002~003`, `P1-T1`, `P1-T2` |
|
||||||
|
| 2026-08-11 | `DEC-005` | 확정 | 한국어 리소스가 없는 CSV 행은 반영하지 않고 `제외`로 분류한다. | 사용자 지시: “한국어 리소스가 없는 것은 반영하지 않는다.” | `I18N-MAP-005`, `P1-T2`, `P1-GATE` |
|
||||||
|
| 2026-08-11 | `DEC-006` | 확정 | JP 오탈자 의심 항목은 CSV 값을 그대로 반영하고 최종 리포트에 확인 필요로 기록한다. | 사용자 지시: “JP 오탈자로 의심되는 것은 우선 그대로 반영하고 최종 리포트에 기록” | `I18N-MAP-004`, `P2-T1` |
|
||||||
|
| 2026-08-11 | `DEC-007` | 확정 | `{{...}}`, `{...}`, `n%` 치환 문구는 Android placeholder/suffix 대응 후보로 재확인한다. | 사용자 지시: “{{}} 형태로 들어가 있는 것은 앱 내에서 변경할 수 있는 리소스” | `I18N-MAP-006`, `P1-T2`, `P2-T1` |
|
||||||
|
| 2026-08-11 | `DEC-008` | 확정 | 중복 KR 후보의 JP가 다르면 CSV JP를 우선 반영한다. | 사용자 지시: “중복되는 것 중 JP가 다르게 적용되어 있는 것은 csv JP를 우선 반영” | `I18N-MAP-002`, `P2-T1` |
|
||||||
|
| 2026-08-11 | `DEC-009` | 확정 | 매핑표 `<br>`은 XML에 literal로 쓰지 않고 `\n`으로 반영한다. | 사용자 지시: “<br>은 그대로 반영하는 것이 아니라 줄바꿈으로 반영” | `I18N-MAP-007`, `P2-T1`, `P2-GATE` |
|
||||||
|
|
||||||
|
## 15. 변경 관리
|
||||||
|
|
||||||
|
요구사항 변경 시 다음을 확인한다.
|
||||||
|
|
||||||
|
- [x] Decision Log에 변경 이유와 날짜를 기록했다.
|
||||||
|
- [x] 관련 요구사항 상태, 본문, 수용 기준을 갱신했다.
|
||||||
|
- [x] `plan-task.md`의 범위, Files, 체크박스, 완료 증거를 코드 변경 전에 갱신했다.
|
||||||
|
- [x] 기존 Progress, 검증 기록, Decision Log를 삭제하거나 덮어쓰지 않았다.
|
||||||
390
docs/20260811_일본어_string_resource_반영/resource-mapping.md
Normal file
390
docs/20260811_일본어_string_resource_반영/resource-mapping.md
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
# 일본어 string resource 매핑표
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 기준 CSV | `docs/20260811_일본어_string_resource_반영/kr_jp_string_resource.csv` |
|
||||||
|
| 한국어 리소스 | `app/src/main/res/values/strings.xml` |
|
||||||
|
| 일본어 리소스 | `app/src/main/res/values-ja/strings.xml` |
|
||||||
|
| 상태 | 리뷰 블로커 반영 완료 |
|
||||||
|
|
||||||
|
## 상태 정의
|
||||||
|
|
||||||
|
| 상태 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| 반영 | HEAD 기준 기존 JA와 달라 `values-ja/strings.xml`에 CSV JP 값 또는 Android placeholder 변환값으로 반영한 대상 |
|
||||||
|
| 변경 없음 | HEAD 기준 기존 JA 값이 CSV JP 값 또는 Android placeholder 변환값과 같아 수정하지 않는 대상 |
|
||||||
|
| 보류 | key는 있으나 현재 JA 값이 목표 값과 다르거나 values-ja key 누락 등으로 확인이 필요한 대상 |
|
||||||
|
| 제외 | 한국어 resource exact/placeholder match가 없어 이번 작업에서 반영하지 않는 대상 |
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
- 반영: 67
|
||||||
|
- 변경 없음: 167
|
||||||
|
- 보류: 0
|
||||||
|
- 제외: 127
|
||||||
|
|
||||||
|
## 매핑 후보
|
||||||
|
|
||||||
|
| CSV row | KR | JP | resource key | 기존 JA | 상태 | 비고 |
|
||||||
|
|---:|---|---|---|---|---|---|
|
||||||
|
| 2 | 홈 | ホーム | `tab_home` | ホーム | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 2 | 홈 | ホーム | `screen_series_detail_tab_home` | ホーム | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 2 | 홈 | ホーム | `screen_series_main_tab_home` | ホーム | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 2 | 홈 | ホーム | `creator_channel_tab_home` | ホーム | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `feed_content_category_default` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `feed_content_category_content` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `tab_content` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `screen_home_recommend_channel_content_label` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `screen_user_profile_content_title` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `screen_search_tab_content` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `screen_search_content_title` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `alarm_add_content_label` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 3 | 콘텐츠 | コンテンツ | `screen_content_main_title` | コンテンツ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 4 | 대화 | トーク | `tab_chat` | チャット | 반영 | CSV JP로 반영 |
|
||||||
|
| 5 | 마이 | マイページ | `tab_my` | マイ | 반영 | CSV JP로 반영 |
|
||||||
|
| 6 | 추천 | おすすめ | `screen_home_tab_recommendation` | おすすめ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 6 | 추천 | おすすめ | `screen_content_tab_recommendation` | おすすめ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 7 | 랭킹 | ランキング | `screen_home_tab_ranking` | ランキング | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 7 | 랭킹 | ランキング | `screen_content_tab_ranking` | ランキング | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 8 | 팔로잉 | フォロー中 | `screen_home_following` | フォロー中 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 8 | 팔로잉 | フォロー中 | `screen_home_tab_following` | フォロー中 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 8 | 팔로잉 | フォロー中 | `screen_live_recommend_channel_prefix` | フォロー中 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 9 | 전체 | すべて | `screen_chat_filter_all` | すべて | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 9 | 전체 | すべて | `creator_channel_live_total_label` | 全体 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `creator_channel_audio_total_label` | 全体 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `creator_channel_series_total_content_count` | 全体 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_home_theme_all` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_content_tab_all` | すべて | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 9 | 전체 | すべて | `creator_channel_fantalk_all_label` | 全体 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `creator_channel_donation_all_label` | すべて | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 9 | 전체 | すべて | `screen_new_characters_all_total_label` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_live_room_total_count_label` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_live_room_create_gender_restriction_all` | 全体 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_service_center_faq_category_all` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_user_follower_list_total_label` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_user_profile_donation_total_label` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `dialog_creator_follow_notify_all` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `screen_content_settings_all` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 9 | 전체 | すべて | `audio_content_label_all` | 全 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 10 | 방금 활동한 크리에이터 | 最近活動したクリエイター | `home_recommendation_section_recently_active_creators` | 最近活動したクリエイター | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 11 | 최근 데뷔한 크리에이터 | 新着クリエイター | `home_recommendation_section_recent_debut_creators` | 最近デビューしたクリエイター | 반영 | CSV JP로 반영 |
|
||||||
|
| 12 | 처음부터 함께 성장! | 今から推せる | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 13 | 크리에이터와 이야기를 나눠요! | クリエイターと話す | `home_recommendation_section_ai_characters` | クリエイターと話しましょう! | 반영 | CSV JP로 반영 |
|
||||||
|
| 14 | 추천 필모그래피 | おすすめフィルモグラフィー | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 15 | 또 다른 모습 | 写真集 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 16 | {{장르}}의 크리에이터 | のクリエイター | `home_recommendation_section_genre_creator_suffix` | のクリエイター | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 17 | 최근 응원이 많은 크리에이터 | 最近応援の多いクリエイター | `home_recommendation_section_cheer_creators` | 最近応援が多いクリエイター | 반영 | CSV JP로 반영 |
|
||||||
|
| 18 | 인기 커뮤니티 | 人気コミュニティ | `home_recommendation_section_popular_community_posts` | 人気コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 19 | 주간 인기 | 週間人気 | `screen_content_ranking_type_weekly_popular` | 週間人気 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 20 | 지금 뜨는 중 | 急上昇 | `screen_content_ranking_type_rising` | 急上昇 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 21 | 남성 인기 | 男性人気 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 22 | 여성 인기 | 女性人気 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 23 | 최근 대화 | 最近のトーク | `screen_home_following_recent_chats_title` | 最近のチャット | 반영 | CSV JP로 반영 |
|
||||||
|
| 24 | 이달의 스케줄 | 今月のスケジュール | `screen_home_following_monthly_schedules_title` | 今月のスケジュール | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 25 | 최근 소식 | 最新情報 | `screen_home_following_recent_news_title` | 最近のお知らせ | 반영 | CSV JP로 반영 |
|
||||||
|
| 26 | {{크리에이터 이름}}님이 이번 주 랭킹 n위에 올랐어요! | %1$sが今週のランキングで%2$sにランクイン! | `screen_home_following_creator_ranking_news_message` | %1$sさんが今週のランキング%2$sに入りました! | 반영 | placeholder 후보; Android placeholder/suffix 형식으로 반영 |
|
||||||
|
| 28 | 아직 팔로우한 크리에이터가 없어요. 관심 있는 크리에이터를 팔로우해 보세요. | まだフォローしているクリエイターがいません。<br>気になるクリエイターをフォローしてみましょう。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 29 | 찾아보기 | 探す | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 30 | 오직 보이스온에서만! | VOICE ON限定 | `content_recommendation_section_original_series` | Voice Onだけで! | 반영 | CSV JP로 반영 |
|
||||||
|
| 31 | 새로 올라온 오디오 | 新着オーディオ | `content_recommendation_section_latest_audio` | 新着オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 32 | 추천 시리즈 | おすすめシリーズ | `screen_series_main_recommend_title` | おすすめシリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 32 | 추천 시리즈 | おすすめシリーズ | `audio_content_recommend_series_title` | おすすめシリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 33 | 무료 오디오 | 無料オーディオ | `content_recommendation_section_free_audio` | 無料オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 34 | 포인트 오디오 | ポイントオーディオ | `content_recommendation_section_point_audio` | ポイントオーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 35 | 최근 댓글이 많은 오디오 | 最近コメントの多いオーディオ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 36 | {{키워드}}의 오디오 | {{キーワード}}のオーディオ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 37 | 추천 오디오 | おすすめオーディオ | `content_recommendation_section_recommended_audio` | おすすめオーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 38 | 매출 | 売上 | `screen_home_sort_revenue` | 売上 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 38 | 매출 | 売上 | `screen_content_ranking_type_revenue` | 売上 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 39 | 판매량 | 販売数 | `screen_home_sort_sales_count` | 販売数 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 39 | 판매량 | 販売数 | `screen_content_ranking_type_sales_count` | 販売数 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 40 | 댓글수 | コメント数 | `screen_content_ranking_type_comment_count` | コメント数 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 41 | 좋아요 | いいね | `screen_home_sort_like_count` | いいね | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 41 | 좋아요 | いいね | `screen_content_ranking_type_like_count` | いいね | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 42 | 오디오 | オーディオ | `screen_content_all_type_audio` | オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 42 | 오디오 | オーディオ | `home_recommendation_activity_audio` | オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 42 | 오디오 | オーディオ | `creator_channel_tab_audio` | オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 42 | 오디오 | オーディオ | `creator_channel_section_audio` | オーディオ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `feed_content_category_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `screen_content_all_type_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `creator_channel_tab_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `creator_channel_section_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `screen_user_profile_series_title` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `screen_search_tab_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `screen_search_series_title` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 43 | 시리즈 | シリーズ | `audio_content_home_tab_series` | シリーズ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 44 | 오리지널 | オリジナル | `screen_content_all_type_original` | オリジナル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_live_now_all_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_content_all_type_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `live_reservation_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_live_room_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_live_room_create_price_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_creator_community_write_price_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `screen_audio_content_upload_price_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `audio_content_price_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `audio_content_tag_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 45 | 무료 | 無料 | `audio_content_home_tab_free` | 無料 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 46 | 포인트 | ポイント | `screen_content_all_type_point` | ポイント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 46 | 포인트 | ポイント | `screen_user_profile_latest_content_point` | ポイント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 46 | 포인트 | ポイント | `audio_content_badge_point` | ポイント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 47 | 화보 | フォト | `screen_home_following_photo_content` | グラビア | 반영 | CSV JP로 반영 |
|
||||||
|
| 48 | 최신순 | 最新順 | `screen_series_content_sort_newest` | 最新順 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 48 | 최신순 | 最新順 | `screen_audition_sort_newest` | 最新順 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 48 | 최신순 | 最新順 | `screen_audio_content_sort_newest` | 最新順 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 49 | 추천순 | おすすめ順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 50 | 인기순 | 人気順 | `screen_audio_content_sort_popularity` | 人気順 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 51 | 높은 가격순 | 高い順 | `screen_audio_content_sort_price_high` | 価格が高い順 | 반영 | CSV JP로 반영 |
|
||||||
|
| 52 | 낮은 가격순 | 安い順 | `screen_audio_content_sort_price_low` | 価格が低い順 | 반영 | CSV JP로 반영 |
|
||||||
|
| 53 | 소장순 | 保有順 | `creator_channel_live_sort_owned` | 所持順 | 반영 | CSV JP로 반영 |
|
||||||
|
| 54 | 팔로우 | フォロー | `screen_home_follow` | フォロー | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 54 | 팔로우 | フォロー | `creator_channel_follow_button` | フォロー | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 55 | 팔로워 | フォロワー | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 56 | 대화하기 | トークする | `creator_channel_chat_button` | トークする | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 56 | 대화하기 | トークする | `character_detail_chat_button` | トークする | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 57 | DM 보내기 | DMを送る | `creator_channel_dm_button` | DMを送る | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `tab_live` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `home_recommendation_section_live` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `home_recommendation_activity_live` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `creator_channel_tab_live` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `screen_live_reservation_title_prefix` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `screen_live_replay_prefix` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 58 | 라이브 | ライブ | `screen_user_profile_live_title` | ライブ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `home_recommendation_activity_community` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `creator_channel_tab_community` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `creator_channel_section_community` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `creator_channel_community_detail_title` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `screen_live_community_title` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `screen_user_profile_community_title` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 59 | 커뮤니티 | コミュニティ | `screen_creator_community_all_title` | コミュニティ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 60 | 팬Talk | ファントーク | `creator_channel_tab_fantalk` | ファンTalk | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 60 | 팬Talk | ファントーク | `creator_channel_section_fantalk` | ファンTalk | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 61 | 후원 | サポート | `creator_channel_tab_donation` | 応援 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 61 | 후원 | サポート | `creator_channel_section_donation` | 応援 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 61 | 후원 | サポート | `screen_audio_content_detail_donation` | ギフト | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 62 | 팬레터 | ファンレター | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 63 | 후원하기 | サポートする | `creator_channel_donation_button` | 応援する | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 63 | 후원하기 | サポートする | `creator_channel_donation_action` | 応援する | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 63 | 후원하기 | サポートする | `screen_live_room_donation_title` | ギフトする | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 64 | 공지 | お知らせ | `creator_channel_section_notice` | お知らせ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 64 | 공지 | お知らせ | `creator_channel_community_notice` | お知らせ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 64 | 공지 | お知らせ | `screen_live_room_notice` | 告知 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 65 | 스케줄 | スケジュール | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 66 | 전체보기 | すべて見る | `view_all` | すべて見る | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 66 | 전체보기 | すべて見る | `screen_series_main_completed_all` | すべて見る | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 66 | 전체보기 | すべて見る | `creator_channel_donation_ranking_all` | すべて見る | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 67 | 전체 오디오의 n%를 소장하고 있어요. | 全オーディオの%1$s%%を所持しています。 | `creator_channel_audio_owned_rate_message` | 全オーディオの%1$s%%を所持しています。 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 68 | 전체 화보의 n%를 소장하고 있어요. | 全フォトのn%を所持しています。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 69 | 전체소장 | 全話購入 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 70 | 완결 | 完結 | `creator_channel_series_status_completed` | 完結 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 70 | 완결 | 完結 | `screen_home_series_complete` | 完結 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 71 | 연재 | 連載 | `creator_channel_series_status_proceeding` | 連載中 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 71 | 연재 | 連載 | `screen_series_detail_label_published_days` | 連載 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 72 | 소장중 | 保有中 | `audio_content_badge_owned` | 所持中 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 72 | 소장중 | 保有中 | `alarm_select_badge_owned` | 所持中 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 73 | 리스트형 | リスト | `creator_channel_community_view_mode_list` | リスト型 | 반영 | CSV JP로 반영 |
|
||||||
|
| 74 | 썸네일형 | グリッド | `creator_channel_community_view_mode_grid` | サムネイル型 | 반영 | CSV JP로 반영 |
|
||||||
|
| 75 | 구매완료 | 購入済み | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 76 | 삭제 | 削除 | `confirm_delete_title` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 76 | 삭제 | 削除 | `screen_message_detail_action_delete` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 76 | 삭제 | 削除 | `screen_roulette_config_option_delete` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 76 | 삭제 | 削除 | `screen_creator_community_record_delete` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 76 | 삭제 | 削除 | `screen_audio_content_detail_delete` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 76 | 삭제 | 削除 | `menu_delete_action` | 削除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 77 | 신고 | 通報 | `report_title` | 通報 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 77 | 신고 | 通報 | `report_button` | 通報 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 77 | 신고 | 通報 | `creator_channel_fantalk_report` | 通報 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 78 | 후원 랭킹 | サポートランキング | `creator_channel_donation_ranking_title` | 応援ランキング | 반영 | CSV JP로 반영 |
|
||||||
|
| 81 | 크리에이터가 라이브를 준비 중입니다.<br>기대해 주세요! | クリエイターがライブを準備しています。<br>お楽しみに! | `creator_channel_live_empty_message` | クリエイターがライブを準備中です。<br>楽しみにお待ちください! | 반영 | CSV JP로 반영 |
|
||||||
|
| 84 | 크리에이터가 오디오를 준비 중입니다.<br>기대해 주세요! | クリエイターがオーディオを準備しています。<br>お楽しみに! | `creator_channel_audio_empty_message` | クリエイターがオーディオを準備中です。<br>楽しみにお待ちください! | 반영 | CSV JP로 반영 |
|
||||||
|
| 87 | 크리에이터가 시리즈를 준비 중입니다.<br>기대해 주세요! | クリエイターがシリーズを準備しています。<br>お楽しみに! | `creator_channel_series_empty_message` | クリエイターがシリーズを準備中です。<br>楽しみにお待ちください! | 반영 | CSV JP로 반영 |
|
||||||
|
| 90 | 크리에이터가 화보를 준비 중입니다.<br>기대해 주세요! | クリエイターがフォトを準備しています。<br>お楽しみに! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 93 | 크리에이터가 커뮤니티를 준비 중입니다.<br>기대해 주세요! | クリエイターがコミュニティを準備しています。<br>お楽しみに! | `creator_channel_community_empty_message` | クリエイターがコミュニティを準備中です。<br>楽しみにお待ちください! | 반영 | CSV JP로 반영 |
|
||||||
|
| 96 | 아직 응원이 없습니다.<br>처음으로 크리에이터를 응원해 보세요! | まだファンレターがありません。<br>最初のファンレターを送ってみましょう! | `creator_channel_fantalk_empty_message` | まだ応援がありません。<br>最初にクリエイターを応援してみましょう! | 반영 | CSV JP로 반영 |
|
||||||
|
| 97 | 응원 남기기 | ファンレターを送る | `creator_channel_fantalk_support_action` | 応援を残す | 반영 | CSV JP로 반영 |
|
||||||
|
| 100 | 아직 후원이 없습니다.<br>처음으로 크리에이터를 후원해 보세요! | まだサポートがありません。<br>最初のサポートをしてみましょう! | `creator_channel_donation_empty_title` | 最初にクリエイターを<br>応援してみましょう! | 반영 | CSV JP로 반영 |
|
||||||
|
| 101 | 당신의 첫 응원을 기다리고 있어요! | あなたの最初のファンレターを待っています! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 102 | 댓글 | コメント | `screen_home_sort_comment_count` | コメント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 102 | 댓글 | コメント | `character_detail_comments_label` | コメント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 102 | 댓글 | コメント | `character_comment_title` | コメント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 102 | 댓글 | コメント | `screen_audio_content_detail_comment_title` | コメント | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 103 | 취소 | キャンセル | `cancel` | キャンセル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 103 | 취소 | キャンセル | `creator_channel_fantalk_write_cancel` | キャンセル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 103 | 취소 | キャンセル | `screen_message_write_cancel` | キャンセル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 103 | 취소 | キャンセル | `screen_live_room_cancel` | キャンセル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 103 | 취소 | キャンセル | `screen_audio_content_upload_cancel` | キャンセル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 104 | 보내기 | 送信 | `creator_channel_fantalk_write_send` | 送信 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 105 | 입력 종료 | 入力終了 | `creator_channel_fantalk_write_exit_title` | 入力終了 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 108 | 입력을 종료할까요?<br>지금 나가면 입력한 내용이 저장되지 않아요. | 入力を終了しますか?<br>終了すると、入力内容は保存されません。 | `creator_channel_fantalk_write_exit_desc` | 入力を終了しますか?<br>今離れると、入力した内容は保存されません。 | 반영 | CSV JP로 반영 |
|
||||||
|
| 109 | 종료 | 終了 | `creator_channel_fantalk_write_exit_confirm` | 終了 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 110 | 계속 작성 | 続ける | `creator_channel_fantalk_write_exit_cancel` | 続けて書く | 반영 | CSV JP로 반영 |
|
||||||
|
| 111 | 응원 삭제 | ファンレターを削除 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 114 | 응원을 삭제할까요?<br>삭제 시 크리에이터의 답글도 같이 사라져요. | ファンレターを削除しますか?<br>削除すると、クリエイターからの返信も削除されます。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 115 | 오늘 | 今日 | `screen_user_profile_donation_stat_today` | 今日 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 116 | 지금 | 今 | `screen_live_now_title_prefix` | 今 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 117 | 지난 | 終了 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 120 | 아직 대화가 없어요.<br>크리에이터와 이야기를 시작해 보세요. | まだ会話がありません。<br>クリエイターと話してみましょう。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 123 | 아직 나눈 AI 채팅이 없어요.<br>크리에이터와 대화를 시작해 보세요. | まだAIチャットがありません。<br>クリエイターと会話を始めてみましょう。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 126 | 아직 나눈 대화가 없어요.<br>좋아하는 크리에이터에게 메세지를 보내보세요. | まだメッセージがありません。<br>好きなクリエイターにメッセージを送ってみましょう。 | `screen_chat_empty_dm` | まだ会話がありません。<br>好きなクリエイターにメッセージを送ってみましょう。 | 반영 | CSV JP로 반영 |
|
||||||
|
| 127 | 크리에이터의 이름을 입력하세요 | クリエイター名を入力 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 128 | 대화 시작 | トーク開始 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 129 | {{크리에이터이름}}님과 AI 채팅 혹은 DM을 시작해 볼까요? | {{クリエイター名}}とAIチャットまたはDMを始めてみませんか? | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 130 | AI 채팅하기 | AIチャット | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 131 | 채팅 설정 | チャット設定 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 132 | 이미지 보관함 | ギャラリー | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 133 | 배경 이미지 설정 | 背景設定 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 134 | 채팅 초기화 | チャットを初期化 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 135 | 신규 크리에이터 | 新着クリエイター | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 136 | 나의 크리에이터 | マイクリエイター | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 137 | AI 캐릭터 채팅 | AIキャラクターチャット | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 138 | 팔로우 해제 | フォロー解除 | `dialog_unfollow_title` | フォロー解除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 141 | 팔로우를 해제할까요?<br>{{크리에이터이름}}의 소식을 받을 수 없어요. | {{크리에이터이름}}からの最新情報を受け取れなくなります。<br>フォローを解除しますか? | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 142 | 해제하기 | 解除する | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 143 | 검색어를 입력하세요 | キーワードを入力 | `audio_content_home_search_placeholder` | 検索ワードを入力 | 반영 | CSV JP로 반영 |
|
||||||
|
| 144 | 전체삭제 | すべて削除 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 145 | 최근 검색 | 最近の検索 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 146 | 인기 검색어 | 急上昇ワード | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 147 | AI 채팅 인기 검색 키워드 | AIチャット急上昇ワード | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 148 | 시리즈 인기 검색 키워드 | シリーズ急上昇ワード | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 149 | 오디오 인기 검색 키워드 | オーディオ急上昇ワード | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 150 | 키워드 | キーワード | `screen_series_detail_section_keywords` | キーワード | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 151 | 검색 결과 | 検索結果 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 152 | 정확도순 | 関連度順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 153 | 많이 찾는 크리에이터 | 人気のクリエイター | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 156 | 관련 크리에이터가 없어요.<br>다른 검색어로 다시 찾아보세요. | 該当するクリエイターがいません。<br>別のキーワードで検索してください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 159 | 관련 시리즈가 없어요.<br>다른 검색어로 다시 찾아보세요. | 該当するシリーズがありません。<br>別のキーワードで検索してください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 162 | 관련 화보가 없어요.<br>다른 검색어로 다시 찾아보세요. | 該当するフォトがありません。<br>別のキーワードで検索してください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 165 | 관련 키워드가 없어요.<br>다른 검색어로 다시 찾아보세요. | 該当するキーワードがありません。<br>別のキーワードで検索してください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 166 | 라이브 시작하기 | ライブを開始 | `creator_channel_live_start_button` | ライブを始める | 반영 | CSV JP로 반영 |
|
||||||
|
| 167 | 오디오 올리기 | オーディオを投稿 | `creator_channel_audio_upload_button` | オーディオを投稿 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 168 | 커뮤니티 글 올리기 | コミュニティ投稿 | `creator_channel_owner_fab_community` | コミュニティ投稿 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 169 | 이메일을 입력해 주세요 | メールアドレスを入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 170 | 이메일 | メールアドレス | `hint_email` | メールアドレス | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 170 | 이메일 | メールアドレス | `screen_profile_update_email_label` | メールアドレス | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 171 | 이용약관 | 利用規約 | `terms_of_service` | 利用規約 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 171 | 이용약관 | 利用規約 | `screen_settings_terms` | 利用規約 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 172 | 개인정보수집 및 이용동의 | 個人情報の収集・利用への同意 | `privacy_policy` | 個人情報収集および利用同意 | 반영 | CSV JP로 반영 |
|
||||||
|
| 173 | 올바른 이메일을 입력해 주세요 | 正しいメールアドレスを入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 174 | 다음으로 | 次へ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 175 | 이미 가입된 이메일이에요 | 登録済みのメールアドレスです | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 176 | 비밀번호를 입력해 주세요 | パスワードを入力してください | `screen_live_room_password_hint` | パスワードを入力してください | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 177 | 비밀번호 | パスワード | `hint_password` | パスワード | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 177 | 비밀번호 | パスワード | `screen_live_room_password_label` | パスワード | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 177 | 비밀번호 | パスワード | `screen_profile_update_password_label` | パスワード | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 178 | 8자 이상 (영문 대/소문자, 특수문자 포함) | 8文字以上(英大文字・小文字・記号を含む) | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 179 | 완료 | 完了 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 180 | 8자 이상, 영문 대/소문자와 특수문자를 포함해 주세요 | 8文字以上、英大文字・小文字・記号を含めてください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 181 | 이메일로 로그인하기 | メールアドレスでログイン | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 182 | 로그인 | ログイン | `title_login` | ログイン | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 183 | 가입되지 않은 이메일이에요 | 登録されていないメールアドレスです | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 184 | 입력한 비밀번호를 다시 확인해 주세요 | パスワードをもう一度確認してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 185 | 인증코드를 입력해 주세요 | 認証コードを入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 186 | {email}로 인증코드를 보냈어요 | {email}に認証コードを送信しました | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 187 | 인증코드 | 認証コード | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 188 | 0:00 후에 인증코드 다시 보내기 | 0:00後に認証コードを再送する | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 189 | 인증 가능한 시간을 초과했어요 | 認証可能時間を超過しました | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 190 | 인증코드 다시 보내기 | 認証コードを再送する | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 191 | 올바른 인증코드를 입력해 주세요 | 正しい認証コードを入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 192 | 새 비밀번호를 입력해 주세요 | 新しいパスワードを入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 193 | 기존 비밀번호와 다르게 설정해 주세요 | 現在のパスワードとは異なるものを設定してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 194 | 비밀번호 설정 완료! | パスワード設定完了! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 197 | 비밀번호 설정을 완료했어요.<br>다시 로그인해 주세요. | パスワードを設定しました。<br>もう一度ログインしてください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 198 | 재설정 종료 | 再設定を終了 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 201 | 지금 나가면 현재 진행중인 재설정이 종료되고,<br>처음부터 다시 진행해야 해요. | 終了すると、進行中の再設定が終了し、<br>最初からやり直す必要があります。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 202 | 계속 재설정 | 再設定を続ける | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 203 | AI 채팅 | AIチャット | `screen_chat_filter_ai` | AIチャット | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 204 | 전체 크리에이터 | すべてのクリエイター | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 205 | 필수 | 必須 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 207 | 가입한 이메일을 입력해 주세요 | 登録メールアドレスを<br>入力してください | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 208 | 존재하지 않는 이메일이에요 | 登録されていないメールアドレスです | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 209 | 모두 팔로우하기 | すべてフォロー | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 210 | 대여중 | レンタル中 | `audio_content_badge_rented` | レンタル中 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 210 | 대여중 | レンタル中 | `alarm_select_badge_rental` | レンタル中 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 211 | 데뷔 | デビュー | `creator_channel_activity_debut` | デビュー | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 211 | 데뷔 | デビュー | `screen_creator_detail_debut` | デビュー | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 212 | 라이브 총 진행 수 | ライブ配信回数 | `creator_channel_activity_live_count` | ライブ総配信数 | 반영 | CSV JP로 반영 |
|
||||||
|
| 213 | 라이브 누적 진행 시간 | ライブ累計配信時間 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 214 | 라이브 누적 참여자 | ライブ累計参加者数 | `creator_channel_activity_live_contributor` | ライブ累計参加者 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 214 | 라이브 누적 참여자 | ライブ累計参加者数 | `screen_creator_detail_live_contributor_count` | ライブ累積参加者 | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 215 | 고정 해제 | 固定を解除 | `screen_creator_community_unpin` | 固定を解除 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 216 | 수정하기 | 編集 | `creator_channel_fantalk_edit` | 編集する | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 216 | 수정하기 | 編集 | `screen_live_room_update_action` | 修正する | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 217 | 삭제하기 | 削除 | `creator_channel_fantalk_delete` | 削除する | 반영 | CSV JP로 반영 |
|
||||||
|
| 218 | 크리에이터에게만 보여요. | クリエイターにのみ表示されます。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 219 | 답글쓰기 | 返信する | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 220 | 신고하기 | 通報する | `screen_audio_content_detail_report` | 通報する | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 220 | 신고하기 | 通報する | `menu_report_action` | 通報する | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 221 | 댓글을 입력하세요 | コメントを入力 | `creator_channel_community_comment_placeholder` | コメントを入力してください | 반영 | CSV JP로 반영 |
|
||||||
|
| 222 | 크리에이터에게 응원의 메세지를 보내주세요! | クリエイターへファンレターを送りましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 223 | 답글을 삭제할까요? | 返信を削除しますか? | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 224 | 답글 삭제 | 返信を削除 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 225 | 크리에이터 | クリエイター | `screen_live_reservation_complete_creator_label` | クリエイター | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 226 | 시리즈 자세히 보기 | シリーズの詳細を見る | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 227 | 소개 | 紹介 | `creator_channel_section_introduce` | 紹介 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 228 | 장르 | ジャンル | `screen_series_detail_label_genre` | ジャンル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 229 | 연령 제한 | 年齢制限 | `screen_live_room_create_age_label` | 年齢制限 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 229 | 연령 제한 | 年齢制限 | `screen_creator_community_write_age_title` | 年齢制限 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 229 | 연령 제한 | 年齢制限 | `screen_audio_content_upload_age_section_title` | 年齢制限 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 230 | 소장 | 購入 | `screen_series_detail_price_keep_label` | 購入 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 230 | 소장 | 購入 | `audio_content_order_label_keep` | 購入 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 231 | 페이지 수 | ページ数 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 232 | 발매일 | 発売日 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 233 | 크리에이터의 다른 화보 | このクリエイターの他のフォト | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 234 | {{화보이름 12자 ...}}와 비슷한 화보 | 「{{화보이름 12자 ...}}」に似たフォト | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 235 | [Premium Photo]의 다른 화보 | 「Premium Photo」の他のフォト | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 236 | 소장하기 | 購入する | `screen_audio_content_detail_action_keep` | 購入する | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 237 | 스크랩 | クリップ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 240 | 마음에 드는 페이지를 스크랩하여<br>감상해 보세요! | お気に入りのページをクリップして<br>楽しみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 241 | 마지막 페이지 | 最後のページ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 244 | {{메거진 이름 12자 초과시...}}은 어떠셨나요?<br>후기를 댓글로 남겨보세요. | 「{{雑誌名...}}」はいかがでしたか<br>感想をコメントでお聞かせください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 245 | 댓글 쓰기 | コメントを書く | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 246 | 첫 페이지에요. | 最初のページです。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 247 | 오래된순 | 古い順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 248 | 보관함 | ライブラリ | `screen_message_filter_keep` | コレクション | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 248 | 보관함 | ライブラリ | `screen_my_storage` | コレクション | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 249 | 구매 | 購入 | `alarm_list_buy_confirm` | 購入 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 250 | 재생목록 | プレイリスト | `screen_audio_content_box_tab_playlists` | プレイリスト | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 251 | 최근 | 最近 | `screen_live_latest_finished_prefix` | 最近 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 252 | 제목순 | タイトル順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 253 | 발매순 | 発売日順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 254 | 대여 | レンタル | `screen_series_detail_price_rental_label` | レンタル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 254 | 대여 | レンタル | `audio_content_order_label_rental` | レンタル | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 255 | 업로드순 | アップロード順 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 258 | 구매한 오디오가 없어요.<br>관심있는 오디오를 구매해 보세요! | 購入したオーディオはありません。<br>気になるオーディオを購入してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 261 | 구매한 화보가 없어요.<br>관심있는 화보를 구매해 보세요! | 購入したフォトはありません。<br>気になるフォトを購入してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 264 | 구매한 커뮤니티 글이 없어요.<br>좋아하는 크리에이터의 글을 구매해 보세요! | 購入したコミュニティ投稿はありません。<br>お気に入りのクリエイターの投稿を購入してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 265 | 나중에 들을 오디오 | あとで聴く | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 268 | 좋아요한 오디오가 없어요.<br>마음에 드는 오디오에 좋아요를 남겨 보세요! | 「いいね」したオーディオはありません。<br>お気に入りのオーディオに「いいね」してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 271 | 좋아요한 화보가 없어요.<br>마음에 드는 화보에 좋아요를 남겨 보세요! | 「いいね」したフォトはありません。<br>お気に入りのフォトに「いいね」してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 274 | 좋아요한 커뮤니티 글이 없어요.<br>마음에 드는 글에 좋아요를 남겨 보세요! | 「いいね」した投稿はありません。<br>お気に入りの投稿に「いいね」してみましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 277 | 최근 본 콘텐츠가 없어요.<br>새로운 콘텐츠를 만나보세요! | 最近見たコンテンツはありません。<br>新しいコンテンツを見つけましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 280 | 최근 본 오디오가 없어요.<br>새로운 오디오를 만나보세요! | 最近聴いたオーディオはありません。<br>新しいオーディオを見つけましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 283 | 최근 본 시리즈가 없어요.<br>새로운 시리즈를 만나보세요! | 最近見たシリーズはありません。<br>しいシリーズを見つけましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음; CSV JP 오탈자 의심(최종 확인 필요) |
|
||||||
|
| 286 | 최근 본 화보가 없어요.<br>새로운 화보를 만나보세요! | 最近見たフォトはありません。<br>新しいフォトを見つけましょう! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 287 | 충전하기 | チャージ | `screen_can_status_charge_action` | チャージする | 반영 | 중복 KR 후보; CSV JP 우선 반영 |
|
||||||
|
| 287 | 충전하기 | チャージ | `screen_can_charge_title` | チャージ | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 288 | 현재 보유한 캔 | 現在のカン | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 289 | 사용할 캔 | 使用するカン | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 290 | 사용할 포인트 | 使用するポイント | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 291 | *포인트 일부를 차감하여 대여합니다. | *ポイントを使用してレンタルします。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 292 | 캔이 부족해요! 충전 후 이용해 주세요. | カンが不足しています。チャージしてご利用ください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 293 | 소장이 완료되었어요! | 購入が完了しました! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 294 | 대여가 완료되었어요! | レンタルが完了しました! | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 295 | 구매에 실패했어요. 다시 시도해 주세요. | 購入に失敗しました。もう一度お試しください。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 296 | 오늘 하루 보지 않기 | 今日一日は表示しない | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 297 | 닫기 | 閉じる | `dialog_event_popup_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `a11y_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `creator_channel_owner_fab_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `dialog_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `screen_live_room_collapse` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `screen_live_room_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 297 | 닫기 | 閉じる | `screen_creator_community_write_close` | 閉じる | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 298 | 콘텐츠 보호를 위해 캡처를 지원하지 않아요. | コンテンツ保護のため、スクリーンショットはご利用いただけません。 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 299 | 답글을 입력하세요 | 返信を入力 | `creator_channel_community_reply_placeholder` | 返信を入力してください | 반영 | CSV JP로 반영 |
|
||||||
|
| 300 | 전체 인기 | 人気ランキング | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 301 | 현재 보유 | 所持数 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 302 | 활동 | アクティビティ | `creator_channel_section_activity` | 活動 | 반영 | CSV JP로 반영 |
|
||||||
|
| 303 | 익명 | 匿名 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 304 | DM 확인하기 | DMを確認 | `creator_channel_dm_check_button` | DMを確認 | 변경 없음 | HEAD 기준 기존 JA와 목표 값 동일 |
|
||||||
|
| 305 | 예정 | 予定 | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 306 | 대화 찾기 | トークを探す | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
|
| 307 | {요일}요 인기 시리즈 | {요일}曜人気シリーズ | | | 제외 | values/strings.xml exact/placeholder match 없음 |
|
||||||
@@ -34,13 +34,14 @@
|
|||||||
./gradlew :app:test
|
./gradlew :app:test
|
||||||
./gradlew :app:testDebugUnitTest
|
./gradlew :app:testDebugUnitTest
|
||||||
./gradlew :app:testReleaseUnitTest
|
./gradlew :app:testReleaseUnitTest
|
||||||
./gradlew :app:connectedDebugAndroidTest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
주의:
|
주의:
|
||||||
- `:app:connectedDebugAndroidTest`는 기기/에뮬레이터 연결이 필요하다.
|
- 기본 자동 검증은 `app/src/test`의 로직/local unit test만 사용한다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest` UI/계측 테스트를 작성하거나 `:app:connectedDebugAndroidTest`를 실행하지 않는다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않으면 기기·에뮬레이터 UI 조작, 스크린샷 캡처, 시각 QA를 검증 계획이나 완료 증거에 포함하지 않는다.
|
||||||
|
- UI 변경은 adapter 분기, mapper, formatter, presentation model, 라우팅 등 화면 표현을 결정하는 로직만 테스트한다. View 크기·margin·padding·constraint·visibility 같은 레이아웃 속성 테스트는 작성하지 않는다.
|
||||||
- `app/build.gradle`에 `lint { checkReleaseBuilds false }`가 있어 릴리스 린트는 `:app:lintRelease`를 명시 실행해야 한다.
|
- `app/build.gradle`에 `lint { checkReleaseBuilds false }`가 있어 릴리스 린트는 `:app:lintRelease`를 명시 실행해야 한다.
|
||||||
- 현재 `app/src/androidTest`에는 테스트 소스가 없으므로 계측 테스트 명령은 신규 테스트 추가 시 사용한다.
|
|
||||||
|
|
||||||
### 1) 단일 테스트 실행 (중요)
|
### 1) 단일 테스트 실행 (중요)
|
||||||
로컬 단위 테스트(`app/src/test`)는 `--tests` 필터를 사용한다.
|
로컬 단위 테스트(`app/src/test`)는 `--tests` 필터를 사용한다.
|
||||||
@@ -144,14 +145,6 @@ Chat Action Phase 테스트 예시:
|
|||||||
- 한글 테스트명을 `--tests`로 실행할 때도 클래스명과 메서드명을 포함한 전체 문자열을 따옴표로 감싼다.
|
- 한글 테스트명을 `--tests`로 실행할 때도 클래스명과 메서드명을 포함한 전체 문자열을 따옴표로 감싼다.
|
||||||
- 메서드 매칭이 불안정하면 클래스 단위로 먼저 실행한다.
|
- 메서드 매칭이 불안정하면 클래스 단위로 먼저 실행한다.
|
||||||
|
|
||||||
### 2) 계측 테스트 클래스/메서드 타깃 실행
|
### 2) UI/계측 테스트 예외
|
||||||
Gradle 인자 방식:
|
- UI/계측 테스트가 반드시 필요한 경우에도 사용자의 명시 요청을 먼저 확인한다.
|
||||||
```bash
|
- 승인된 작업에서만 해당 Task의 계획 문서에 대상, 실행 명령, 종료 조건과 정리 절차를 기록하고 실행한다.
|
||||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest
|
|
||||||
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod
|
|
||||||
```
|
|
||||||
|
|
||||||
ADB 대안:
|
|
||||||
```bash
|
|
||||||
adb shell am instrument -w -e class kr.co.vividnext.sodalive.SomeInstrumentedTest#someMethod <test_package>/<runner>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
- 테스트 추가 시 단일 실행 명령 예시도 `docs/agent-guides/build-test-style.md`에 갱신한다.
|
- 테스트 추가 시 단일 실행 명령 예시도 `docs/agent-guides/build-test-style.md`에 갱신한다.
|
||||||
- 신규 테스트는 로직에 대한 테스트 코드만 작성한다. View 크기, margin, padding, constraint, visibility 같은 UI 레이아웃/표현 속성 검증 테스트는 작성하지 않는다.
|
- 신규 테스트는 로직에 대한 테스트 코드만 작성한다. View 크기, margin, padding, constraint, visibility 같은 UI 레이아웃/표현 속성 검증 테스트는 작성하지 않는다.
|
||||||
- UI 작업에서 테스트가 필요하면 adapter view type 선택, rank 구간 분류, mapper, formatter, presentation model, visibility를 결정하는 순수 로직처럼 화면 표현을 결정하는 입력/출력 계약만 검증한다.
|
- UI 작업에서 테스트가 필요하면 adapter view type 선택, rank 구간 분류, mapper, formatter, presentation model, visibility를 결정하는 순수 로직처럼 화면 표현을 결정하는 입력/출력 계약만 검증한다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest` UI/계측 테스트를 추가하거나 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 실행하지 않는다.
|
||||||
|
|
||||||
### 6) 주석
|
### 6) 주석
|
||||||
- 의미 단위별로 주석을 작성한다.
|
- 의미 단위별로 주석을 작성한다.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## 작업 절차 체크리스트
|
## 작업 절차 체크리스트
|
||||||
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
||||||
- 변경 중: 공개 API 스키마를 임의 변경하지 말고 작은 단위로 안전하게 수정한다.
|
- 변경 중: 공개 API 스키마를 임의 변경하지 말고 작은 단위로 안전하게 수정한다.
|
||||||
- 변경 후: 최소 단일 테스트(`--tests`) 또는 `./gradlew :app:test`를 실행하고 필요 시 `./gradlew :app:ktlintCheck`를 수행한다.
|
- 변경 후: `app/src/test`의 최소 로직 단일 테스트(`--tests`) 또는 `./gradlew :app:test`를 실행하고 필요 시 `./gradlew :app:ktlintCheck`를 수행한다.
|
||||||
|
|
||||||
## 작업 계획 문서 규칙 (docs)
|
## 작업 계획 문서 규칙 (docs)
|
||||||
- 모든 구현 작업은 PRD 문서와 구현 계획/TASK 문서가 모두 준비된 뒤에 시작한다.
|
- 모든 구현 작업은 PRD 문서와 구현 계획/TASK 문서가 모두 준비된 뒤에 시작한다.
|
||||||
@@ -38,8 +38,10 @@
|
|||||||
- 각 Task 또는 Phase Gate에는 독립적으로 실행·판정할 수 있는 목표, 시작 조건, 완료 증거, 범위 밖 항목을 명시한다.
|
- 각 Task 또는 Phase Gate에는 독립적으로 실행·판정할 수 있는 목표, 시작 조건, 완료 증거, 범위 밖 항목을 명시한다.
|
||||||
- 각 Task에는 구현 시 생성·수정·확인할 파일 경로와 필요한 인터페이스를 명시한다.
|
- 각 Task에는 구현 시 생성·수정·확인할 파일 경로와 필요한 인터페이스를 명시한다.
|
||||||
- 구현 Task의 실행 단계는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서의 체크박스로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
- 구현 Task의 실행 단계는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서의 체크박스로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
||||||
|
- 테스트 계획은 `app/src/test`의 로직/local unit test로 한정한다. 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest`, `connectedDebugAndroidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 Task·Gate·완료 증거에 포함하지 않는다.
|
||||||
|
- UI 변경의 자동 테스트는 adapter 분기, mapper, formatter, presentation model, 라우팅 같은 입력/출력 로직만 대상으로 하며 레이아웃 속성을 직접 검증하지 않는다.
|
||||||
- 테스트 작성이 현실적으로 불가능한 read-only 리뷰·문서·외부 확인 Task에는 `TDD 예외 사유`와 실행 명령·대조표·수동 확인을 포함한 `대체 검증 방법`을 명시한다.
|
- 테스트 작성이 현실적으로 불가능한 read-only 리뷰·문서·외부 확인 Task에는 `TDD 예외 사유`와 실행 명령·대조표·수동 확인을 포함한 `대체 검증 방법`을 명시한다.
|
||||||
- 각 Phase 또는 Task에는 실행 명령, 기대 결과, 수동 확인 항목 등 검증 기준을 함께 작성한다.
|
- 각 Phase 또는 Task에는 실행 명령과 기대 결과 등 검증 기준을 함께 작성한다. 수동 확인은 사용자가 명시적으로 요청한 경우에만 포함한다.
|
||||||
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||||
- 실제 실행 결과는 샘플의 `Progress` 형식에 맞춰 무엇을/왜/어떻게 수행했는지와 남은 항목을 한국어로 누적한다.
|
- 실제 실행 결과는 샘플의 `Progress` 형식에 맞춰 무엇을/왜/어떻게 수행했는지와 남은 항목을 한국어로 누적한다.
|
||||||
- 특정 phase 또는 task에 직접 대응되는 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)은 해당 phase 또는 task 아래에 `검증 기록`으로 한국어로 남긴다.
|
- 특정 phase 또는 task에 직접 대응되는 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)은 해당 phase 또는 task 아래에 `검증 기록`으로 한국어로 남긴다.
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
- 의존성: 실제 소비 Task에서 필요한 최소 dependency만 추가한다.
|
- 의존성: 실제 소비 Task에서 필요한 최소 dependency만 추가한다.
|
||||||
- 계약: 제공되지 않은 endpoint, DTO, enum, 오류 status/key와 validation 상한을 추정하지 않는다.
|
- 계약: 제공되지 않은 endpoint, DTO, enum, 오류 status/key와 validation 상한을 추정하지 않는다.
|
||||||
- backend 구현 전 UI 확인이 필요하면 제공 계약 기반 explicit mock mode를 사용하고 실제 404 자동 fallback·production mock을 금지하며 mock/server 완료 증거를 분리한다.
|
- backend 구현 전 UI 확인이 필요하면 제공 계약 기반 explicit mock mode를 사용하고 실제 404 자동 fallback·production mock을 금지하며 mock/server 완료 증거를 분리한다.
|
||||||
|
- 테스트 범위: `app/src/test`의 로직/local unit test만 계획한다. 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 Task·Gate·완료 증거에 포함하지 않는다. UI 변경은 화면 표현을 결정하는 입력/출력 로직만 테스트하고 레이아웃 속성을 직접 검증하지 않는다.
|
||||||
- 구현: 모든 구현 Task는 아래 `RED → GREEN → REFACTOR` 순서를 체크박스에 명시하고, 가장 작은 실패 test에서 시작해 최소 구현으로 통과시킨다.
|
- 구현: 모든 구현 Task는 아래 `RED → GREEN → REFACTOR` 순서를 체크박스에 명시하고, 가장 작은 실패 test에서 시작해 최소 구현으로 통과시킨다.
|
||||||
- 검증: focused test에서 시작해 영향받는 package/feature 회귀로 확장한다. 전체 회귀는 공통 경계 변경, 여러 domain/phase 변경,
|
- 검증: focused test에서 시작해 영향받는 package/feature 회귀로 확장한다. 전체 회귀는 공통 경계 변경, 여러 domain/phase 변경,
|
||||||
release/final Gate에서 전체 상태 증거를 별도로 요구하는 경우, targeted test만으로 영향 범위를 판단할 수 없는 실패 또는 사용자
|
release/final Gate에서 전체 상태 증거를 별도로 요구하는 경우, targeted test만으로 영향 범위를 판단할 수 없는 실패 또는 사용자
|
||||||
|
|||||||
Reference in New Issue
Block a user