# 20260326_멤버정보응답확장및콘텐츠보기설정동기화.md ## 개요 - `/member/info` 응답 스펙 확장(`countryCode`, `isAdultContentVisible`, `contentType`)을 앱 데이터 모델과 로컬 상태에 반영한다. - 설정 화면의 `콘텐츠 보기 설정` 노출 조건을 기존 `SharedPreferenceManager.isAuth == true` 유지 + `countryCode != "KR"` 조건 추가로 확장한다. - `ContentSettings`에서 값 변경 시 `/member/content-preference`(PATCH) API로 서버 동기화를 수행하고, API 호출 구간에 `LoadingDialog`를 표시한다. - 빠른 연타 상황에서는 **RxJava debounce를 사용하지 않고** 마지막 상태만 서버로 전송되도록 처리한다. ## 요구사항 해석(확정) - `/member/info` 응답 모델에 아래 필드를 추가한다. - `countryCode: String` - `isAdultContentVisible: Boolean` - `contentType: ContentType` - `/member/info`에서 수신한 `countryCode`, `isAdultContentVisible`, `contentType`을 로컬 저장소(`SharedPreferenceManager`)에 동기화한다. - `SettingsActivity`의 `콘텐츠 보기 설정` 메뉴 노출은 아래 중 하나라도 만족하면 표시한다. - `SharedPreferenceManager.isAuth == true` - `countryCode != "KR"` - `ContentSettingsActivity`에서 성인 콘텐츠 토글/콘텐츠 타입 변경 시 `/member/content-preference` PATCH를 호출한다. - `/member/content-preference` PATCH 요청은 실제로 변경된 필드만 전송할 수 있도록 request 파라미터를 optional로 지원한다. - API 호출 동안 Loading UI를 노출한다. - 연속 입력 시 debounce 처리하여 마지막 값만 전송한다. - debounce 구현은 RxJava 연산자(`debounce`, `switchMap` 등)를 사용하지 않는다. ## 현재 구조 조사 요약 - `UserApi.getMemberInfo()`는 이미 `@GET("/member/info")`로 연결되어 있고 응답은 `GetMemberInfoResponse`를 사용한다. - `MainViewModel.getMemberInfo()`가 `SharedPreferenceManager.isAuth`를 갱신하는 현재 유일 경로다. - `SettingsActivity`에서 `rlContentSettings` 노출 조건은 현재 `SharedPreferenceManager.isAuth` 단일 조건이다. - `ContentSettingsActivity`/`ContentSettingsViewModel`은 현재 로컬 `SharedPreferenceManager`만 갱신하며 서버 PATCH 연동이 없다. - `ContentSettingsViewModel`은 현재 `UserRepository`를 주입받지 않으며(`AppDI.kt`에서 `ContentSettingsViewModel()`), 로딩/오류 상태 LiveData도 없다. ## 설계 결정 - `GetMemberInfoResponse`를 확장하고, `MainViewModel.getMemberInfo()`에서 신규 필드를 `SharedPreferenceManager`에 동기화한다. - 국가 코드 저장을 위해 `Constants.PREF_COUNTRY_CODE` + `SharedPreferenceManager.countryCode`를 추가한다. - 설정 메뉴 노출 조건은 `isAuth || countryCode != "KR"`로 통합한다. - `countryCode` 미존재/공백 시 기본값은 `"KR"`로 간주하여 기존 노출 정책을 깨지 않도록 한다. - `/member/content-preference` PATCH API를 `UserApi`/`UserRepository`에 추가하고, 요청/응답 DTO를 분리한다. - `ContentSettingsViewModel`에 `UserRepository`를 주입해 서버 동기화 책임을 이동한다. - debounce는 RxJava 없이 `Handler(Looper.getMainLooper()) + Runnable`로 구현한다. - 입력마다 이전 Runnable을 취소하고 지연 전송을 재등록한다. - 지연 종료 시점의 최신 상태(`isAdultContentVisible`, `contentType`)만 전송한다. - API 진행 중 추가 변경이 들어오면 최신 상태를 대기열로 유지하고, 현재 요청 완료 직후 마지막 상태 1회만 추가 전송한다. - `LoadingDialog`는 **실제 PATCH 요청 수행 구간**에만 표시한다(디바운스 대기 시간에는 미표시). ## 완료 기준 (Acceptance Criteria) - [x] AC1: `/member/info` 응답 확장 필드 3개가 모델에 반영되고, 로컬 저장소 동기화까지 정상 동작한다. - [x] AC2: 설정 화면에서 `isAuth == true` 또는 `countryCode != "KR"`이면 `콘텐츠 보기 설정` 메뉴가 노출된다. - [x] AC3: `ContentSettings`의 토글/타입 변경이 `/member/content-preference` PATCH로 서버에 전달된다. - [x] AC4: PATCH 요청 수행 중 `LoadingDialog`가 표시되고 완료/실패 시 정상 해제된다. - [x] AC5: 연속 탭 시 마지막 상태만 서버로 전송된다. - [x] AC6: debounce 구현에서 RxJava debounce 계열 연산자를 사용하지 않는다. ## 구현 체크리스트 ### 1) `/member/info` 응답 모델 확장 - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/notification/GetMemberInfoResponse.kt` - `countryCode`, `isAdultContentVisible`, `contentType` 필드 추가 - [x] `contentType`은 현재 로컬에서 사용하는 `ContentType` enum과 동일 타입으로 사용하도록 매핑 정합을 유지한다. ### 2) 로컬 저장소 키/상태 확장 - [x] `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt` - `PREF_COUNTRY_CODE` 상수 추가 - [x] `app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt` - `countryCode: String` 프로퍼티 추가 - 필요 시 멤버 정보 기반 기본값 동기화 로직 정의 - [x] `app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt` - `getMemberInfo()` 성공 시 `countryCode`, `isAdultContentVisible`, `contentType`을 각각 `SharedPreferenceManager.countryCode`, `SharedPreferenceManager.isAdultContentVisible`, `SharedPreferenceManager.contentPreference`로 동기화한다. ### 3) 설정 메뉴 노출 조건 확장 - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt` - `rlContentSettings` 표시 조건을 `isAuth || countryCode != "KR"`로 변경 - 기존 숨김/표시 및 클릭 연결 흐름 유지 ### 4) 콘텐츠 설정 PATCH API 추가 - [x] `app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt` - `@PATCH("/member/content-preference")` API 정의 추가 - [x] `app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt` - content preference 업데이트 메서드 추가 - [x] 신규 DTO 파일 추가 - 요청: `UpdateContentPreferenceRequest` (`isAdultContentVisible`, `contentType`) - 응답: `UpdateContentPreferenceResponse` (`isAdultContentVisible`, `contentType`) ### 5) ContentSettings 서버 동기화 + Non-Rx debounce - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsViewModel.kt` - `UserRepository` 주입으로 생성자 변경 - 로딩/오류 상태 LiveData 추가 - 로컬 상태 반영 후 debounce 스케줄링 함수 추가(Handler/Runnable) - in-flight + pending 상태를 분리해 마지막 상태 1회 전송 보장 - `onCleared()`에서 callback 정리 - [x] `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - `viewModel { ContentSettingsViewModel(get()) }`로 DI 갱신 ### 6) ContentSettings 화면 로딩 UI 반영 - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsActivity.kt` - `LoadingDialog` 초기화 및 `viewModel.isLoading` 관찰 - 요청 중 표시/요청 완료 해제 처리 ### 7) 검증 - [ ] `lsp_diagnostics`로 수정 파일 신규 오류 확인 - [x] `./gradlew :app:testDebugUnitTest` 실행 - [x] `./gradlew :app:assembleDebug` 실행 - [ ] 수동 QA - 인증 사용자 + 비인증/해외 국가 코드 조합별 메뉴 노출 확인 - 콘텐츠 설정 연타 시 네트워크 요청이 마지막 값 위주로 수렴되는지 확인 - 요청 중 LoadingDialog 표시/해제 확인 ### 8) 추가 요구사항 반영 (2026-03-27) - [x] `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt` - 본인인증/인증완료 아이템(`btnIdentityVerification`)을 `countryCode == "KR"`일 때만 노출 - 비KR에서는 본인인증 버튼을 숨김 처리 ### 9) 콘텐츠 설정 PATCH optional request 반영 (2026-03-27) - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/UpdateContentPreferenceRequest.kt` - `isAdultContentVisible`, `contentType`를 nullable optional 파라미터로 변경 - [x] `app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsViewModel.kt` - 변경된 필드만 request에 담아 PATCH 호출하도록 분기 ### 10) 캐릭터 상세 진입 인증 체크 국가 분기 반영 (2026-03-27) - [x] `app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt` - 캐릭터 상세 진입 전 `ensureLoginAndAuth`를 KR/비KR 분기로 조정 - [x] `app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt` - 캐릭터 상세 진입 전 `ensureLoginAndAuth`를 KR/비KR 분기로 조정 ### 11) KR 인증완료 + 민감 콘텐츠 OFF 케이스 동작 일치화 (2026-03-27) - [x] `app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt` - `ensureLoginAndAdultAuth`(19금 라이브 상세)에서 KR 인증완료 상태라도 `isAdultContentVisible == false`면 설정 가이드로 이동 - `ensureLoginAndAuth`(캐릭터 상세)에서 KR 인증완료 상태라도 `isAdultContentVisible == false`면 설정 가이드로 이동 - [x] `app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt` - `ensureLoginAndAdultAuth`(19금 라이브 상세)에서 KR 인증완료 + 민감 콘텐츠 OFF를 비KR OFF와 동일 처리 - [x] `app/src/main/java/kr/co/vividnext/sodalive/live/now/all/LiveNowAllActivity.kt` - `ensureLoginAndAdultAuth`(19금 라이브 상세)에서 KR 인증완료 + 민감 콘텐츠 OFF를 비KR OFF와 동일 처리 - [x] `app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt` - `ensureLoginAndAuth`(캐릭터 상세)에서 KR 인증완료 + 민감 콘텐츠 OFF를 비KR OFF와 동일 처리 ## 영향 파일(예상) ### 필수 - `app/src/main/java/kr/co/vividnext/sodalive/settings/notification/GetMemberInfoResponse.kt` - `app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt` - `app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt` - `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt` - `app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt` - `app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt` - `app/src/main/java/kr/co/vividnext/sodalive/settings/SettingsActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsViewModel.kt` - `app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` ### 신규 파일(예상) - `app/src/main/java/kr/co/vividnext/sodalive/settings/UpdateContentPreferenceRequest.kt` - `app/src/main/java/kr/co/vividnext/sodalive/settings/UpdateContentPreferenceResponse.kt` ## 리스크 및 확인사항 - `contentType`은 현재 로컬에서 사용하는 `ContentType`과 동일 타입을 사용한다. - 멤버 정보 API 호출 시점은 앱 실행 후 3번째 실행 흐름으로, 메뉴 노출 조건 반영 타이밍상 문제는 없는 것으로 본다. - PATCH 실패 시 직전 값으로 롤백한다(값 변경 시 API 호출 + LoadingDialog 표시 구간에서 사용자 대기가 가능한 UX를 전제로 함). - 연타 + 네트워크 지연 상황에서 LoadingDialog 과도 점멸을 방지하기 위해 표시 조건을 실제 API 요청 구간으로 제한한다(반영). ## 검증 계획 - 정적 확인: 응답/요청 DTO 필드, 메뉴 노출 조건식, debounce 상태 변수 흐름 점검 - 자동 검증: 단위 테스트 및 디버그 빌드 성공 확인 - 수동 검증: 한국/비한국 노출 케이스, 토글 연타 케이스, 마지막 값 전송 케이스 ## 검증 기록 - 기록 템플릿(후속 누적): - YYYY-MM-DD - 무엇/왜/어떻게: - 실행 명령/도구: - `명령 또는 사용 도구` - 결과: - 2026-03-26 - 무엇/왜/어떻게: `/member/info` 응답 확장, 설정 메뉴 노출 확장, `/member/content-preference` PATCH, Non-Rx debounce 제약을 반영한 구현 계획 문서를 작성했다. - 실행 명령/도구: - `read(docs/*, UserApi.kt, GetMemberInfoResponse.kt, SettingsActivity.kt, ContentSettingsActivity.kt, ContentSettingsViewModel.kt, SharedPreferenceManager.kt, AppDI.kt)` - `grep("/member/info|content-preference|isAuth|countryCode|ContentType")` - `apply_patch(docs/20260326_멤버정보응답확장및콘텐츠보기설정동기화.md 생성)` - 결과: - 요구사항과 추가 제약("debounce는 RxJava 미사용")이 반영된 체크리스트 중심 계획 문서를 생성했다. - 2026-03-26 - 무엇/왜/어떻게: 사용자 피드백에 따라 리스크/확인사항을 확정 대응안으로 갱신하고, `/member/info` 수신 필드 3종의 로컬 저장소 동기화 요구를 문서에 명시했다. - 실행 명령/도구: - `read(docs/20260326_멤버정보응답확장및콘텐츠보기설정동기화.md)` - `apply_patch(docs/20260326_멤버정보응답확장및콘텐츠보기설정동기화.md 수정)` - 결과: - `contentType 동일 타입 사용`, `멤버 정보 호출 타이밍 이슈 없음`, `PATCH 실패 롤백`, `LoadingDialog API 구간 제한`이 문서에 반영되었다. - `/member/info` 확장 필드(`countryCode`, `isAdultContentVisible`, `contentType`)의 로컬 저장소 동기화 요구가 AC/체크리스트에 반영되었다. - 2026-03-26 - 무엇/왜/어떻게: 계획 문서 기준으로 `/member/info` 응답 확장 필드 로컬 동기화, 설정 메뉴 노출 조건 확장, `/member/content-preference` PATCH 연동, ContentSettings의 Non-Rx debounce + in-flight/pending 마지막 값 전송 보장, LoadingDialog 연동을 구현했다. - 실행 명령/도구: - `task(explore x2)` - `grep/read(app/src/main/java/**)` - `apply_patch(소스 12개 파일 + DTO 2개 파일)` - `lsp_diagnostics(수정된 .kt 파일 일괄 시도)` - `./gradlew :app:testDebugUnitTest` - `./gradlew :app:assembleDebug` - 결과: - `GetMemberInfoResponse`에 `countryCode/isAdultContentVisible/contentType`가 추가되고 `MainViewModel.getMemberInfo()`에서 `SharedPreferenceManager`로 동기화된다. - `SettingsActivity`의 `콘텐츠 보기 설정` 노출 조건이 `isAuth || countryCode != "KR"`로 확장되었다. - `UserApi/UserRepository`에 `/member/content-preference` PATCH와 요청/응답 DTO가 추가되었다. - `ContentSettingsViewModel`에 `Handler + Runnable` 기반 debounce와 in-flight/pending 제어를 적용해 연타 시 마지막 상태만 서버로 전송되며, 요청 중에만 `isLoading`이 true가 된다. - `ContentSettingsActivity`가 `isLoading`을 관찰해 `LoadingDialog`를 표시/해제하고 오류 토스트를 노출한다. - `lsp_diagnostics`는 현재 환경에 Kotlin LSP가 없어 실행 불가 메시지를 확인했다. - 자동 검증: `:app:testDebugUnitTest`, `:app:assembleDebug` 모두 성공했다. - 2026-03-26 - 무엇/왜/어떻게: 정적 검사 대체 검증과 실제 단말 스모크 실행으로 구현 안정성을 추가 확인했다. - 실행 명령/도구: - `./gradlew :app:ktlintCheck` - `adb devices` - `adb install -r app/build/outputs/apk/debug/app-debug.apk` - `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1` - `adb shell dumpsys activity | grep -E "ResumedActivity|mFocusedApp|topResumedActivity|mCurrentFocus"` - `adb logcat -d -s AndroidRuntime` - `adb shell pidof kr.co.vividnext.sodalive.debug` - 결과: - `ktlintCheck`는 기존 코드베이스(주로 `LiveRoomActivity` 등) 기등록 스타일 위반으로 실패했다. - 이번 변경 파일 기준으로는 `ContentSettingsActivity`, `GetMemberInfoResponse` 지적 항목을 정리 후 재빌드하여 `:app:testDebugUnitTest`, `:app:assembleDebug` 재성공을 확인했다. - 디버그 APK 설치 및 런처 실행(monkey) 후 `SplashActivity`가 resumed 상태이며 앱 프로세스(pid) 실행 중임을 확인했다. - 2026-03-27 - 무엇/왜/어떻게: 추가 요청에 따라 `MyPageFragment`의 본인인증/인증완료 아이템을 한국 접속국가에서만 노출하도록 분기했다. - 실행 명령/도구: - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)` - `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt)` - `./gradlew :app:testDebugUnitTest` - `./gradlew :app:assembleDebug` - 결과: - `countryCode.ifBlank { "KR" } == "KR"` 조건에서만 `btnIdentityVerification`이 표시되며, 비KR에서는 숨김 처리된다. - `lsp_diagnostics`는 현재 환경에서 Kotlin LSP 미구성으로 실행 불가 메시지를 확인했다. - 자동 검증: `:app:testDebugUnitTest`, `:app:assembleDebug` 모두 성공했다. - 2026-03-27 - 무엇/왜/어떻게: `/member/content-preference` 요청에 변경된 값만 전송하기 위해 request 필드를 optional로 전환하고, `confirmedState` 대비 변경 필드만 body에 담아 PATCH 호출하도록 조정했다. - 실행 명령/도구: - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/settings/UpdateContentPreferenceRequest.kt)` - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsViewModel.kt)` - `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/settings/UpdateContentPreferenceRequest.kt)` - `lsp_diagnostics(app/src/main/java/kr/co/vividnext/sodalive/settings/ContentSettingsViewModel.kt)` - `./gradlew :app:testDebugUnitTest :app:assembleDebug` - 결과: - `UpdateContentPreferenceRequest`의 `isAdultContentVisible`, `contentType`가 nullable optional로 변경되었다. - `ContentSettingsViewModel.syncContentPreference()`에서 `confirmedState`와 비교해 변경된 필드만 request에 포함하며, 변경 필드가 없으면 API 호출을 생략한다. - `lsp_diagnostics`는 현재 환경에서 Kotlin LSP 미구성으로 실행 불가 메시지를 확인했다. - 자동 검증: `:app:testDebugUnitTest`, `:app:assembleDebug` 모두 성공했다. - 2026-03-27 - 무엇/왜/어떻게: 캐릭터 터치 후 상세 진입 시 사용되는 `ensureLoginAndAuth` 인증 체크를 접속국가 기준으로 분기했다. KR은 기존 본인인증 다이얼로그 흐름을 유지하고, 비KR은 `isAdultContentVisible`이 꺼져 있으면 `ContentSettingsActivity`로 이동해 민감한 콘텐츠 스위치 안내 흐름을 사용하도록 맞췄다. - 실행 명령/도구: - `task(explore x2: 캐릭터 상세 진입 경로/인증 게이트 탐색)` - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt)` - `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterTabFragment.kt)` - `lsp_diagnostics(HomeFragment.kt, CharacterTabFragment.kt)` - `./gradlew :app:testDebugUnitTest :app:assembleDebug` - `adb devices` - `adb install -r app/build/outputs/apk/debug/app-debug.apk` - `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1` - `adb shell uiautomator dump /sdcard/window_dump.xml` - `adb pull /sdcard/window_dump.xml /tmp/sodalive_window_dump.xml` - 결과: - `HomeFragment`와 `CharacterTabFragment`의 캐릭터 상세 진입 전 인증 체크가 `countryCode.ifBlank { "KR" } == "KR"` 기준으로 분기된다. - KR에서 미인증이면 기존 인증 다이얼로그/인증 플로우를 유지한다. - 비KR에서 `isAdultContentVisible == false`이면 `ContentSettingsActivity`로 이동하고 `EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE`를 전달한다. - `lsp_diagnostics`는 현재 환경에서 Kotlin LSP 미구성으로 실행 불가 메시지를 확인했다. - 자동 검증: `:app:testDebugUnitTest`, `:app:assembleDebug` 모두 성공했다. - 수동 검증 시도 중 ADB 장치 연결이 해제되어 캐릭터 탭 실제 터치 시나리오를 끝까지 완료하지 못했다. - 2026-03-27 - 무엇/왜/어떻게: 한국 접속 + 본인인증 완료 상태에서도 `isAdultContentVisible == false`이면 19금 라이브 상세와 채팅 캐릭터 상세를 비KR OFF와 동일하게 처리하도록 게이트 조건을 조정했다. - 실행 명령/도구: - `task(explore: live/character gate 위치 점검)` - `grep("ensureLoginAndAdultAuth|ensureLoginAndAuth|onCharacterClick")` - `apply_patch(HomeFragment.kt, LiveFragment.kt, LiveNowAllActivity.kt, CharacterTabFragment.kt)` - `lsp_diagnostics(수정된 .kt 파일 4개)` - `./gradlew :app:testDebugUnitTest :app:assembleDebug` - `adb devices` - `adb shell am force-stop kr.co.vividnext.sodalive.debug` - `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1` - `adb shell uiautomator dump /sdcard/window_dump.xml` - 결과: - KR에서 `isAuth=true`라도 `isAdultContentVisible=false`이면 19금 라이브 상세/캐릭터 상세 진입 시 `ContentSettingsActivity` 안내 흐름으로 분기된다. - 기존 KR 미인증 케이스의 본인인증 다이얼로그 흐름은 유지된다. - 자동 검증: `:app:testDebugUnitTest`, `:app:assembleDebug` 성공. - `lsp_diagnostics`는 현재 환경에서 Kotlin LSP 미구성으로 실행 불가 메시지를 확인했다. - 수동 QA는 디바이스 연결이 반복 해제되어 조건 기반 실제 탭 시나리오를 끝까지 수행하지 못했다.