docs(creator): 채널 표시 정리 문서를 완료한다

This commit is contained in:
2026-08-26 16:30:16 +09:00
parent 1f88d721d7
commit 10886153b2
3 changed files with 701 additions and 0 deletions
@@ -0,0 +1,492 @@
# 크리에이터 채널 표시와 상대 시간 다국어 정리 구현 계획
> **For agentic workers:** 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 Task별로 실행한다. 사용자가 다중 에이전트 실행을 명시하지 않으면 현재 세션에서 순차 실행한다.
**Goal:** 크리에이터 채널의 팬레터·후원 표시 결함을 바로잡고 모든 `v2` 크리에이터 채널 상대 시간을 현재 화면 locale의 공통 formatter 경로로 통일한다.
**Architecture:** API의 raw UTC를 presentation model까지 유지하고 adapter 또는 Activity가 `formatUtcRelativeTimeText(context, rawUtc)`를 호출한다. 새 formatter abstraction은 만들지 않으며 shared DI 파일은 `v2` ViewModel 등록 인자만 최소 수정하고 기존 formatter provider와 레거시 등록은 유지한다.
**Tech Stack:** Kotlin, Android ViewBinding, RecyclerView, Koin, JUnit4, Robolectric
**Spec:** `docs/20260826_크리에이터_채널_표시와_상대_시간_다국어_정리/prd.md`
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 완료 |
| 작성일 | 2026-08-26 |
| 요구사항 기준 | `docs/20260826_크리에이터_채널_표시와_상대_시간_다국어_정리/prd.md` |
| API 기준 | 변경 없음 |
| 현재 Phase | 완료 |
| 현재 활성 Goal | 없음 |
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | `3/3` | 완료 | 없음 |
| 2 | 완료 | `3/3` | 완료 | 없음 |
## 범위
### 포함
- 팬레터 item `신고`의 한국어·영어·일본어 bind-time locale 적용
- 팬레터·후원 empty 상태의 count bar 비노출 회귀 검증
- 후원 item 우측 상단 badge의 캔 단위 제거
- 팬레터·커뮤니티·후원·채널 홈 상대 시간의 표시 시점 공통 formatter 통합
- `app/src/test` local unit test, resource merge, Kotlin compile, ktlint 검증
### 제외
- 레거시 화면의 상대 시간 호출부와 `RelativeTimeFormatter.kt` 변경
- 절대 시각 formatter, API/DTO, 신고 API, pagination과 empty 안내 UI 변경
- 후원 빈 message fallback의 캔 단위 제거
- 신규 dependency와 신규 formatter abstraction
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷과 시각 QA
## 전역 제약
- 모든 production 변경은 `v2` 크리에이터 채널 동작과 그 ViewModel DI 등록에 한정한다.
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt``v2` ViewModel 생성자의 제거된 formatter 인자만 수정한다.
- `single<UtcRelativeTimeTextFormatter>` provider와 레거시 ViewModel 등록은 변경하지 않는다.
- 기존 `formatUtcRelativeTimeText(Context, String?)`를 재사용하고 파싱·구간·fallback 정책을 바꾸지 않는다.
- locale 문자열과 상대 시간은 application Context에서 미리 만들지 않고 실제 View 또는 Activity Context에서 만든다.
- 현재 source가 이미 만족하는 empty 계약은 검증 결과가 실패하지 않으면 production code를 수정하지 않는다.
- 구현 Task는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서를 지킨다.
- focused test부터 실행하고 Phase Gate에서 직접 영향 package 회귀로 확장한다.
- 새 Kotlin test 이름은 한글 backtick 문장으로 작성한다.
- View 크기·margin·constraint·visibility 속성을 직접 검사하는 신규 test를 만들지 않는다.
---
## Phase 1: 팬레터·후원 표시 계약 정리
**Phase 결과:** 팬레터 `신고`, empty count와 후원 badge 수량이 확정된 표시 계약을 따른다.
**선행조건:** PRD `DEC-001`~`DEC-005` 확인.
**Phase 완료 조건:** `P1-T1`~`P1-T3``P1-GATE` 완료, Task별 검증 기록 누적.
### 구현 항목
#### Task 1.1 팬레터 `신고` bind-time locale 적용
**Goal 실행 `P1-T1`:** 재사용 View가 이전 언어 text를 가지고 있어도 현재 item Context의 `신고` 문자열로 다시 바인딩한다.
- **시작 조건:** `FAN-001`, `LOC-001`, `LOC-002` 확정.
- **완료 증거:** 영어·일본어 adapter 재바인딩 test와 팬레터 action 회귀 test PASS.
- **범위 밖:** 신고 click, dialog, API와 다른 action label 변경.
**Files:**
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkAdapterLocaleTest.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
- Modify: `docs/agent-guides/build-test-style.md`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkActionTest.kt`
**Interfaces:**
- Consumes: `CreatorChannelFanTalkRightAction.Report`, `R.string.creator_channel_fantalk_report`
- Produces: `CreatorChannelFanTalkAdapter.ViewHolder.bind()`가 현재 `binding.root.context`의 report text를 매번 적용하는 계약
- [x] **RED:** 영어·일본어 Context에서 holder의 report text를 한국어 `신고`로 오염시킨 뒤 `bind()`가 각각 `Report`, `通報`로 복원하는 adapter test를 작성한다.
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkAdapterLocaleTest"`를 실행해 기존 `bindRightAction()`이 text를 재설정하지 않아 assertion이 실패하는지 확인한다.
```kotlin
tvCreatorChannelFantalkReport.text = root.context.getString(R.string.creator_channel_fantalk_report)
```
- [x] **GREEN:** report 분기에서 아래 한 줄을 추가하고 기존 visibility·listener 분기를 유지한다.
- 검증 기록: `CreatorChannelFanTalkAdapter.kt:66`에서 `root.context.getString(R.string.creator_channel_fantalk_report)`로 매 bind 재설정한다.
- [x] **GREEN 확인:** 같은 focused test와 `CreatorChannelFanTalkActionTest`를 실행해 locale text와 신고 action 계약이 PASS하는지 확인한다.
- [x] **REFACTOR:** 새 helper를 만들지 않고 import·test fixture 중 이번 Task가 만든 중복만 정리한다. 신규 test class의 단일 실행 예시를 `build-test-style.md`에 추가한 뒤 `./gradlew :app:ktlintCheck` 결과를 기록한다.
- 검증 기록: RED는 `expected:<[Report]> but was:<[신고]>`로 실패했다. GREEN은 `CreatorChannelFanTalkAdapterLocaleTest`, `CreatorChannelFanTalkActionTest`, `./gradlew :app:ktlintCheck` 모두 `BUILD SUCCESSFUL`이다. 사용자 수동 확인 결과 신고 버튼 문구 변경도 확인됐다. Spec review와 quality review 모두 blocking 이슈 없이 PASS했다.
#### Task 1.2 팬레터·후원 empty count 계약 회귀 확인
**Goal 실행 `P1-T2`:** 표시 가능한 첫 페이지 item이 없을 때 두 탭이 `Empty` 상태를 유지해 count bar를 표시하지 않는 계약을 확인한다.
- **시작 조건:** `FAN-002`, `DON-001`, `DEC-004` 확정.
- **완료 증거:** 기존 ViewModel empty 경계 test PASS와 Fragment `bindEmpty()` count bar 비노출 source 대조 기록.
- **범위 밖:** empty illustration, 안내 문구, 버튼, ranking 표시와 content count bar 삭제.
- **TDD 예외 사유:** 현재 source와 기존 test가 count 0·빈 목록의 `Empty` 상태를 이미 구현한다. 실패 재현 없이 동일 production 분기를 다시 작성하면 중복 변경이 된다.
- **대체 검증 방법:** 아래 test와 source 대조를 실행한다. 하나라도 실패하면 production code를 바로 수정하지 않고 실패 경로, 대상 파일, 회귀 test를 포함한 신규 수정 Task를 이 Phase에 먼저 추가한다.
- [x] `CreatorChannelFanTalkViewModelTest`의 count 0, 빈 목록, count 양수·빈 목록 test를 실행한다.
- [x] `CreatorChannelDonationViewModelTest`의 count 0과 빈 목록 test를 실행한다.
- [x] `CreatorChannelFanTalkFragment.bindEmpty()``CreatorChannelDonationFragment.bindEmpty()`가 각각 count bar container 전체를 숨기는지 source로 대조한다.
- [x] content 상태에서는 기존 `전체 N` count bar를 유지하는지 source로 대조한다.
검증 기록:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`.
- `CreatorChannelFanTalkFragment.bindEmpty()``layoutCreatorChannelFantalkCountBar.isVisible = false`, `bindContent()``true`로 대조했다.
- `CreatorChannelDonationFragment.bindEmpty()``layoutCreatorChannelDonationCountBar.isVisible = false`, `bindContent()``true`로 대조했다.
검증 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"
rg -n "bindEmpty|CountBar.isVisible" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragment.kt
```
**Expected:** 두 ViewModel test PASS, 두 `bindEmpty()`에서 count bar `false`, 두 `bindContent()`에서 `true`가 확인된다.
#### Task 1.3 후원 badge 캔 단위 제거
**Goal 실행 `P1-T3`:** 후원 item 우측 상단 badge가 한국어·영어·일본어에서 수량 숫자만 표시한다.
- **시작 조건:** `DON-002`, `DON-003` 확정.
- **완료 증거:** binder locale test가 세 locale 모두 `1,000`을 반환하고 fallback message 회귀가 유지됨.
- **범위 밖:** badge icon·색상·크기, 후원 message와 ranking 수량 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelDonationCardBinder.kt`
- Modify: `app/src/main/res/layout/item_creator_channel_donation.xml`
- Modify: `app/src/main/res/values/strings.xml`
- Modify: `app/src/main/res/values-en/strings.xml`
- Modify: `app/src/main/res/values-ja/strings.xml`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
**Interfaces:**
- Consumes: `Int.moneyFormat()`
- Produces: `bindCreatorChannelDonationCard(..., can: Int, ...)``canView.text == can.moneyFormat()` 계약
- [x] **RED:** 기존 `후원 can binder는 한국어 영어 일본어에서 1,000 단위 표시를 사용한다` test를 `세 locale에서 1,000 숫자만 표시한다` 계약으로 바꾸고 expected를 모두 `1,000`으로 변경한다.
- [x] **RED 확인:** `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest.후원 can binder는 한국어 영어 일본어에서 1,000 숫자만 표시한다"`를 실행해 기존 unit suffix 때문에 실패하는지 확인한다.
```kotlin
canView.text = can.moneyFormat()
```
- [x] **GREEN:** binder를 아래 최소 코드로 변경하고 layout preview도 `tools:text="20"`으로 맞춘다.
- 검증 기록: `CreatorChannelDonationCardBinder.kt``canView.text = can.moneyFormat()`만 사용하고 `item_creator_channel_donation.xml` preview는 `tools:text="20"`이다.
- [x] **GREEN:** 더 이상 참조되지 않는 `creator_channel_donation_can_format`을 한국어·영어·일본어 resource에서 제거하되 `creator_channel_donation_fallback_message`는 유지한다.
- [x] **GREEN 확인:** focused test와 `CreatorChannelDonationActionTest`, `CreatorChannelDonationMapperTest`를 실행해 badge와 fallback message 계약이 PASS하는지 확인한다.
- [x] **REFACTOR:** 제거된 resource 참조가 0건인지 `rg -n "creator_channel_donation_can_format" app/src`로 확인하고 이번 Task가 만든 unused import만 정리한다.
- 검증 기록: RED는 `expected:<1,000[]> but was:<1,000[캔]>`로 실패했다. GREEN은 `CreatorChannelDonationFragmentLayoutTest`, `CreatorChannelDonationActionTest`, `CreatorChannelDonationMapperTest` 모두 `BUILD SUCCESSFUL`이다. `rg -n "creator_channel_donation_can_format" app/src` 결과 0건이며 `git diff --check`도 출력 없이 PASS했다. Spec review와 quality review 모두 blocking 이슈 없이 PASS했다.
### Phase 1 Gate
**Goal 실행 `P1-GATE`:** 팬레터·후원 표시 계약과 직접 영향 회귀를 판정한다.
- **시작 조건:** `P1-T1`~`P1-T3` 완료.
- **완료 증거:** 아래 명령 PASS와 실제 결과 기록.
- **범위 밖:** 상대 시간 presentation data 전환.
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"
./gradlew :app:mergeDebugResources
./gradlew :app:compileDebugKotlin
./gradlew :app:ktlintCheck
git diff --check
```
**Expected:** 모든 명령 exit code 0, 팬레터 report locale·empty 상태·후원 숫자 badge·fallback message 회귀 PASS.
검증 기록:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`.
- `git diff --check` 실행 결과 출력 없이 PASS.
---
## Phase 2: `v2` 크리에이터 채널 상대 시간 표시 통합
**Phase 결과:** 팬레터·커뮤니티·후원·채널 홈의 상대 시간이 raw UTC와 현재 화면 Context를 사용해 공통 formatter 한 경로로 표시된다.
**선행조건:** `P1-GATE` 완료.
**Phase 완료 조건:** `P2-T1`~`P2-T3``P2-GATE` 완료, Task별 검증 기록 누적.
### 구현 항목
#### Task 2.1 팬레터 원글·답글·상세 bind-time 상대 시간 전환
**Goal 실행 `P2-T1`:** 팬레터 원글과 답글의 raw UTC를 목록·상세 View Context에서 공통 formatter로 표시한다.
- **시작 조건:** `TIME-001`, `TIME-002`, `LOC-001`, `P1-GATE` 완료.
- **완료 증거:** mapper raw UTC test, 목록·상세 한국어·영어·일본어 test와 FanTalk 회귀 PASS.
- **범위 밖:** FanTalk API, 신고·수정·삭제·답글 action 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModel.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailUiModels.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailActivity.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailViewModel.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` — FanTalk `v2` ViewModel 등록 인자만 수정
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModelTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkAdapterLocaleTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailViewModelTest.kt`
**Interfaces:**
- Produces: `CreatorChannelFanTalkUiModel.createdAtUtc: String`
- Produces: `CreatorChannelFanTalkReplyUiModel.createdAtUtc: String`
- Produces: FanTalk detail parcel payload의 `createdAtUtc: String`
- Consumes: `formatUtcRelativeTimeText(binding.root.context, item.createdAtUtc)`
- [x] **RED:** mapper가 원글·답글 `createdAtUtc`를 변경하지 않고 보존하는 test와 영어·일본어 목록/상세 Context에서 locale 상대 시간을 반환하는 test를 작성한다.
- [x] **RED 확인:** FanTalk mapper·adapter·detail focused test를 실행해 raw UTC property와 bind-time formatting 부재로 실패하는지 확인한다.
- [x] **GREEN:** UI model과 detail parcel payload의 `createdAtText``createdAtUtc`로 교체하고 mapper에서 formatter 호출 없이 response 값을 전달한다.
- [x] **GREEN:** 목록 adapter와 상세 Activity의 원글·답글 time View에 아래 형식으로 값을 설정한다.
```kotlin
timeView.text = formatUtcRelativeTimeText(timeView.context, item.createdAtUtc)
```
- [x] **GREEN:** `CreatorChannelFanTalkViewModel``CreatorChannelFanTalkDetailViewModel``UtcRelativeTimeTextFormatter` 생성자 의존성을 제거하고 `AppDI.kt`의 두 `v2` 등록에서 해당 `get()`만 제거한다.
- [x] **GREEN 확인:** FanTalk focused test와 detail/action/pagination 회귀를 실행해 locale 출력과 기존 action이 PASS하는지 확인한다.
- [x] **REFACTOR:** FanTalk package에서 `relativeTimeTextFormatter.format`과 display용 `createdAtText` 참조가 0건인지 확인하고 이번 Task가 만든 unused import만 제거한다.
- 검증 기록: RED는 `createdAtUtc` 미존재와 제거 예정 생성자 인자로 `:app:compileDebugUnitTestKotlin FAILED`가 발생했다. GREEN은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` 실행 결과 `BUILD SUCCESSFUL`이다. `rg -n "relativeTimeTextFormatter\.format|createdAtText" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk` 결과 출력 없이 0건이다. Spec review는 PASS했다. Quality review가 `P1-T1`의 신고 문구 변경을 `P2-T1` 범위 외 diff로 지적했으나, 해당 변경은 이미 `P1-T1`에서 승인·검증된 요구사항이므로 `P2-T1` blocking 이슈로 채택하지 않았다.
검증 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"
rg -n "relativeTimeTextFormatter\.format|createdAtText" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk
```
**Expected:** FanTalk test PASS, 검색 결과에 상대 시간 사전 포맷 field·호출 0건.
#### Task 2.2 커뮤니티 게시글·댓글·답글 bind-time 상대 시간 전환
**Goal 실행 `P2-T2`:** 커뮤니티 게시글·댓글·답글의 raw UTC를 목록·상세·답글 화면 Context에서 공통 formatter로 표시한다.
- **시작 조건:** `P2-T1` 완료, `TIME-001`, `TIME-002`, `LOC-001` 확정.
- **완료 증거:** mapper/ViewModel raw UTC test, list/detail/comment/reply locale test와 Community 회귀 PASS.
- **범위 밖:** community image, paywall, reaction, comment action과 API 변경.
**Files:**
- 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/CreatorChannelCommunityViewModel.kt`
- 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/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/community/detail/ui/CreatorChannelCommunityCommentAdapter.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyViewModel.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyActivity.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/CreatorChannelCommunityReplyAdapter.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` — Community `v2` ViewModel 등록 인자만 수정
- Modify: `docs/agent-guides/build-test-style.md`
- 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/CreatorChannelCommunityViewModelTest.kt`
- 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/CreatorChannelCommunityReplyViewModelTest.kt`
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityRelativeTimeLocaleTest.kt`
**Interfaces:**
- Produces: `CreatorChannelCommunityPostUiModel.createdAtUtc: String`
- Produces: `CreatorChannelCommunityPostDetailUiModel.createdAtUtc: String`
- Produces: comment/reply model의 기존 `createdAtUtc: String`만 표시 입력으로 사용
- Removes: comment/reply의 display용 `createdAtText``EXTRA_CREATED_AT_TEXT`
- Consumes: `formatUtcRelativeTimeText(view.context, createdAtUtc)`
- [x] **RED:** list post·detail post·comment·reply가 raw UTC를 보존하고 영어·일본어 View Context에서 공통 formatter 결과를 표시하는 test를 작성한다.
- [x] **RED 확인:** Community mapper·detail·reply·locale focused test를 실행해 mapper/ViewModel 사전 포맷과 adapter 직접 text 대입 때문에 실패하는지 확인한다.
- [x] **GREEN:** list/detail UI model을 `createdAtUtc` 중심으로 변경하고 comment/reply model의 `createdAtText` field를 제거한다.
- [x] **GREEN:** mapper와 세 ViewModel에서 `relativeTimeTextFormatter.format()` 호출 및 formatter 생성자 의존성을 제거한다.
- [x] **GREEN:** list adapter, detail Activity, comment adapter, reply Activity와 reply adapter가 각 time View의 Context로 공통 formatter를 호출한다.
- [x] **GREEN:** reply Activity navigation에서 `EXTRA_CREATED_AT_TEXT`를 제거하고 기존 `EXTRA_CREATED_AT_UTC`만 전달한다.
- [x] **GREEN:** `AppDI.kt`의 Community list/detail/reply `v2` ViewModel 등록에서 제거된 formatter `get()`만 삭제한다.
- [x] **GREEN 확인:** Community focused test와 action/pagination 회귀를 실행해 locale 출력과 기존 기능이 PASS하는지 확인한다.
- [x] **REFACTOR:** Community package에서 `relativeTimeTextFormatter.format`, display용 `createdAtText`, `EXTRA_CREATED_AT_TEXT`가 0건인지 확인하고 이번 Task가 만든 unused import만 제거한다. 신규 locale test class의 단일 실행 예시를 `build-test-style.md`에 추가한다.
- 검증 기록: RED는 제거 예정 생성자·field·extra 경계에서 `No value passed for parameter 'relativeTimeTextFormatter'`, `Unresolved reference 'createdAtUtc'`, `No parameter with name 'createdAtUtc' found`, `No value passed for parameter 'createdAtText'``:app:compileDebugUnitTestKotlin FAILED`가 발생했다. GREEN은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"` 실행 결과 `BUILD SUCCESSFUL`이다. `rg -n "relativeTimeTextFormatter\.format|createdAtText|EXTRA_CREATED_AT_TEXT" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community` 결과 출력 없이 0건이다. Spec review와 quality review 모두 blocking 이슈 없이 PASS했다.
검증 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"
rg -n "relativeTimeTextFormatter\.format|createdAtText|EXTRA_CREATED_AT_TEXT" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community
```
**Expected:** Community test PASS, 검색 결과에 상대 시간 사전 포맷 field·호출·extra 0건.
#### Task 2.3 후원 의존성 정리와 채널 전체 호출부 audit
**Goal 실행 `P2-T3`:** 후원 mapper의 사용하지 않는 formatter/context 의존성을 제거하고 `v2` 크리에이터 채널 상대 시간 표시부 전체를 audit한다.
- **시작 조건:** `P2-T2` 완료, `TIME-003`, `TIME-004` 확정.
- **완료 증거:** 후원 mapper/ViewModel test, 채널 홈 회귀와 호출부 검색 결과 PASS.
- **범위 밖:** 채널 홈의 이미 정상인 bind-time formatter 호출 재작성.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationMappers.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationUiModels.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModel.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragment.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` — Donation `v2` ViewModel 등록 인자만 수정
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationMapperTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModelTest.kt`
- Verify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- Verify: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationAdapter.kt`
**Interfaces:**
- Produces: `List<CreatorChannelDonationResponse>.toDonationUiModels()` 무인자 mapper
- Preserves: Donation adapter와 Home section adapter의 `formatUtcRelativeTimeText(view.context, rawUtc)` 호출
- [x] **RED:** `CreatorChannelDonationMapperTest`를 무인자 mapper가 raw `createdAtUtc`를 그대로 presentation data에 보존하는 계약으로 갱신한다.
- [x] **RED 확인:** Donation mapper focused test를 실행해 기존 context·formatter 인자 계약 때문에 실패하는지 확인한다.
- [x] **GREEN:** Donation mapper의 사용하지 않는 `Context``UtcRelativeTimeTextFormatter` 인자를 제거하고 ViewModel 호출을 무인자로 변경한다.
- [x] **GREEN:** Donation ViewModel의 formatter와 application Context 의존성을 제거하고 `AppDI.kt`의 Donation `v2` 등록에서 해당 `get()`만 제거한다.
- [x] **GREEN 확인:** Donation mapper/ViewModel/adapter test를 실행해 raw UTC와 bind-time 공통 formatter 계약이 PASS하는지 확인한다.
- [x] **REFACTOR:** 아래 inventory 명령으로 `v2` 크리에이터 채널에 사전 포맷 호출이 없고 모든 상대 시간 View가 공통 함수를 사용하는지 확인한다. 이미 정상인 채널 홈 코드는 수정하지 않는다.
- 검증 기록: RED는 old mapper 인자의 `No value passed for parameter 'context'`, `No value passed for parameter 'relativeTimeTextFormatter'`, 신규 field의 `Unresolved reference 'createdAtUtc'``:app:compileDebugUnitTestKotlin FAILED`가 발생했다. GREEN은 Donation mapper focused test와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeMapperTest"` 실행 결과 `BUILD SUCCESSFUL`이다. `rg -n "UtcRelativeTimeTextFormatter|relativeTimeTextFormatter\.format" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 결과 출력 없이 0건이다. `rg -n "formatUtcRelativeTimeText" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 결과에는 FanTalk 목록·상세, Community 목록·상세·댓글·답글, Donation adapter, Home adapter 표시부가 포함된다. Spec review와 quality review 모두 blocking 이슈 없이 PASS했다.
```bash
rg -n "UtcRelativeTimeTextFormatter|relativeTimeTextFormatter\.format" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel
rg -n "formatUtcRelativeTimeText" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel
```
**Expected:** 첫 번째 검색 결과 0건. 두 번째 검색 결과에 팬레터 목록·상세, 커뮤니티 목록·상세·댓글·답글, 후원과 채널 홈 표시부가 모두 포함된다.
### Phase 2 Gate
**Goal 실행 `P2-GATE`:** `v2` 크리에이터 채널 상대 시간 통합과 공통 formatter 회귀를 최종 판정한다.
- **시작 조건:** `P2-T1`~`P2-T3` 완료.
- **완료 증거:** 아래 명령과 호출부 inventory PASS, 실제 결과를 Progress와 Verification Log에 누적.
- **범위 밖:** 레거시 화면과 절대 시각 formatter.
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.RelativeTimeFormatterTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"
./gradlew :app:mergeDebugResources
./gradlew :app:compileDebugKotlin
./gradlew :app:ktlintCheck
git diff --check
```
**Expected:** 모든 명령 exit code 0, 한국어·영어·일본어 locale test PASS, 레거시 formatter와 호출부 diff 0건.
검증 기록:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.RelativeTimeFormatterTest"` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`.
- `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`.
- 최초 `./gradlew :app:ktlintCheck``CreatorChannelCommunityRelativeTimeLocaleTest.kt`의 50, 53라인 max line length 위반으로 실패했다. 줄바꿈만 수정한 뒤 `CreatorChannelCommunityRelativeTimeLocaleTest``./gradlew :app:ktlintCheck`를 재실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- `git diff --check` 실행 결과 출력 없이 PASS.
- `rg -n "UtcRelativeTimeTextFormatter|relativeTimeTextFormatter\.format" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 결과 출력 없이 0건.
- `rg -n "formatUtcRelativeTimeText" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 결과 FanTalk 목록·상세, Community 목록·상세·댓글·답글, Donation adapter, Home adapter 표시부가 모두 포함됐다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 없음 | 아니요 | locale test fixture와 실제 binding Context 대조 |
| 2 | `P1-T2` | 없음 | 예 | 실패 시 신규 회귀 수정 Task를 먼저 추가 |
| 3 | `P1-T3` | 없음 | 예 | badge와 fallback message 범위를 재대조 |
| 4 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 실패 소유 Task 보정 |
| 5 | `P2-T1` | `P1-GATE` | 아니요 | FanTalk raw UTC payload 경계 보정 |
| 6 | `P2-T2` | `P2-T1` | 아니요 | Community navigation/raw UTC 경계 보정 |
| 7 | `P2-T3` | `P2-T2` | 아니요 | 누락 호출부를 소유 Task에 추가 |
| 8 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 생성 |
```text
P1-T1 ─┐
P1-T2 ─┼→ P1-GATE → P2-T1 → P2-T2 → P2-T3 → P2-GATE
P1-T3 ─┘
```
## 변경 금지 항목
- `RelativeTimeFormatter.kt`의 파싱·구간·fallback 정책을 변경하지 않는다.
- 레거시 화면의 formatter와 호출부를 수정하지 않는다.
- `AppDI.kt`의 formatter singleton과 `v2` 크리에이터 채널 외 등록을 수정하지 않는다.
- content 상태의 `전체 N` count bar를 삭제하지 않는다.
- 후원 fallback message의 단위를 제거하지 않는다.
- 제공된 API/DTO field 이름과 값을 변경하지 않는다.
- test를 삭제·skip·완화하거나 신규 dependency를 추가하지 않는다.
- 요청 범위 밖 refactor와 formatting 정리를 하지 않는다.
## 의사결정 및 중단 규칙
- empty count 기존 test와 source 대조가 모두 통과하면 `P1-T2`는 production diff 없이 완료한다.
- 사용자 재현과 local test가 계속 충돌하면 build version과 재현 artifact가 제공되기 전 report/empty 범위를 추정 수정하지 않는다.
- 레거시 파일 또는 공통 formatter 수정이 필요해지면 Task를 중단하고 사용자 승인을 먼저 받는다.
- 구현 범위가 바뀌면 PRD Decision Log와 이 계획을 먼저 갱신한다.
- 완료된 Task와 검증 기록은 삭제하거나 미완료로 되돌리지 않는다.
## Progress
### 문서 작성 — 2026-08-26
- 상태: 완료
- 무엇을: 사용자 인터뷰와 현재 source 대조를 바탕으로 PRD와 구현 계획을 작성했다.
- 왜: 구현 전 요구사항·범위·완료 증거를 확정하기 위해서다.
- 어떻게:
- 관련 source, locale resource, 기존 formatter, ViewModel/adapter와 기존 test를 read-only로 확인했다.
- production code와 test code는 변경하지 않았다.
- 남은 항목: Phase 1~2 구현 전체
- 다음 행동: 사용자가 코드 구현을 별도로 요청하면 `P1-T1`부터 시작한다.
### 구현 완료 — 2026-08-26
- 상태: 완료
- 무엇을: Phase 1 표시 계약과 Phase 2 상대 시간 통합을 구현했다.
- 왜: PRD의 FanTalk report locale, empty count, Donation badge 숫자-only, `v2` 크리에이터 채널 bind-time 상대 시간 요구사항을 충족하기 위해서다.
- 어떻게:
- RED → GREEN 순서로 focused local unit test를 작성·갱신했다.
- Phase별 spec/quality review와 최종 전체 review를 수행했다.
- 계측 test, 기기·에뮬레이터 UI 조작, 스크린샷, 시각 QA는 사용자 지시에 따라 실행하지 않았다.
- 남은 항목: 없음
- 다음 행동: 사용자가 앱 화면에서 표시를 확인한다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-08-26 | `PLAN-DEC-001` | 확정 | 새 formatter 없이 기존 공통 함수를 View/Activity Context에서 호출한다. | PRD `DEC-003` | `P2-T1`~`P2-T3` |
| 2026-08-26 | `PLAN-DEC-002` | 확정 | empty count는 현재 구현 회귀 검증을 우선한다. | PRD `DEC-004` | `P1-T2` |
| 2026-08-26 | `PLAN-DEC-003` | 확정 | 이번 실행은 문서 작성에서 종료한다. | PRD `DEC-005` | 전체 |
| 2026-08-26 | `PLAN-DEC-004` | 확정 | 후속 사용자 지시에 따라 현재 작업트리에서 Phase 1~2를 구현하고 시각 QA는 사용자가 직접 수행한다. | PRD `DEC-006` | 전체 |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| `ISSUE-001` | Medium | 후보 | 팬레터 report locale과 empty count 사용자 재현이 현재 정적 source·기존 상태 test와 일치하지 않는다. | `P1-T1`, `P1-T2` | local 재바인딩 test와 기존 회귀 test로 먼저 재현한다. |
| `ISSUE-002` | Medium | 확정 | 후원 badge binder가 locale별 캔 단위를 붙인다. | `P1-T3` | 숫자 formatting만 유지하고 unit resource를 제거한다. |
| `ISSUE-003` | High | 확정 | 팬레터·커뮤니티는 application Context 기반 formatter로 미리 문자열화하고 후원·채널 홈은 View Context에서 변환한다. | Phase 2 | raw UTC와 bind-time 공통 formatter로 통합한다. |
| `ISSUE-004` | Low | 해결 | `P2-T3` 파일 목록에 raw UTC field 이름 변경의 직접 소비자인 Donation UI model과 Fragment가 누락되었다. | `P2-T3` | 구현 전에 두 파일을 Task 대상에 추가했다. |
## Verification Log
- 2026-08-26: 문서 작성 단계이므로 production code·test code 구현과 Gradle build/test는 실행하지 않았다.
- 2026-08-26: placeholder 검색 결과 0건, 요구사항 `FAN-001`~`LOC-002`와 Goal `P1-T1`~`P2-GATE` 연결을 확인했다.
- 2026-08-26: `git diff --check` 실행 결과 출력 없이 PASS했다.
- 2026-08-26: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`, exit code 0으로 계획에 사용하는 Gradle task가 존재함을 확인했다.
- 2026-08-26: Phase 1 Gate 검증 명령 6개가 모두 PASS했다. 기기·에뮬레이터 UI 조작, 스크린샷, 시각 QA는 사용자 지시에 따라 실행하지 않았다.
- 2026-08-26: Phase 2 Gate 검증 명령과 호출부 inventory가 PASS했다. 계측 test, 기기·에뮬레이터 UI 조작, 스크린샷, 시각 QA는 사용자 지시에 따라 실행하지 않았다.
- 2026-08-26: 최종 fresh 검증에서 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.RelativeTimeFormatterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`가 병렬 Gradle 실행 중 `CreatorChannelCommunityDetailViewModelTest > 채널 작성자는 상세 embedded 타인 댓글의 삭제 메뉴 권한을 가진다`에서 1회 실패했다. 같은 테스트 단독 실행과 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`는 모두 `BUILD SUCCESSFUL`이었고, 최종 묶음 명령을 단독 재실행해 `BUILD SUCCESSFUL`을 확인했다.
- 2026-08-26: 최종 fresh 검증으로 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 모두 PASS했다. PRD와 plan-task의 미완료 체크박스 검색 결과 0건이다.
- 2026-08-26: 최종 전체 review에서 production, test, resource, DI, navigation 변경은 PASS했고 문서 리뷰 보고서 누락만 blocking으로 지적됐다. `reviews/phase1-creator-channel-display-contract-review.md``reviews/phase2-relative-time-unification-review.md`를 추가하고 PRD 관련 review를 연결했다.
@@ -0,0 +1,199 @@
# 크리에이터 채널 표시와 상대 시간 다국어 정리 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 기준 확정 |
| 작성일 | 2026-08-26 |
| 최종 수정일 | 2026-08-26 |
| 대상 제품 | Android SodaLive `v2` 크리에이터 채널 |
| 작성자·결정권자 | 작성: Codex / 결정: 사용자 |
| 관련 API Contract | 없음 — API request/response 변경 없음 |
| 관련 구현 계획 | `docs/20260826_크리에이터_채널_표시와_상대_시간_다국어_정리/plan-task.md` |
| 관련 review | `reviews/phase1-creator-channel-display-contract-review.md`, `reviews/phase2-relative-time-unification-review.md` |
## 1. Overview
크리에이터 채널의 팬레터·후원·커뮤니티 화면에서 locale에 따라 달라져야 하는 문구와 상대 시간이 일관되게 표시되어야 한다. 상대 시간은 새 formatter를 만들지 않고 기존 `formatUtcRelativeTimeText(Context, String?)`를 화면 Context에서 호출하는 방식으로 통일한다.
이번 문서는 구현 기준만 확정한다. 문서 작성 단계에서는 production code와 test code를 변경하지 않는다.
## 2. Problem Statement
사용자가 확인한 문제는 다음과 같다.
- 앱을 영어 또는 일본어로 설정하고 재시작해도 팬레터 item의 `신고`가 한국어로 표시된다.
- 팬레터와 후원 탭의 내용이 없을 때 `전체 0`이 표시된다.
- 후원 item 우측 상단 캔 수량에 `캔`, `cans`, `CAN` 단위가 붙는다.
- 상대 시간 생성 위치가 mapper, ViewModel, adapter와 Activity로 나뉘어 일부 화면은 선택 언어 Context를 사용하고 일부 화면은 application Context를 사용한다.
현재 `main` 소스 대조 결과는 다음과 같다.
| 항목 | 현재 소스 근거 | 판단 |
|---|---|---|
| 팬레터 `신고` | `creator_channel_fantalk_report`는 한국어·영어·일본어 리소스가 있고 item XML이 해당 리소스를 참조한다. | 사용자 재현과 정적 소스가 불일치하므로 adapter 재바인딩 회귀 test로 먼저 재현한다. |
| 팬레터 empty count | `CreatorChannelFanTalkViewModel`은 count 0 또는 빈 목록을 `Empty`로 만들고 Fragment의 `bindEmpty()`는 count bar를 숨긴다. | 현재 계약을 회귀 검증하며 실패 근거 없이 production code를 중복 수정하지 않는다. |
| 후원 empty count | `CreatorChannelDonationViewModel`은 count 0 또는 빈 목록을 `Empty`로 만들고 Fragment의 `bindEmpty()`는 count bar를 숨긴다. | 현재 계약을 회귀 검증하며 실패 근거 없이 production code를 중복 수정하지 않는다. |
| 후원 캔 수량 | `bindCreatorChannelDonationCard()``creator_channel_donation_can_format`으로 단위를 붙인다. | production 변경 필요 |
| 상대 시간 | 채널 홈·후원은 view Context에서 공통 함수를 호출하지만 팬레터·커뮤니티는 injected formatter로 미리 문자열화한다. | `v2` 크리에이터 채널 표시 시점 호출로 통합 필요 |
## 3. Goals
### 3.1 제품 목표
- 팬레터 item의 `신고`가 한국어 `신고`, 영어 `Report`, 일본어 `通報`로 표시된다.
- 팬레터·후원 탭의 empty 상태에서는 count bar 전체가 표시되지 않는다.
- 후원 item 우측 상단 수량은 locale과 관계없이 숫자만 표시한다.
- `v2` 크리에이터 채널의 상대 시간은 화면별 중복 계산 없이 기존 공통 formatter를 사용한다.
### 3.2 품질 목표
- locale에 종속되는 문자열은 application Context에서 미리 생성하지 않고 실제 표시 View 또는 Activity의 Context에서 생성한다.
- 현재 소스가 이미 충족하는 empty 계약은 회귀 test와 근거 확인만 수행한다.
- 한국어·영어·일본어의 동일 입력에 대해 locale별 출력이 local unit test로 검증된다.
## 4. Non-Goals
- `kr.co.vividnext.sodalive.v2.creator.channel` 밖의 레거시 화면과 상대 시간 호출부는 수정하지 않는다.
- `formatUtcRelativeTimeText()`가 정의된 기존 공통 파일의 파싱 규칙, 시간 구간, 문자열 문구는 변경하지 않는다.
- 예약 시각, 라이브 시작 시각, `HH:mm`처럼 절대 시각을 표시하는 formatter는 통합 대상이 아니다.
- API, DTO field, pagination, 정렬, empty 안내 문구와 후원 fallback message는 변경하지 않는다.
- 후원 item 본문의 fallback message에 포함된 캔 단위는 제거하지 않는다. 단위 제거 대상은 우측 상단 badge뿐이다.
- 신규 dependency, 신규 formatter abstraction, `app/src/androidTest`, 기기·에뮬레이터 조작과 시각 QA를 추가하지 않는다.
## 5. Target Users and Permissions
| 사용자 | 목표 | 권한 영향 |
|---|---|---|
| 크리에이터 채널 방문자 | 선택한 언어로 팬레터·커뮤니티·후원 정보를 읽는다. | 기존 조회·신고 권한 유지 |
| 크리에이터 채널 소유자 | empty·content 상태와 후원 내역을 정확히 확인한다. | 기존 소유자 action 유지 |
인증, 신고 권한, 글 수정·삭제 권한은 이번 범위에서 변경하지 않는다.
## 6. 대상 화면과 데이터 흐름
상대 시간 적용 범위는 다음과 같다.
| 영역 | 상대 시간 표시 | 기준 |
|---|---|---|
| 채널 홈 | 후원, 공지, 팬레터 등 현재 `CreatorChannelHomeSectionAdapter`가 표시하는 상대 시간 | 현재 view Context 호출 유지 |
| 팬레터 탭 | 원글, 크리에이터 답글 | raw UTC를 item bind 시 변환 |
| 팬레터 상세 | 원글, 답글 | raw UTC를 Activity bind 시 변환 |
| 커뮤니티 탭 | list item 작성 시각 | raw UTC를 item bind 시 변환 |
| 커뮤니티 상세 | 게시글, 댓글 | raw UTC를 Activity 또는 item bind 시 변환 |
| 커뮤니티 답글 | 부모 댓글, 답글 | raw UTC를 Activity 또는 item bind 시 변환 |
| 후원 탭 | 후원 item 작성 시각 | 현재 view Context 호출 유지 |
표시 흐름은 아래 하나로 통일한다.
```text
API raw UTC
→ v2 response/UI model에서 raw UTC 유지
→ View/Activity bind 시 현재 Context 전달
→ formatUtcRelativeTimeText(context, rawUtc)
→ 한국어·영어·일본어 상대 시간 표시
```
## 7. 기능 요구사항
### 7.1 팬레터 표시
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|---|---|---|---|---|
| `FAN-001` | 확정 | 타인 팬레터의 `신고` label은 bind 시 현재 item Context의 기존 문자열 리소스를 사용한다. | 한국어 `신고`, 영어 `Report`, 일본어 `通報`가 재바인딩 후 표시된다. | `P1-T1` |
| `FAN-002` | 확정 | 표시 가능한 팬레터가 없으면 count bar 전체를 숨긴다. | count 0 또는 첫 페이지 빈 목록에서 `Empty` 상태이며 `전체 0`이 표시되지 않는다. | `P1-T2` |
### 7.2 후원 표시
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|---|---|---|---|---|
| `DON-001` | 확정 | 표시 가능한 후원이 없으면 count bar 전체를 숨긴다. | count 0 또는 첫 페이지 빈 목록에서 `Empty` 상태이며 `전체 0`이 표시되지 않는다. | `P1-T2` |
| `DON-002` | 확정 | 후원 item 우측 상단 badge는 수량 숫자만 표시한다. | 1,000캔 후원은 한국어·영어·일본어에서 모두 `1,000`으로 표시된다. | `P1-T3` |
| `DON-003` | 확정 | 후원 본문의 빈 message fallback은 기존 locale별 문구를 유지한다. | `1,000캔을 후원하였습니다.`, `Supported with 1000 cans.`, `1000CANを応援しました。` 계약을 변경하지 않는다. | `P1-GATE` |
### 7.3 상대 시간 공통화
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|---|---|---|---|---|
| `TIME-001` | 확정 | `v2` 크리에이터 채널의 모든 상대 시간 표시부는 기존 `formatUtcRelativeTimeText()`를 호출한다. | 대상 package에서 별도 상대 시간 계산 또는 application Context 기반 사전 문자열화가 남지 않는다. | `P2-T1`~`P2-T3` |
| `TIME-002` | 확정 | 팬레터와 커뮤니티 presentation data는 표시 전까지 raw UTC를 유지한다. | mapper/ViewModel은 locale 문자열을 만들지 않고 adapter/Activity가 현재 Context로 변환한다. | `P2-T1`, `P2-T2` |
| `TIME-003` | 확정 | 후원과 채널 홈의 기존 bind-time 공통 formatter 사용을 유지한다. | 후원·채널 홈 회귀 test와 호출부 inventory가 통과한다. | `P2-T3` |
| `TIME-004` | 확정 | 기존 공통 formatter의 경계값과 파싱 실패 정책을 유지한다. | `RelativeTimeFormatterTest`가 수정 없이 통과한다. | `P2-GATE` |
### 7.4 locale 계약
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|---|---|---|---|---|
| `LOC-001` | 확정 | 지원 locale은 한국어 `ko`, 영어 `en`, 일본어 `ja`이다. | 세 locale Context에서 label과 상대 시간 검증이 통과한다. | `P1-T1`, `P2-T1`, `P2-T2` |
| `LOC-002` | 확정 | locale 문자열은 실제 표시 Context에서 가져온다. | item 재바인딩과 Activity render가 현재 Context의 resource를 반환한다. | `P1-T1`, `P2-T1`, `P2-T2` |
## 8. UI/UX Expectations
- content 상태에서는 기존 `전체 N` count bar를 유지한다.
- empty 상태에서는 `전체` label과 `0` count를 각각 숨기는 것이 아니라 count bar container 전체를 숨긴다.
- 후원 badge의 icon, 배경, padding, 색상은 유지하고 text에서 단위만 제거한다.
- `신고`의 style, 위치, 클릭 동작과 신고 dialog/API는 변경하지 않는다.
- 상대 시간의 글꼴, 색상, 위치와 시간 구간별 문구는 변경하지 않는다.
## 9. API 계약
이번 작업은 API Contract 변경이 없다. 서버가 제공하는 `createdAtUtc`, `fanTalkCount`, `donationCount`, 목록 field를 그대로 사용한다.
## 10. 성능과 품질 요구사항
- bind 시 기존 formatter 한 번을 호출하며 별도 cache와 background 작업을 추가하지 않는다.
- 신규 dependency를 추가하지 않는다.
- 자동 검증은 `app/src/test`의 JUnit4/Robolectric local unit test만 사용한다.
- UI 검증은 adapter binding, mapper raw UTC 보존, ViewModel empty 상태처럼 화면 표현을 결정하는 로직만 다룬다.
- View 크기·간격·constraint·visibility 속성을 직접 검증하는 신규 test는 작성하지 않는다.
- 공통 formatter 파일과 레거시 상대 시간 호출부는 변경하지 않는다.
## 11. 성공 기준
### 11.1 기능 수용 기준
- [x] 팬레터 `신고`가 한국어·영어·일본어 Context에 맞게 표시된다. (`FAN-001`, `LOC-001`)
- [x] 팬레터와 후원 empty 상태에서 `전체 0` count bar가 표시되지 않는다. (`FAN-002`, `DON-001`)
- [x] 후원 badge가 locale과 관계없이 숫자만 표시한다. (`DON-002`)
- [x] 팬레터·커뮤니티·후원·채널 홈의 상대 시간이 표시 시점 공통 formatter 경로를 사용한다. (`TIME-001`~`TIME-003`)
- [x] 기존 상대 시간 구간과 fallback message 동작이 유지된다. (`DON-003`, `TIME-004`)
### 11.2 검증 기준
- [x] focused local unit test가 모두 통과한다.
- [x] `kr.co.vividnext.sodalive.v2.creator.channel.*` 회귀 test가 통과한다.
- [x] `RelativeTimeFormatterTest`, resource merge, Kotlin compile, ktlint와 `git diff --check`가 통과한다.
- [x] 계측 test, 기기·에뮬레이터 UI 조작과 스크린샷 QA를 실행하지 않는다.
## 12. Open Questions
없음. 구현 방향을 바꾸는 새 정보가 확인되면 production code를 변경하기 전에 이 문서의 Decision Log와 `plan-task.md`를 먼저 갱신한다.
## 13. 요구사항 추적표
| 요구사항 | 계획 Phase/Goal | 자동 검증 | 수동 검증 |
|---|---|---|---|
| `FAN-001`, `LOC-001` | Phase 1 / `P1-T1` | 팬레터 adapter locale test | 없음 |
| `FAN-002`, `DON-001` | Phase 1 / `P1-T2` | 팬레터·후원 ViewModel empty 상태 test | 없음 |
| `DON-002`, `DON-003` | Phase 1 / `P1-T3`, `P1-GATE` | 후원 card binder locale test | 없음 |
| `TIME-001`, `TIME-002` | Phase 2 / `P2-T1`, `P2-T2` | 팬레터·커뮤니티 mapper/adapter/detail test | 없음 |
| `TIME-003`, `TIME-004` | Phase 2 / `P2-T3`, `P2-GATE` | 후원·채널 홈·공통 formatter 회귀 test | 없음 |
## 14. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal |
|---|---|---|---|---|---|
| 2026-08-26 | `DEC-001` | 확정 | 상대 시간 통합 범위는 `v2` 크리에이터 채널로 제한하고 레거시 화면은 제외한다. | 사용자 인터뷰에서 B안 선택 | `TIME-001`~`TIME-004`, Phase 2 |
| 2026-08-26 | `DEC-002` | 확정 | 영어·일본어 모두 앱 재시작 후 팬레터 `신고` 미번역이 재현된 것으로 요구사항을 정의한다. | 사용자 인터뷰 응답 B, C | `FAN-001`, `LOC-001` |
| 2026-08-26 | `DEC-003` | 확정 | 새 formatter를 만들지 않고 기존 `formatUtcRelativeTimeText()`를 화면 Context에서 재사용한다. | 사용자 승인 권장 설계안 | `TIME-001`, `TIME-002`, Phase 2 |
| 2026-08-26 | `DEC-004` | 확정 | 현재 소스가 이미 충족하는 empty count 계약은 회귀 검증을 우선하고 실패 근거 없이 중복 구현하지 않는다. | 최소 변경 원칙과 현재 ViewModel/Fragment 대조 | `FAN-002`, `DON-001`, `P1-T2` |
| 2026-08-26 | `DEC-005` | 확정 | 이번 요청에서는 PRD와 구현 계획만 작성하고 code 구현은 진행하지 않는다. | 사용자 직접 지시 | 전체 |
| 2026-08-26 | `DEC-006` | 확정 | 확정된 PRD와 구현 계획에 따라 현재 작업트리에서 구현하며 worktree와 시각 QA는 사용하지 않는다. | 사용자 후속 구현 지시 | 전체 |
## 15. 변경 관리
- 요구사항 변경 시 Decision Log를 먼저 갱신한다.
- 레거시 formatter 또는 레거시 호출부 수정이 필요해지면 구현 전에 사용자 승인을 다시 받는다.
- 리뷰에서 수정 항목이 확정되면 기존 완료 Task를 다시 열지 않고 회귀 수정 Task를 추가한다.
- 기존 검증 기록은 삭제하거나 덮어쓰지 않고 누적한다.
+10
View File
@@ -132,6 +132,16 @@ CreatorChannel 현재 라이브 공통 컴포넌트 테스트 예시:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"
``` ```
CreatorChannel 팬톡 locale 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkAdapterLocaleTest"
```
CreatorChannel 커뮤니티 상대 시간 locale 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityRelativeTimeLocaleTest"
```
Chat Action Phase 테스트 예시: Chat Action Phase 테스트 예시:
```bash ```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*"