From 5969f50888f4bc605a4e85ac69db0986d33b8707 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 16 Jun 2026 21:19:03 +0900 Subject: [PATCH] =?UTF-8?q?fix(creator):=20=EC=B1=84=EB=84=90=20=ED=99=88?= =?UTF-8?q?=20=ED=99=9C=EB=8F=99=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item_creator_channel_home_activity.xml | 87 ++++++++--- .../CreatorChannelActivitySourceTest.kt | 144 +++++++++++++++--- .../plan-task.md | 12 +- 3 files changed, 193 insertions(+), 50 deletions(-) diff --git a/app/src/main/res/layout/item_creator_channel_home_activity.xml b/app/src/main/res/layout/item_creator_channel_home_activity.xml index 087fc8ed..53992c9e 100644 --- a/app/src/main/res/layout/item_creator_channel_home_activity.xml +++ b/app/src/main/res/layout/item_creator_channel_home_activity.xml @@ -11,139 +11,176 @@ + android:orientation="vertical" + android:paddingHorizontal="@dimen/spacing_20"> + android:textColor="#939393" /> + android:includeFontPadding="true" /> + android:textColor="#939393" /> + android:letterSpacing="0" + android:lineSpacingMultiplier="1.45" + android:textColor="@color/white" /> + android:textColor="#939393" /> + android:letterSpacing="0" + android:lineSpacingMultiplier="1.45" + android:textColor="@color/white" /> + android:textColor="#939393" /> + android:letterSpacing="0" + android:lineSpacingMultiplier="1.45" + android:textColor="@color/white" /> + android:textColor="#939393" /> + android:letterSpacing="0" + android:lineSpacingMultiplier="1.45" + android:textColor="@color/white" /> + android:textColor="#939393" /> + android:letterSpacing="0" + android:lineSpacingMultiplier="1.45" + android:textColor="@color/white" /> diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt index cc0f187b..4a3df00e 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt @@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelFan import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardHeightDp import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelSeriesCardWidthDp import kr.co.vividnext.sodalive.v2.creator.channel.ui.calculateCreatorChannelScheduleTimelineLineCount +import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelDebutActivityValue import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDate import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleDayOfWeek import kr.co.vividnext.sodalive.v2.creator.channel.ui.formatCreatorChannelScheduleTime @@ -403,18 +404,20 @@ class CreatorChannelActivitySourceTest { "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" ).readText() - assertTrue(adapter.contains("creator_channel_activity_debut")) - assertTrue(adapter.contains("creator_channel_activity_debut_format")) + val activityLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_activity.xml").readText() + + assertTrue(activityLayout.contains("creator_channel_activity_debut")) + assertTrue(adapter.contains("formatCreatorChannelDebutActivityValue")) assertTrue(adapter.contains("creator_channel_activity_live_count_format")) assertTrue(adapter.contains("creator_channel_activity_live_duration_format")) assertTrue(adapter.contains("creator_channel_activity_live_contributor_format")) assertTrue(adapter.contains("creator_channel_activity_audio_count_format")) assertTrue(adapter.contains("creator_channel_activity_series_count_format")) - assertTrue(adapter.contains("creator_channel_activity_live_count")) - assertTrue(adapter.contains("creator_channel_activity_live_duration")) - assertTrue(adapter.contains("creator_channel_activity_live_contributor")) - assertTrue(adapter.contains("creator_channel_activity_audio_count")) - assertTrue(adapter.contains("creator_channel_activity_series_count")) + assertTrue(activityLayout.contains("creator_channel_activity_live_count")) + assertTrue(activityLayout.contains("creator_channel_activity_live_duration")) + assertTrue(activityLayout.contains("creator_channel_activity_live_contributor")) + assertTrue(activityLayout.contains("creator_channel_activity_audio_count")) + assertTrue(activityLayout.contains("creator_channel_activity_series_count")) assertFalse(adapter.contains("addActivityRow(activity.dDay, activity.debutDateUtc.orEmpty())")) assertFalse(adapter.contains("\"Live ")) assertFalse(adapter.contains("\"Audio ")) @@ -496,7 +499,7 @@ class CreatorChannelActivitySourceTest { assertFalse(activityLayout.contains("creator_channel_activity_photo")) assertFalse(activityLayout.contains("bg_round_corner_16_7_222222")) assertTrue(adapter.contains("private val activityDebutValue: TextView?")) - assertTrue(adapter.contains("activityDebutValue?.text = formatDebutActivityValue")) + assertTrue(adapter.contains("activityDebutValue?.text = formatCreatorChannelDebutActivityValue")) assertTrue(adapter.contains("activitySeriesCountValue?.text = itemView.context.getString")) assertFalse(adapter.contains("private fun addActivityRow")) assertFalse(adapter.contains("addActivityRow(")) @@ -656,7 +659,12 @@ class CreatorChannelActivitySourceTest { assertTrue(adapter.contains("private val donationEmpty: View?")) assertTrue(adapter.contains("donationItemsScrollView?.isVisible = item.donations.isNotEmpty()")) assertTrue(adapter.contains("donationEmpty?.isVisible = item.donations.isEmpty()")) - assertTrue(adapter.contains("private fun bindDonations(item: CreatorChannelHomeSection.Donations) {\n donationItems?.removeAllViews()")) + assertTrue( + adapter.contains( + "private fun bindDonations(item: CreatorChannelHomeSection.Donations) {\n" + + " donationItems?.removeAllViews()" + ) + ) assertTrue(adapter.contains("R.layout.item_creator_channel_home_donation_row")) assertTrue(adapter.contains("donationItems?.addView(row)")) assertTrue(adapter.contains("val visibleDonations = item.donations.take(MAX_DONATION_ITEM_COUNT)")) @@ -687,25 +695,25 @@ class CreatorChannelActivitySourceTest { assertTrue(adapter.contains("val isDonationEmptyButtonVisible = !item.isOwner")) assertTrue(adapter.contains("donationButton?.isVisible = isDonationButtonVisible")) assertTrue(adapter.contains("donationEmptyButton?.isVisible = isDonationEmptyButtonVisible")) - assertTrue( - adapter.contains( - "donationButton?.setOnClickListener(if (isDonationButtonVisible) View.OnClickListener { onDonationClick() } else null)" - ) - ) - assertTrue( - adapter.contains( - "donationEmptyButton?.setOnClickListener(if (isDonationEmptyButtonVisible) View.OnClickListener { onDonationClick() } else null)" - ) - ) + assertTrue(adapter.contains("donationButton?.setOnClickListener(")) + assertTrue(adapter.contains("if (isDonationButtonVisible) View.OnClickListener { onDonationClick() } else null")) + assertTrue(adapter.contains("donationEmptyButton?.setOnClickListener(")) + assertTrue(adapter.contains("if (isDonationEmptyButtonVisible) View.OnClickListener { onDonationClick() } else null")) assertTrue(fragment.contains("override fun postChannelDonation(can: Int, isSecret: Boolean, message: String)")) - assertTrue(fragment.contains("viewModel.postChannelDonation(can = can, isSecret = isSecret, message = message)")) + assertTrue( + fragment.contains("viewModel.postChannelDonation(can = can, isSecret = isSecret, message = message)") + ) assertTrue(fragment.contains("host.onCreatorChannelDonationClicked()")) assertTrue(activity.contains("LiveRoomDonationDialog")) assertTrue(activity.contains("isLiveDonation = true")) assertTrue(activity.contains("messageMaxLength = 100")) - assertTrue(activity.contains("secretToggleLabelResId = R.string.screen_user_profile_channel_donation_secret")) + assertTrue( + activity.contains("secretToggleLabelResId = R.string.screen_user_profile_channel_donation_secret") + ) assertTrue(activity.contains("applySecretMissionMessageHint = false")) - assertTrue(activity.contains("homeActionDelegate?.postChannelDonation(can = can, isSecret = isSecret, message = message)")) + assertTrue( + activity.contains("homeActionDelegate?.postChannelDonation(can = can, isSecret = isSecret, message = message)") + ) assertTrue(activity.contains("dialog.show(screenWidth - 26.7f.dpToPx().toInt())")) } @@ -992,6 +1000,30 @@ class CreatorChannelActivitySourceTest { assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)")) } + @Test + fun `Phase 12 오디오 컨텐츠는 표시 개수만큼 높이를 차지하고 free tag는 wrap content를 사용한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val audioItemLayout = projectFile( + "app/src/main/res/layout/item_creator_channel_home_audio_content.xml" + ).readText() + + assertTrue(audioItemLayout.contains("android:id=\"@+id/tv_audio_content_free_tag\"")) + assertTrue(audioItemLayout.contains("android:layout_width=\"wrap_content\"")) + assertFalse( + audioItemLayout.contains( + "android:id=\"@+id/tv_audio_content_free_tag\"\n" + + " style=\"@style/Typography.Body6\"\n" + + " android:layout_width=\"34dp\"" + ) + ) + assertTrue(adapter.contains("private fun updateAudioContentsGridSpan(itemCount: Int)")) + assertTrue(adapter.contains("spanCount = itemCount.coerceIn(")) + assertTrue(adapter.contains("AUDIO_GRID_SPAN_COUNT")) + assertTrue(adapter.contains("updateAudioContentsGridSpan(visibleAudioContents.size)")) + } + @Test fun `시리즈 섹션은 Figma Contents series size m 카드 row로 렌더링한다`() { val adapter = projectFile( @@ -1018,7 +1050,7 @@ class CreatorChannelActivitySourceTest { assertTrue(seriesItemLayout.contains("@+id/tv_series_original_text")) assertTrue(seriesItemLayout.contains("android:text=\"Only\"")) assertTrue(seriesItemLayout.contains("@font/phosphate_solid")) - assertTrue(seriesItemLayout.contains("android:layout_width=\"70dp\"")) + assertTrue(seriesItemLayout.contains("android:layout_width=\"wrap_content\"")) assertTrue(seriesItemLayout.contains("@drawable/bg_series_original_tag")) assertTrue(seriesItemLayout.contains("@drawable/ic_series_original")) assertTrue(seriesItemLayout.contains("android:layout_width=\"163dp\"")) @@ -1079,6 +1111,72 @@ class CreatorChannelActivitySourceTest { assertEquals(206, calculateCreatorChannelSeriesCardHeightDp(146)) } + @Test + fun `Phase 12 시리즈 original tag는 wrap content sizing과 상세 이동 계약을 사용한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt" + ).readText() + val seriesItemLayout = projectFile( + "app/src/main/res/layout/item_creator_channel_home_series_content.xml" + ).readText() + + assertTrue(seriesItemLayout.contains("android:id=\"@+id/layout_series_original_tag\"")) + assertTrue(seriesItemLayout.contains("android:layout_width=\"wrap_content\"")) + assertTrue(seriesItemLayout.contains("android:layout_height=\"wrap_content\"")) + assertTrue(seriesItemLayout.contains("android:paddingStart=\"8dp\"")) + assertTrue(seriesItemLayout.contains("android:paddingEnd=\"8dp\"")) + assertTrue(seriesItemLayout.contains("android:minHeight=\"24dp\"")) + assertFalse(seriesItemLayout.contains("android:layout_width=\"70dp\"")) + assertTrue(fragment.contains("onSeriesClick = ::onSeriesClicked")) + assertTrue(fragment.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)")) + assertTrue(fragment.contains("host.onCreatorChannelSeriesClicked(series)")) + assertTrue(source.contains("import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity")) + assertTrue(source.contains("private fun onSeriesClicked(series: CreatorChannelSeriesResponse)")) + assertTrue(source.contains("SeriesDetailActivity::class.java")) + assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, series.seriesId)")) + } + + @Test + fun `Phase 12 활동 영역은 Figma label value typography와 debut 날짜 포맷을 사용한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val activityLayout = projectFile("app/src/main/res/layout/item_creator_channel_home_activity.xml").readText() + + assertTrue(activityLayout.contains("@layout/view_section_title")) + assertTrue(activityLayout.contains("style=\"@style/Typography.Body2\"")) + assertTrue(activityLayout.contains("android:lineSpacingMultiplier=\"1.45\"")) + assertTrue(activityLayout.contains("android:letterSpacing=\"0\"")) + assertTrue(activityLayout.contains("android:textColor=\"#939393\"")) + assertTrue(activityLayout.contains("android:textColor=\"@color/white\"")) + assertTrue(activityLayout.contains("android:layout_marginEnd=\"@dimen/spacing_8\"")) + assertTrue(activityLayout.contains("android:layout_marginBottom=\"@dimen/spacing_8\"")) + assertTrue(adapter.contains("formatCreatorChannelDebutActivityValue(")) + assertFalse( + adapter.contains("itemView.context.getString(R.string.creator_channel_activity_debut_format, debutDate, dDay)") + ) + + assertEquals( + "2026.06.11(D+1)", + formatCreatorChannelDebutActivityValue( + "2026-06-10T15:00:00Z", + "D+1", + TimeZone.getTimeZone("Asia/Seoul") + ) + ) + assertEquals( + "D+1", + formatCreatorChannelDebutActivityValue(null, "D+1", TimeZone.getTimeZone("Asia/Seoul")) + ) + assertEquals( + "D+1", + formatCreatorChannelDebutActivityValue("invalid", "D+1", TimeZone.getTimeZone("Asia/Seoul")) + ) + } + @Test fun `커뮤니티 섹션은 Figma feed card 3개와 전체보기 capsule로 렌더링한다`() { val adapter = projectFile( diff --git a/docs/20260611_크리에이터_채널_홈_탭/plan-task.md b/docs/20260611_크리에이터_채널_홈_탭/plan-task.md index 7b48abd8..47c7e619 100644 --- a/docs/20260611_크리에이터_채널_홈_탭/plan-task.md +++ b/docs/20260611_크리에이터_채널_홈_탭/plan-task.md @@ -1257,7 +1257,7 @@ - [x] `LiveRoomDonationDialog`가 validation/dismiss/charge navigation을 소유하고 `UserProfileActivity`도 callback-only 패턴을 쓰므로, Activity dismiss 흐름은 변경하지 않는다. - [x] 로컬 can 차감은 전체 balance resync가 아니라 최소 안전 장치로 `(SharedPreferenceManager.can - can).coerceAtLeast(0)`만 적용한다. -- [ ] **Task 12.2: 오디오/시리즈 아이템 세부 UI와 터치 액션 보강** +- [x] **Task 12.2: 오디오/시리즈 아이템 세부 UI와 터치 액션 보강** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` - `app/src/main/res/layout/item_creator_channel_home_audio.xml` @@ -1279,8 +1279,12 @@ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` + - 검증 기록: + - 2026-06-16: Figma `296:14895`와 시리즈 `Contents(type=series, size=m)` 구조를 확인해 오디오 free tag와 시리즈 original tag sizing 기준을 재확인했다. RED로 `CreatorChannelActivitySourceTest`에 오디오 `tv_audio_content_free_tag` `wrap_content`, audio grid span count 동적 보정, 시리즈 original tag `wrap_content`/padding, 시리즈 상세 이동 계약을 추가했고, 최초 실행은 `formatCreatorChannelDebutActivityValue` 미구현 컴파일 실패로 RED를 확인했다. + - 2026-06-16: `item_creator_channel_home_audio_content.xml`의 free tag width를 `wrap_content`로 변경하되 `minWidth=34dp`와 padding을 유지했다. `CreatorChannelHomeSectionAdapter`는 표시되는 오디오 개수에 맞춰 `GridLayoutManager.spanCount = itemCount.coerceIn(1, AUDIO_GRID_SPAN_COUNT)`로 보정해 1개 항목에서 3개 row 높이를 예약하지 않게 했다. `item_creator_channel_home_series_content.xml`의 original tag는 width/height `wrap_content`, `minHeight=24dp`, horizontal padding으로 조정하고, `CreatorChannelHomeFragment`/`CreatorChannelActivity`에 `CreatorChannelSeriesResponse` click forwarding과 `SeriesDetailActivity` + `Constants.EXTRA_SERIES_ID` 이동을 연결했다. + - 2026-06-16: 검증 중 기존 source assertion의 과거 `70dp` tag width 계약과 줄바꿈 전 문자열 계약이 Phase 12 구현과 충돌해 테스트를 새 계약 기준으로 갱신했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 PASS했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation warning과 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다. -- [ ] **Task 12.3: 활동 영역 Figma 정합성과 데뷔 날짜/D+n 버그 수정** +- [x] **Task 12.3: 활동 영역 Figma 정합성과 데뷔 날짜/D+n 버그 수정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt` - `app/src/main/res/layout/item_creator_channel_home_activity.xml` @@ -1302,6 +1306,10 @@ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` + - 검증 기록: + - 2026-06-16: Figma `296:15001` 스크린샷/metadata를 확인해 활동 섹션이 `SectionTitle(활동)`, label/value 16sp medium row, label `#939393`, value white, label/value 8dp gap, row 간 8dp 구조임을 반영했다. `item_creator_channel_home_activity.xml`의 각 label/value에 `Typography.Body2`, `lineSpacingMultiplier=1.45`, `letterSpacing=0`, label `#939393`, value `@color/white`, label `marginEnd=8dp`를 적용했다. + - 2026-06-16: `formatCreatorChannelDebutActivityValue(debutDateUtc, dDay, timeZone, locale)` helper를 추가해 UTC ISO 날짜를 `yyyy.MM.dd`로 변환하고 `yyyy.MM.dd(D+n)` 형식으로 표시하도록 수정했다. `debutDateUtc`가 null/blank이거나 파싱 실패해도 `dDay`가 누락되지 않도록 `dDay`를 fallback으로 반환한다. + - 2026-06-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 PASS했다. 최초 `ktlintCheck`는 새/기존 source assertion과 adapter 긴 줄로 실패했으나 줄바꿈만 보정한 뒤 재실행해 PASS했다. ---