From 5dd58d0092f3ad90458539b872f5ce57e9dae47e Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 30 Apr 2026 12:47:14 +0900 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=EC=B1=84=ED=8C=85=20=EC=BF=BC?= =?UTF-8?q?=ED=84=B0=20=EC=B6=A9=EC=A0=84=20=ED=99=95=EC=9E=A5=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EA=B8=B0=EB=A1=9D=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bg_chat_quota_rewarded_ad_button.xml | 6 + docs/20260430_채팅쿼터충전확장계획.md | 178 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 app/src/main/res/drawable/bg_chat_quota_rewarded_ad_button.xml create mode 100644 docs/20260430_채팅쿼터충전확장계획.md diff --git a/app/src/main/res/drawable/bg_chat_quota_rewarded_ad_button.xml b/app/src/main/res/drawable/bg_chat_quota_rewarded_ad_button.xml new file mode 100644 index 00000000..0f955d11 --- /dev/null +++ b/app/src/main/res/drawable/bg_chat_quota_rewarded_ad_button.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/20260430_채팅쿼터충전확장계획.md b/docs/20260430_채팅쿼터충전확장계획.md new file mode 100644 index 00000000..0c8e755e --- /dev/null +++ b/docs/20260430_채팅쿼터충전확장계획.md @@ -0,0 +1,178 @@ +# 20260430 채팅 쿼터 충전 확장 계획 + +## 작업 체크리스트 +- [x] 기존 채팅 쿼터 UI, 응답 DTO, 구매 API, Yandex 광고 사용 패턴을 근거 파일 기준으로 조사한다. + QA: `ChatRoomActivity`, `ChatMessageAdapter`, `ChatQuotaPurchaseRequest`, `TalkApi`, `ChatRepository`, `AudioContentDetailActivity`, `app/build.gradle`를 근거로 현재 구조를 설명할 수 있어야 한다. +- [x] 쿼터 안내 노출 조건을 `nextRechargeAtEpoch != null`에서 `totalRemaining <= 0` 기준으로 전환한다. + QA: 채팅방 입장, 메시지 전송 응답, 쿼터 상태 조회, 쿼터 구매 응답 모두에서 `updateQuotaUi`가 `totalRemaining` 기준으로만 노출 여부를 결정해야 한다. +- [x] 무료 충전 제거에 맞춰 카운트다운 및 무료 대기 문구 관련 로직을 제거한다. + QA: `CountDownTimer`, `tv_time`, `nextRechargeAtEpoch` 기반 갱신, `checkQuotaStatus()`의 무료 충전 재조회 목적 로직이 더 이상 남지 않아야 한다. +- [x] `item_chat_quota_notice`를 2단 구성의 신규 구매 UI로 교체한다. + QA: 상단에는 광고 버튼 1개, 하단에는 캔 구매 버튼 2개가 가로 배치되어야 하며, 기존 시간/무료 안내 문구 영역은 제거되어야 한다. +- [x] 광고 버튼과 캔 구매 버튼의 텍스트/스타일을 요청 사양대로 반영한다. + QA: 광고 버튼은 `광고 / 5채팅`, 하단 버튼은 `10 / 15채팅`, `20 / 40채팅` 구조를 가지며, 각 캔 숫자 앞에 `ic_can` 아이콘이 표시되고 캔 숫자는 bold, 채팅 개수는 medium이어야 한다. +- [x] 쿼터 구매 요청 DTO를 광고/캔 구매 구분이 가능하도록 확장한다. + QA: 요청 본문에 `container`, `chargeType`, `canOption`이 포함되고, 캔 구매 시 `CAN_10`, `CAN_20`, 광고 구매 시 `AD` 타입을 표현할 수 있어야 한다. +- [x] Yandex 채팅 쿼터 전용 rewarded ad unit id를 buildType별 `BuildConfig`로 추가한다. + QA: `debug`/`release`에서 채팅 쿼터 광고용 별도 `BuildConfig` 키가 생성되어야 하며, 기존 오디오 콘텐츠 전면 광고 id와 분리되어야 한다. +- [x] `totalRemaining == 1`일 때 광고를 미리 준비하고, 광고 버튼 터치 시 rewarded 광고를 표시한 뒤 `onRewarded()` 시점에 쿼터 충전 API를 호출하도록 흐름을 추가한다. + QA: 광고 미로드/표시 실패 시에는 API가 호출되지 않아야 하고, 사용자가 reward 조건을 충족했을 때만 `AD` 타입 구매 요청이 한 번만 전송되어야 한다. +- [x] 캔 구매 버튼 2종이 각각 올바른 구매 옵션으로 API를 호출하고, 성공 시 헤더 캔 수와 쿼터 UI를 갱신하도록 정리한다. + QA: 10캔 버튼은 15채팅, 20캔 버튼은 40채팅과 연결되고, 성공 후 `SharedPreferenceManager.can` 및 `tvCanBadge`가 실제 차감값에 맞게 갱신되어야 한다. +- [x] 변경 결과를 문서 하단 검증 기록에 누적한다. + QA: 최소 빌드, 테스트, 수동 확인 계획과 실제 실행 결과가 한국어로 누적되어야 한다. + +## 범위 메모 +- 이번 요청은 `ChatRoomActivity`의 채팅 쿼터 부족 안내와 구매 흐름을 광고/캔 3가지 선택지로 확장하는 작업으로 한정한다. +- 무료 충전은 제거되므로, 기존 `nextRechargeAtEpoch` 기반 표시 판단과 카운트다운 UI/로직은 제거 대상으로 본다. +- 안내 노출 여부는 `totalRemaining <= 0`일 때만 표시하는 방향으로 고정한다. +- 광고 버튼은 Yandex **rewarded ad**를 사용하고, reward 지급 콜백인 `onRewarded()` 시점에 쿼터 충전 API를 호출한다. +- reward 기준으로 처리하므로, 광고가 단순히 표시되거나 닫힌 것만으로는 보상을 지급하지 않는다. +- 광고 unit id는 기존 `app/build.gradle`의 `buildConfigField` 패턴을 유지하되, 채팅 쿼터 지면은 별도 키로 분리한다. +- 캔 구매 옵션은 요청에 맞춰 `10캔→15채팅`, `20캔→40채팅` 두 가지만 제공한다. +- `AD` 타입 쿼터 지급은 서버가 광고 보상 완료를 검증할 수 있는 구조(예: SSV, 검증 토큰, nonce/idempotency)인지 별도 확인이 필요하다. +- 요청 범위를 넘는 별도 결제 화면 추가, 무료 충전 대체 UX 확장, 다른 화면 공통화 리팩터링은 제외한다. + +## 조사 근거 +- 현재 채팅 쿼터 흐름 + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt` + - `app/src/main/res/layout/item_chat_quota_notice.xml` +- 쿼터 응답/구매 DTO + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendChatMessageResponse.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaStatusResponse.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaPurchaseRequest.kt` +- API/Repository + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt` +- 기존 Yandex 광고/전면광고 패턴 + - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/app/SodaLiveApp.kt` + - `app/build.gradle` +- 공식 문서 + - `https://ads.yandex.com/helpcenter/en/dev/android/interstitial` + - `https://ads.yandex.com/helpcenter/en/dev/android/rewarded` + +## 구현 계획 + +### 1. 쿼터 상태 판단 기준 전환 +- 수정 대상: + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendChatMessageResponse.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaStatusResponse.kt` +- 계획: + - `updateQuotaUi` 시그니처와 호출부를 `nextRechargeAtEpoch` 대신 `totalRemaining` 중심으로 재구성한다. + - 입장 응답, 메시지 전송 응답, 쿼터 조회/구매 응답에서 모두 동일한 판단식을 사용한다. + - `inputContainer` 표시 여부도 `totalRemaining > 0` 기준으로 정리한다. + +### 2. 무료 충전/카운트다운 제거 +- 수정 대상: + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt` + - `app/src/main/res/layout/item_chat_quota_notice.xml` +- 계획: + - `quotaTimer`, `startQuotaCountdown`, `stopQuotaCountdown`, `formatEpochToHms`, `formatMillisToHms`, `checkQuotaStatus`의 역할을 재평가하고, 무료 충전용 로직은 제거한다. + - `ChatListItem.QuotaNotice`가 시간 텍스트를 들고 다니는 구조를 단순화한다. + - `QuotaNoticeViewHolder`의 `tv_time` 의존성을 제거하고, 버튼 클릭만 처리하는 형태로 바꾼다. + +### 3. QuotaNotice 레이아웃 교체 +- 수정 대상: + - `app/src/main/res/layout/item_chat_quota_notice.xml` + - 필요 시 관련 drawable / string 리소스 +- 계획: + - 상단 시간/무료 안내 블록을 제거하고 광고 버튼 1개를 `match_parent`로 배치한다. + - 하단에는 좌우 2개 버튼을 동일 행에 둔다. + - 광고 버튼 배경색 `RGB(254, 248, 227)`, border `RGB(247, 203, 80)`를 기존 코드 스타일에 맞춰 hex로 정의한다. + - 버튼 텍스트는 국제화 리소스로 분리하되, 요청 문구를 그대로 반영한다. + +### 4. 캔 구매 DTO 및 API 호출 확장 +- 수정 대상: + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaPurchaseRequest.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` +- 계획: + - 기존 `ChatQuotaPurchaseRequest`를 요청 사양에 맞게 확장하고, 필요하면 내부 클래스명도 API 계약과 맞는 방향으로 정리한다. + - `ChatRoomQuotaChargeType`, `ChatRoomQuotaCanOption` enum을 추가한다. + - `purchaseChatQuota(...)`가 광고/캔 버튼별로 다른 요청 본문을 받을 수 있게 repository 시그니처를 확장한다. + +### 5. 채팅 쿼터 전용 Yandex rewarded 흐름 추가 +- 수정 대상: + - `app/build.gradle` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` +- 계획: + - `debug`/`release` 각각에 채팅 쿼터 rewarded ad unit id `BuildConfig` 키를 추가한다. + - 기존 Yandex 광고 초기화 구조는 유지하고, rewarded ad의 loader / load listener / event listener / `show(activity)` 패턴을 채팅방에 맞게 적용한다. + - `totalRemaining == 1` 시점에 preload를 시도하고, 광고 버튼 터치 시 로드된 광고가 있으면 표시한다. + - `onRewarded()` 콜백에서 `AD` 타입 쿼터 구매 API를 호출한다. + - `onAdFailedToShow`, `onAdDismissed`, `onDestroy`에서 listener와 ad 참조를 정리한다. + +### 6. 버튼별 구매 액션과 로컬 상태 갱신 정리 +- 수정 대상: + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` +- 계획: + - 어댑터 콜백을 광고/10캔/20캔으로 구분 가능한 형태로 확장하거나, 단일 콜백에 옵션 인자를 전달하도록 바꾼다. + - 캔 구매 성공 시에는 선택한 `needCan`만큼 `SharedPreferenceManager.can`을 차감하고 `tvCanBadge`를 갱신한다. + - 광고 구매 성공 시에는 캔 차감 없이 쿼터 UI만 갱신한다. + - 응답의 `totalRemaining` 기준으로 입력창 복구 여부와 QuotaNotice 제거 여부를 결정한다. + +## 예상 수정 파일 +- `docs/20260430_채팅쿼터충전확장계획.md` +- `app/build.gradle` +- `app/src/main/res/layout/item_chat_quota_notice.xml` +- `app/src/main/res/values/strings.xml` +- `app/src/main/res/values-en/strings.xml` +- `app/src/main/res/values-ja/strings.xml` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaPurchaseRequest.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaStatusResponse.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomEnterResponse.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/SendChatMessageResponse.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt` + +## 검증 계획 +- `./gradlew :app:assembleDebug` +- `./gradlew :app:testDebugUnitTest` +- 필요 시 `./gradlew :app:ktlintCheck` +- 수동 확인: + - `totalRemaining > 0` 상태에서 입력창이 보이고 QuotaNotice가 사라지는지 확인한다. + - `totalRemaining <= 0` 상태에서 신규 QuotaNotice UI가 노출되는지 확인한다. + - `totalRemaining == 1`일 때 rewarded 광고 preload가 준비되는지 로그 또는 디버그 포인트로 확인한다. + - 광고 버튼 탭 시 rewarded 광고가 뜨고 `onRewarded()` 직후 `AD` 타입 구매 요청이 1회 호출되는지 확인한다. + - 10캔/20캔 버튼 각각이 15채팅/40채팅 구매와 연결되고, 성공 후 헤더 캔 수와 입력창 상태가 갱신되는지 확인한다. + +## 검증 기록 +- 2026-04-30 + - 무엇: 채팅 쿼터 충전 확장 작업의 계획 문서를 생성했다. + - 왜: 저장소 규칙에 따라 구현 전에 `docs` 아래 계획 문서를 먼저 만들고, 그 문서를 기준으로 범위·근거·검증 기준을 고정해야 하기 때문이다. + - 어떻게: + - 생성 파일: `docs/20260430_채팅쿼터충전확장계획.md` + - 근거 파일: `ChatRoomActivity.kt`, `ChatMessageAdapter.kt`, `item_chat_quota_notice.xml`, `ChatQuotaPurchaseRequest.kt`, `ChatQuotaStatusResponse.kt`, `TalkApi.kt`, `ChatRepository.kt`, `ChatRoomEnterResponse.kt`, `SendChatMessageResponse.kt`, `AudioContentDetailActivity.kt`, `app/build.gradle` + - 근거 문서: `https://ads.yandex.com/helpcenter/en/dev/android/interstitial`, `https://ads.yandex.com/helpcenter/en/dev/android/rewarded` + - 결과: 무료 충전 제거, `totalRemaining` 기준 노출 전환, 광고/캔 3가지 구매 UI, DTO 확장, Yandex rewarded 연동 범위, 예상 수정 파일과 검증 계획을 구현 전에 먼저 확정했다. +- 2026-04-30 + - 무엇: 채팅 쿼터 충전 확장 구현과 검증을 완료했다. + - 왜: 무료 충전 제거 이후에도 채팅 쿼터 부족 상태에서 rewarded 광고와 2종의 캔 구매 옵션으로 다시 충전할 수 있어야 했기 때문이다. + - 어떻게: + - 수정 파일: `app/build.gradle`, `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt`, `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt`, `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/quota/ChatQuotaPurchaseRequest.kt`, `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRepository.kt`, `app/src/main/res/layout/item_chat_quota_notice.xml`, `app/src/main/res/drawable/bg_chat_quota_rewarded_ad_button.xml`, `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`, `app/src/test/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapterTest.kt` + - 구현 내용: `totalRemaining` 기준 QuotaNotice 노출 전환, countdown/free-wait 로직 제거, rewarded 광고 preload/show/onRewarded 구매 호출, 10캔/20캔 구매 분기, 채팅 쿼터 전용 rewarded ad unit id 추가, QuotaNotice 3버튼 UI 적용 + - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 실행 결과: `BUILD SUCCESSFUL` + - 실행 명령: `./gradlew :app:ktlintCheck` + - 실행 결과: `BUILD SUCCESSFUL` + - 진단 도구: `lsp_diagnostics` + - 진단 결과: `.kt` LSP 서버 미설정으로 `No LSP server configured for extension: .kt` + - 수동 확인 시도: `adb devices` + - 수동 확인 결과: 연결된 Android 기기가 없어 앱 실행 기반의 광고 노출/버튼 동작 수동 검증은 이번 세션에서 진행하지 못했다. +- 2026-04-30 + - 무엇: 리뷰 피드백을 반영해 중복 요청 방지와 rewarded preload 조건을 보강하고, 서버 검증 리스크를 문서에 기록했다. + - 왜: 빠른 연속 탭으로 인한 중복 구매/중복 광고 시도와, 이미 quota가 소진된 상태에서 첫 광고 탭이 항상 실패하는 UX를 줄여야 했기 때문이다. + - 어떻게: + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt`, `docs/20260430_채팅쿼터충전확장계획.md` + - 로직 보강: `isQuotaPurchaseInFlight`, `isChatQuotaRewardedAdShowing` 가드 추가, rewarded preload 조건을 `totalRemaining <= 1`로 확장 + - 문서 반영: `AD` 타입 쿼터 지급의 서버 검증/SSV/idempotency 확인 필요성을 범위 메모에 기록