# V2 공통 접근 가드와 도메인 액션 Plan/Task ## Assumptions - 작업 범위는 `kr.co.vividnext.sodalive.v2` 하위 신규/기존 코드, 대응 테스트, 사용자 승인을 받은 레거시 `DeepLinkActivity`, 필요한 경우 이를 조립하는 `AppDI.kt`, 본 작업 문서로 제한한다. - 화면별 최초 집계 API와 Response는 feature가 계속 소유할 수 있다. - 공통화 대상은 최소 두 호출부에서 반복되거나 모든 화면에서 동일해야 하는 정책과 사용자 행동이다. - 기존 UX, API, Intent extra, Activity result, 화면 새로고침 동작을 보존한다. - 직접적인 하위 Activity 결과는 `ActivityResult -> 명시적 도메인 결과 -> feature 단일 handler`를 기본 전달 방식으로 사용한다. - 현재 화면 안에서 Action/Repository가 직접 반환하는 결과에는 불필요하게 `ActivityResult`를 도입하지 않고 같은 handler를 호출한다. - `SharedFlow`/observer는 Activity 밖의 독립 소비자가 다수 존재하거나 백그라운드 변경 전파 요구가 확인될 때만 사용한다. - 레거시 기능은 수정하지 않고 `v2` adapter/wrapper에서 호출한다. 단, V2 푸시/딥링크 진입점으로 계속 사용할 `DeepLinkActivity`의 Action 연결 변경은 사용자가 승인했다. - 구현은 Access부터 도메인별 Phase로 진행하며 각 Phase를 독립적으로 검증한다. - 모든 도메인에 동일한 형식의 클래스를 강제하지 않고 실제 중복과 정책이 확인된 최소 단위만 추출한다. - `AppDI.kt`는 실제 외부 의존성, 공유 생명주기 또는 구현 교체가 필요한 계약만 조립한다. 상태 없는 Action/Handler는 호출 경계에서 직접 조합할 수 있으며 DI 등록 자체를 완료 조건으로 삼지 않는다. ### 최종 소유 경계 - Access: 접근 허용 판단과 기존 로그인/본인인증/성인 설정 UX 실행을 소유한다. - 도메인 Action: 안정적인 ID/command 검사, 필요한 `AccessRequirement` 선택, Access 실행 요청과 `Ignored`/`Blocked`/도메인 결과 반환을 소유한다. - Action Handler: Access 실행기 주입과 레거시 `Intent`/Activity/Dialog/navigation adapter를 소유한다. - feature: UI model -> command 변환, 화면별 최초 query/API, 단일 화면 생성·mutation API, `ActivityResult` 수신과 refresh/callback/projection 조합을 소유한다. - 보호된 도메인 Action의 호출부는 같은 Access guard를 바깥에서 중복 실행하지 않는다. 공통 Access 직접 호출은 화면 진입 자체 또는 화면 전용 동작에 사용한다. - 도메인 관련 코드를 모두 이관하지도, 페이지 이동만 이관하지도 않는다. 여러 호출부에서 같아야 하는 유효성·접근 정책·결과 계약은 Action으로, Android 화면 이동은 Handler로 이관하고 화면별 query/API와 후처리는 feature에 유지한다. - parent-child Fragment composition, same-feature child flow, 도메인 Action이 없는 system-only route, Home on-air 목록 화면 진입, 단일 owner 기능은 합의된 반복 정책이 없으면 feature에 유지한다. ## Success Criteria - `v2` 로그인/성인 접근 판단의 단일 소유자가 존재한다. - Activity와 Fragment가 동일한 Access 공개 진입점을 사용한다. - 합의된 도메인별 반복 동작이 단일 Action 공개 진입점을 사용한다. - Action 입력은 화면 전용 UI model/DTO가 아닌 ID, 값 또는 명시적 command다. - 화면별 최초 query와 UI composition은 유지된다. - Community mutation은 명시적 변경 결과를 반환하고 Creator Channel의 단일 composition handler가 Home/Community projection 갱신을 결정한다. - Activity result를 사용하는 도메인 흐름은 raw result를 화면마다 해석하지 않고 명시적 결과와 단일 handler를 사용한다. - feature 간 직접 Activity/Coordinator 의존 중 합의된 반복 navigation/정책 대상은 공통 Action 또는 Handler 계약으로 대체되고, 최종 소유 경계의 예외는 근거와 함께 유지된다. - 푸시/딥링크의 Content, Creator, Community, Chat 이동은 기존 Action을 사용하고 Live 이동은 `LiveActionCoordinator`를 사용한다. - Live room ID는 channel ID보다 우선하며, Community post ID는 creator ID보다 우선한다. - 각 Phase의 테스트, 컴파일, ktlint, whitespace 검증 결과가 문서에 누적된다. ### Phase 0: 기준선과 Action 카탈로그 확정 - [x] **Task 0.1: `v2` 구조와 중복 접근 구현 기준선 조사** - 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/` - `app/src/test/java/kr/co/vividnext/sodalive/v2/` - 확인 내용: - 운영 Kotlin 258개, 약 25,294줄 - 테스트 Kotlin 127개, 약 23,809줄 - `SharedPreferenceManager` 직접 참조 29개 파일, 92곳 - API 11개, Repository 11개, ViewModel 22개, Activity/Fragment 20개 - 검증 기준: 전체 재배치가 아닌 반복 동작별 점진 전환이 필요한 근거를 기록한다. - 검증 기록: - 2026-07-14: `find`, `wc`, `rg`로 `v2` 운영/테스트 코드 규모와 직접 의존을 확인했다. `widget` 64개는 UI 재사용 컴포넌트이므로 초기 도메인 Action 전환 대상에서 제외한다. - [x] **Task 0.2: 초기 Action 카탈로그 작성** - 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: Access, Content, Live, Creator, Community, Chat의 초기 공통 동작과 호출 화면을 기록한다. - 검증 기준: 화면별 집계 query와 공통 action을 구분하고, FanTalk/Donation/Schedule은 실제 재사용 수요 확인 전 추출 대상에서 제외한다. - 검증 기록: - 2026-07-14: 기존 호출부 조사 결과 Access, 오디오/시리즈 진입, Live 상세/입장, Creator Channel 진입, Community 상세, Chat/DM 진입을 초기 후보로 확정했다. - [x] **Task 0.3: 구현 직전 호출부 및 동작 차이 상세 목록 확정** - 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/` - `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageFragment.kt` (호출부 확인만 수행하고 수정하지 않음) - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: 각 Action별 호출 함수, 접근 조건, Intent extra, Activity result, callback/refresh 차이를 기록한다. - 검증 기준: - 공통 계약으로 보존할 동작과 화면에 남길 후처리가 구분된다. - Activity result 흐름은 adapter에서 만들 명시적 결과와 feature 단일 handler 후보가 기록된다. - observer가 필요한 흐름은 Activity 밖의 소비자 수, lifecycle, replay 요구사항이 함께 기록된다. - 확인 결과: - Access: `HomeMainFragment`, `ContentMainFragment`, `ChatMainFragment`는 기존 `ensureMainV2NavigationAllowed`를 사용했고, `CreatorChannelActivity`, `HomeOnAirLiveActivity`는 자체 `ensureLoginAndAdultAuth`를 중복 구현하고 있었다. - Access: `MainV2Activity`는 chat tab, audio notification route처럼 로그인 차단 UX가 필요한 navigation guard와 push token/member/deeplink 초기화처럼 부수효과 없이 허용 여부만 확인하는 흐름이 섞여 있었다. Phase 1에서는 두 용도를 `ensureV2Access(AccessRequirement.Login)`와 `isV2AccessAllowed(AccessRequirement.Login)`로 분리해 기존 route 처리 순서와 invalid audio content id 반환 계약을 유지한다. - Content: `ContentMainFragment`의 오디오/시리즈 상세 진입은 ID 검사 후 판단 가능한 성인 여부를 guard에 전달하며, banner/overview/전체 탭 이동은 login guard만 필요하다. - Live: `HomeMainFragment`와 `HomeOnAirLiveActivity`는 로그인 후 room detail을 조회하고, detail의 `isAdult`로 성인 Access를 다시 검사한다. Creator Channel 현재 라이브도 동일 입장 흐름을 사용한다. - Creator/Community: `CreatorChannelActivity`는 Activity 자체 진입 로그인 guard와 Community/FanTalk/Live ActivityResult refresh를 소유한다. Phase 1에서는 result adapter를 만들지 않고 기존 launcher/refresh를 유지한다. - Chat: title bar 및 Home following chat은 login guard 후 기존 AI/DM intent helper를 사용한다. DM room result adapter나 observer 변경은 Phase 5 대상으로 유지한다. - Legacy MyPage: `MyPageFragment.showLoginActivity`는 실행 Activity가 `MainV2Activity`이면 공개 `showLoginActivity()`를 호출하므로 해당 호환 진입점을 유지해야 한다. #### 상세 호출부 및 계약 | Action 후보 | 호출부 | Access 조건 | Intent extra / 입력 | Result / callback 차이 | Phase 1 처리 | | --- | --- | --- | --- | --- | --- | | Login/Adult Access | `HomeMainFragment.openWithLoginGuard`, `openFollowingTab`, title bar cash/search/bell, `ContentMainFragment` title/overview/banner/detail, `ChatMainFragment` title bar, `CreatorChannelActivity` community/FanTalk/live, `HomeOnAirLiveActivity` live entry | Login 또는 `forAdultContent(isAdult)` | 화면별 기존 Activity/Intent helper 그대로 사용 | 화면별 후처리는 호출부에 유지 | `ensureV2Access`로 전환하고 정책 판단만 공통화 | | MainV2 chat tab | `MainV2Activity.selectChatTabWithLoginGuard`, `openChatTab`, initial chat filter | Login | `EXTRA_CHAT_FILTER`는 기존 consume/open 흐름 유지 | 탭 전환은 허용 시에만 `viewModel.clickTab(CHAT)` | `ensureV2Access(Login)` 사용, legacy `showLoginActivity()`는 guard 위임만 유지 | | Legacy MyPage login bridge | `MyPageFragment.showLoginActivity` -> `MainV2Activity.showLoginActivity` | Login | `MainV2Activity.intent.extras` | 레거시 Activity 타입 분기와 로그인 화면 이동 계약 유지 | `MainV2Activity.showLoginActivity()`를 유지하고 `ensureV2Access(Login)`에 위임 | | MainV2 deeplink/audio notification | `onCreate`, `onNewIntent`, `handleAudioNotificationRoute`, `executeDeeplink` | Login 허용 여부 확인 또는 Login guard | `EXTRA_DATA`, `deep_link`, `EXTRA_AUDIO_NOTIFICATION_ROUTE`, `EXTRA_AUDIO_CONTENT_ID` | audio route가 먼저 소비되고 invalid audio content id는 `false` 반환해 기존 후속 처리 가능 | route 순서와 반환 계약 보존, side-effect 없는 분기는 `isV2AccessAllowed(Login)` 사용 | | Content 상세/전체/overview | `ContentMainFragment.openAudioContentDetail`, `openSeriesDetail`, `openContentOverview`, `openContentAllTab` | Login 또는 콘텐츠 성인 여부에 따른 AdultContent | `Constants.EXTRA_AUDIO_CONTENT_ID`, series id, `ContentOverviewType`, `MainContentAllType`, `ContentSort` | Activity result 없음, 전체 탭 이동은 `MainV2Activity.openContentAllTab` 호출 | Phase 1에서는 guard만 공통화, Content Action 계약은 Phase 2에서 구현 | | Live entry | `HomeMainFragment.onLiveClick`, `HomeOnAirLiveActivity.enterLiveRoom`, `CreatorChannelActivity.onCreatorChannelCurrentLiveClicked` | Login 후 room detail의 `isAdult`로 AdultContent | room id/live id, room detail | 기존 `liveCoordinator.enterLiveRoom` 및 on-air 목록 refresh/상태는 호출부에 유지 | Phase 1에서는 중복 adult guard 제거, Live Action은 Phase 3 | | Creator / Community | `CreatorChannelActivity.onCreatorChannelCommunityPostClicked`, Home creator/profile/community 진입 | Login | creator id, post id, existing `newIntent` | Creator Channel의 Home/Community/FanTalk refresh 조합과 launcher는 Activity 소유 유지 | Phase 1에서는 login guard만 공통화, result adapter는 Phase 4 | | Chat / DM | `HomeMainFragment.openFollowingChat`, title bar search/cash, `MainV2Activity` chat route | Login | AI room id, DM room id, existing chat intent helpers | DM room result adapter/observer 없음, 기존 Activity 전환만 수행 | Phase 1에서는 login guard만 공통화, Chat Action은 Phase 5 | #### observer 판단 - Phase 1 Access는 one-shot UI navigation/action 차단이므로 전역 observer나 EventBus를 추가하지 않는다. - ActivityResult/refresh가 있는 Creator Channel Community/FanTalk는 기존 launcher와 Activity 내부 composition handler 후보를 Phase 4에서 정리한다. - Live room entry는 detail 조회 후 접근 판단이 필요하므로 repository/result observer가 아니라 호출부 callback 조합을 유지하고 Phase 3에서 Action 계약을 분리한다. #### 검증 기록 - 2026-07-14: `rg`와 `explore` 조사로 Access, Content, Live, Creator, Community, Chat 후보의 호출부와 차이를 확정했다. Phase 1 구현 범위는 Access로 제한하고, Content 이후 도메인 Action은 카탈로그 상태로 유지한다. - 2026-07-14: 리뷰 보완으로 `MainV2Activity`의 직접 `isLoggedIn()`/`showLoginActivity()` 소유 여부를 재확인했고, 로그인 UX 실행이 필요한 경로와 단순 허용 여부 확인 경로를 분리해 공통 Access 정책으로 이관하기로 확정했다. - 2026-07-14: 리뷰 게이트 보완으로 Action 후보별 호출 함수, Access 조건, Intent 입력, Result/callback 차이, observer 판단을 표로 추가해 Task 0.3 검증 기준을 충족하도록 문서를 보강했다. - 2026-07-14: Phase 0/1 리뷰 답변에 따라 레거시 `MyPageFragment`에서 `MainV2Activity.showLoginActivity()`를 호출하는 호환 경로를 상세 호출부 표에 추가했다. 레거시 파일은 수정하지 않았다. ### Phase 1: V2 공통 Access 정책과 실행기 - [x] **Task 1.1: Access 판단 단위 테스트 작성** - 생성 예정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/access/AccessPolicyTest.kt` - 검증 대상: - 로그인 필요 요구사항의 로그인/미로그인 결과 - 성인 콘텐츠 요구사항의 미로그인 결과 - 국내 미인증 사용자의 본인인증 필요 결과 - 국외 사용자의 본인인증 생략 - 성인 콘텐츠 표시 설정 비활성 결과 - 모든 조건 충족 시 허용 결과 - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.AccessPolicyTest" ``` - 기대 결과: 구현 전 RED, 구현 후 GREEN. - 검증 기록: - 2026-07-14: 테스트를 먼저 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.AccessPolicyTest"`를 실행해 `AccessPolicy`, `AccessRequirement`, `AccessDecision`, `AccessState` 미정의 컴파일 오류로 RED를 확인했다. - [x] **Task 1.2: Access 입력·출력 계약과 순수 정책 구현** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessRequirement.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessDecision.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt` - 작업: 로그인, 국가/본인인증, 성인 콘텐츠 표시 설정을 명시적 입력과 결과로 판단한다. - 검증 기준: Android, Activity, Fragment, Dialog, Retrofit 의존 없이 Task 1.1 테스트가 통과한다. - 검증 기록: - 2026-07-14: `AccessRequirement`, `AccessDecision`, `AccessState`, `AccessPolicy`를 추가하고 같은 단일 테스트 명령을 재실행해 GREEN을 확인했다. - [x] **Task 1.3: Activity/Fragment 공용 Access 실행기 구현** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt` - 수정 가능 파일: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - `AccessDecision`을 로그인, 본인인증, 콘텐츠 설정 UX로 연결한다. - 기존 `LoginActivity`, `V2ModalDialog`, `Auth.auth`, `MyPageViewModel.authVerify`, `ContentSettingsActivity`를 재사용한다. - Activity와 Fragment에서 같은 공개 함수를 호출할 수 있게 한다. - 검증 기준: - `Allowed`에서만 원래 action을 한 번 실행한다. - 거부 결과에서 원래 action을 실행하지 않는다. - 기존 Intent flag/extra와 인증 완료 후 흐름을 유지한다. - 검증 기록: - 2026-07-14: `V2AccessGuard`를 추가해 Activity/Fragment 공용 `ensureV2Access`를 제공하고, 기존 `LoginActivity` extra/flag, `V2ModalDialog`, `Auth.auth`, `MyPageViewModel.authVerify`, `SplashActivity`, `ContentSettingsActivity` extra 계약을 source test로 고정했다. - 2026-07-14: `MainV2Activity`의 초기화/조건부 실행처럼 로그인 UX를 띄우지 않아야 하는 경로를 위해 같은 `AccessPolicy`와 `AccessStateProvider`를 사용하는 `isV2AccessAllowed`를 추가했다. - 2026-07-14: 리뷰 보완으로 기존 `MainV2Activity.isLoggedIn()`의 `token.isNotBlank() && token.length > 10` 기준을 `AccessPolicy`에 보존하고, 10자/11자/공백 토큰 경계 테스트를 추가했다. - 2026-07-14: 레거시 `MyPageFragment` 본문을 수정하지 않고 기존 `MainV2Activity.showLoginActivity()` 호출 계약을 유지하기 위해 `MainV2Activity`에 얇은 호환 메서드를 남기고 내부에서 `ensureV2Access(AccessRequirement.Login)`을 호출하게 했다. - 2026-07-14: 사용자 확인 결과 현재 `token.length > 10` 기준은 실제 토큰 계약에서 동작 변경을 만들지 않으므로 추가 코드 변경 없이 유지한다. - [x] **Task 1.4: 기존 Access 구현을 공통 실행기로 전환** - 수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuard.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 작업: - 기존 호출부를 `V2AccessGuard`로 전환한다. - 화면 진입 guard와 클릭 action guard의 기존 종료/후처리 차이를 보존한다. - 모든 호출부 전환 후 중복 `ensureLoginAndAdultAuth` 구현을 제거한다. - 검증 기준: - `CreatorChannelActivity`, `HomeOnAirLiveActivity`에 별도 로그인/성인 판단 함수가 남지 않는다. - 대상 클릭 handler에 직접 token 조건문이 남지 않는다. - 검증 기록: - 2026-07-14: `HomeMainFragment`, `ContentMainFragment`, `ChatMainFragment`, `CreatorChannelActivity`, `HomeOnAirLiveActivity` 호출부를 `ensureV2Access`와 `AccessRequirement`로 전환했다. `MainV2LoginGuard.kt`와 Activity별 `ensureLoginAndAdultAuth` 구현을 제거했다. - 2026-07-14: `MainV2Activity`의 chat tab, audio notification route는 `ensureV2Access(AccessRequirement.Login)`로 전환하고, deeplink 실행 전 조건, push token update, member info 조회 등은 `isV2AccessAllowed(AccessRequirement.Login)`로 전환했다. Activity 내부 직접 `isLoggedIn()` 구현은 제거했고, 기존 외부 호출 호환용 `showLoginActivity()`는 공통 guard 위임만 수행한다. - 2026-07-14: `rg -n "ensureMainV2NavigationAllowed|ensureLoginAndAdultAuth" app/src/main/java/kr/co/vividnext/sodalive/v2 app/src/test/java/kr/co/vividnext/sodalive/v2 --glob '*.kt'` 결과 production 잔존은 없고, source test의 제거 확인 assertion만 남았다. - 2026-07-14: 대상 UI 파일의 `SharedPreferenceManager.(token|countryCode|isAuth|isAdultContentVisible)` 검색 결과 Creator Channel의 API auth header 생성 함수만 남았고, 접근 정책 직접 판단은 제거됐다. - [x] **Task 1.5: 기존 Access source test를 새 계약에 맞게 전환** - 수정 예정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2AudioNotificationRouteSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainTitleBarActionSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentLoginGuardSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainTitleBarActionSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainTitleBarActionSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - 작업: 이전 함수명 고정 assertion을 새 공통 Access 사용 및 기존 UX 보존 검증으로 최소 수정한다. - 검증 기준: 정책은 `AccessPolicyTest`, 호출 연결은 필요한 source test가 담당한다. - 검증 기록: - 2026-07-14: `MainV2LoginGuardSourceTest`, Home/Content/Chat title bar source test, Home/Content login guard source test, `CreatorChannelActivitySourceTest`의 Access 관련 assertion을 새 `V2AccessGuard` 계약 기준으로 전환했다. - 2026-07-14: `MainV2ActivitySourceTest`와 `MainV2AudioNotificationRouteSourceTest`를 새 `ensureV2Access`/`isV2AccessAllowed` 경계 기준으로 갱신하고, `MainV2Activity`에 직접 `isLoggedIn()` 판단 함수가 남지 않음을 고정했다. - 2026-07-14: `MainV2LoginGuardSourceTest`에 `MainV2Activity.showLoginActivity()`가 기존 호출명은 유지하되 `ensureV2Access(AccessRequirement.Login)`으로 위임하는지 확인하는 assertion을 추가했다. - [x] **Task 1.6: Access Phase 검증 및 문서 반영** - 파일: - `docs/agent-guides/build-test-style.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck --rerun-tasks git diff --check HEAD ``` - 기대 결과: 신규 Access 테스트와 영향받은 기존 테스트, 컴파일, whitespace 검증이 통과한다. `ktlintCheck`는 전체 기준선을 재실행하고, 저장소 기존 위반으로 실패하면 Phase 1 변경 파일에 신규 위반이 없음을 완료 gate로 확인한다. - 검증 기록: - 2026-07-14: Access 관련 좁은 회귀 명령 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.현재 라이브 카드는 동일한 enterLiveRoom 플로우로 입장한다" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.커뮤니티 게시물 클릭 source는 상세 Activity newIntent로 진입하고 invalid postId를 무시한다" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.FanTalk fixed plus source는 Activity overlay와 write result refresh 계약을 연결한다" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.FanTalk item click source는 owner gate와 상세 ActivityResult refresh를 연결한다"`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: 더 넓은 관련 테스트 묶음은 497개 중 `CreatorChannelActivitySourceTest > 팬Talk 섹션은 Figma ListComment layout과 bind로 렌더링한다` 1개가 `@drawable/ic_chevron_down_white` source assertion으로 실패했다. Access 변경 파일과 무관한 기존 FanTalk layout assertion으로 확인해 별도 관찰로 남긴다. - 2026-07-14: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: `./gradlew :app:ktlintCheck`는 이번 변경과 무관한 `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt:118-120` 기존 max line length/argument wrapping 위반으로 실패했다. 해당 파일에는 이번 diff가 없음을 `git diff -- app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt`로 확인했다. - 2026-07-14: `git diff --check`는 whitespace 오류 없이 통과했다. - 2026-07-14: `MainV2LoginGuardSourceTest`의 신규 adapter assertion line length를 수정한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.*" && ./gradlew :app:compileDebugKotlin && ./gradlew :app:ktlintCheck && git diff --check`를 재실행해 모두 `BUILD SUCCESSFUL` 또는 무출력 통과를 확인했다. - 2026-07-14: 리뷰 게이트에서 `token.isNotBlank() && token.length > 10` 보존과 `mypage` adapter 제거가 차단 이슈로 확인되어, `AccessPolicy` 로그인 기준과 `MainV2Activity.showLoginActivity()` 호환 메서드 위임 방식으로 보완했다. - 2026-07-14: 보완 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.*" && ./gradlew :app:compileDebugKotlin && ./gradlew :app:ktlintCheck && git diff --check`를 재실행해 모두 `BUILD SUCCESSFUL` 또는 무출력 통과를 확인했다. - 2026-07-14: 추가 리뷰에서 11자 이상 공백 토큰이 허용되는 회귀를 확인해 `AccessPolicy`에 `isNotBlank()` 조건을 복원하고 공백 토큰 회귀 테스트와 PRD 문구를 보강했다. - 2026-07-14: Kotlin LSP 진단은 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행할 수 없었고, Gradle compile/test로 대체 검증했다. - [x] **Task 1.7: 리뷰 보완으로 Access 실행 계약과 source test 범위 강화** - 생성 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuardTest.kt` - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `docs/agent-guides/build-test-style.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - `AccessDecision` 실행 분기를 Android 비의존 함수로 분리해 `Allowed` callback과 거부 UX callback 실행 횟수를 실제 입력·출력으로 검증한다. - Creator Channel 커뮤니티 게시물 클릭 source test가 Activity 전체가 아닌 해당 handler 본문에서 guard와 launcher 순서를 확인하게 한다. - Task 1.4와 Task 1.5의 실제 변경 파일 목록을 현재 diff와 일치시킨다. - 검증 기준: - `Allowed`는 원래 action을 정확히 한 번 실행하고 거부 UX callback은 실행하지 않는다. - 모든 거부 결과는 원래 action을 실행하지 않고 대응 UX callback만 정확히 한 번 실행한다. - 커뮤니티 게시물 handler 밖의 다른 로그인 guard로 source test가 통과하지 않는다. - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.V2AccessGuardTest" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.커뮤니티 게시물 클릭 source는 상세 Activity newIntent로 진입하고 invalid postId를 무시한다" ``` - 기대 결과: 실행 계약 테스트는 구현 전 RED, 구현 후 GREEN이고 강화된 source test가 통과한다. - 검증 기록: - 2026-07-14: `V2AccessGuardTest`를 먼저 추가하고 단일 테스트를 실행해 `executeAccessDecision` 미정의 컴파일 오류로 RED를 확인했다. - 2026-07-14: 기존 `V2AccessGuard.handleDecision`의 분기를 Android 비의존 `executeAccessDecision`으로 추출한 뒤 같은 단일 테스트가 `BUILD SUCCESSFUL`로 전환되는 것을 확인했다. 네 가지 `AccessDecision`별 반환값과 실행 callback이 정확히 하나인지 검증한다. - 2026-07-14: Creator Channel 커뮤니티 handler source에서 guard 문자열을 일시 제거한 변이 입력으로 대상 테스트가 실패하는 것을 확인한 뒤, 실제 handler source 검사로 복원해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-14: Access, Main, On-air 전체와 변경 영향 Creator Channel 4개 테스트를 묶어 재실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-14: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew tasks --all`을 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. ktlint에는 기존 `.editorconfig`의 `disabled_rules` 속성 사용에 대한 deprecation warning이 남아 있다. - 2026-07-14: `git diff --check HEAD`를 실행해 staged와 unstaged를 합친 전체 변경에 whitespace 오류가 없음을 확인했다. - 2026-07-14: 사용자 확인에 따라 현재 layout에 존재하지 않아 기존 테스트를 실패시키던 `ic_chevron_down_white` source assertion은 수동 삭제한 상태를 유지한다. Phase 1 운영 코드 변경과는 무관한 기존 테스트 정리다. - [x] **Task 1.8: 코드 리뷰 후 ktlint 재현성 보완** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - 캐시를 배제한 `ktlintCheck`에서 재현된 기존 source test 3줄의 max line length와 argument wrapping 위반만 수정한다. - 기존 인증 문자열과 테스트 의미는 변경하지 않는다. - 검증 기준: - `./gradlew :app:ktlintCheck --rerun-tasks`로 전체 기준선을 재검사하고, Phase 1 변경 파일에 신규 위반이 없음을 보고서에서 확인한다. - 수정한 source test와 Access 관련 회귀 테스트가 통과한다. - `git diff --check HEAD`가 통과한다. - RED 검증 기록: - 2026-07-14: `./gradlew :app:ktlintCheck --rerun-tasks`를 실행해 `HomeFollowingFragmentSourceTest.kt:118-120`의 max line length와 argument wrapping 위반으로 `BUILD FAILED`를 재현했다. 해당 파일은 기존 Phase 1 diff에 없었으므로, 이전 통과 기록은 Gradle up-to-date 캐시에 의해 실제 전체 린트를 재실행하지 않은 결과였다. - GREEN 검증 기록: - 2026-07-14: 세 문자열 assertion만 ktlint 규칙에 맞게 줄바꿈하고 `HomeFollowingFragmentSourceTest`를 `--rerun-tasks`로 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-14: 수정 후 `./gradlew :app:ktlintCheck --rerun-tasks`를 재실행한 결과 `ktlintTestSourceSetCheck` 보고서는 0건이 되었고 Phase 1 변경 파일에서 위반이 검출되지 않았다. 전체 명령은 Phase 1 범위 밖 파일의 기존 ktlint 위반으로 실패했으며, 레거시 운영 코드 위반과 함께 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentCommentedAudioAdapter.kt:29`의 기존 V2 위반 1건도 포함되므로 전체 통과가 아닌 기준선 제약으로 기록한다. - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest" --rerun-tasks`를 실행해 Access 회귀와 수정한 source test가 `BUILD SUCCESSFUL`로 통과함을 확인했다. - 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - [x] **Task 1.9: 코드 리뷰 후 FanTalk source test와 ktlint gate 정정** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - FanTalk write와 item click source test가 Activity 전체가 아닌 해당 함수 본문에서 login guard와 launcher 순서를 확인하게 한다. - PRD의 ktlint 완료 기준을 현재 저장소 기준선에 맞춰 `ktlintCheck` 전체 재실행과 Phase 변경 파일 신규 위반 없음으로 명확히 한다. - 기존 ktlint 실패 기록에서 V2 파일 위반 1건을 레거시 위반으로 잘못 분류한 표현을 정정한다. - 검증 기준: - FanTalk write/item click handler 밖의 다른 `ensureV2Access` 문자열로 source test가 통과하지 않는다. - Phase 1 완료 판정은 전체 ktlint 통과로 오인되지 않고 기준선 제약과 변경 파일 신규 위반 여부를 함께 기록한다. - 관련 source test와 whitespace 검증이 통과한다. - 검증 기록: - 2026-07-14: `openFanTalkWrite`와 `onCreatorChannelFanTalkItemClicked` 본문만 `sourceSection`으로 추출해 guard, launcher, 실행 순서를 검증하도록 source test를 강화했다. - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.FanTalk fixed plus source는 Activity overlay와 write result refresh 계약을 연결한다" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest.FanTalk item click source는 owner gate와 상세 ActivityResult refresh를 연결한다" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-14: `./gradlew :app:ktlintCheck --rerun-tasks`는 `ktlintMainSourceSetCheck` 기준선 위반으로 실패했다. 보고서에는 기존 main source 위반 551줄이 남아 있고, 그중 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentCommentedAudioAdapter.kt:29`의 V2 max line length 위반 1건이 포함된다. - 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과해 이번에 수정한 source test 파일에는 신규 ktlint 위반이 없음을 확인했다. - 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - 2026-07-14: 리뷰어 게이트에서 P2/P3 반영, assertion 약화 없음, 문서 모순 없음에 대해 조건부 없는 `PASS`를 확인했다. - [x] **Task 1.10: Phase 0/1 재리뷰 결과 반영** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - `V2AccessGuard.handleDecision`이 각 거부 결과를 로그인, 본인인증, 콘텐츠 설정 UX callback에 연결하는 실제 결선을 source assertion으로 고정한다. - Task 1.6의 ktlint 기대 결과를 PRD의 최종 gate와 일치시킨다. - 검증 기준: - 로그인, 본인인증, 콘텐츠 설정 callback 결선이 서로 바뀌면 source test가 실패한다. - `ktlintCheck`는 전체 기준선을 재실행하고, 기존 위반으로 실패하면 Phase 1 변경 파일의 신규 위반이 없음을 확인한다. - 관련 회귀 테스트, 컴파일, test source ktlint, whitespace 검증이 통과한다. - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2LoginGuardSourceTest" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck --rerun-tasks ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks git diff --check HEAD ``` - 기대 결과: 결선 source test, 컴파일, test source ktlint, whitespace 검증이 통과하고 전체 ktlint 기준선 제약이 기존 기록과 일치한다. - 검증 기록: - 2026-07-14: `handleDecision` 본문만 추출해 로그인, 본인인증, 콘텐츠 설정 `AccessDecision`이 각각 `startLogin`, `showAuthDialog`, `startContentSettings` callback에 연결되는지 검증하도록 source test를 강화했다. - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2LoginGuardSourceTest" --rerun-tasks`를 실행해 테스트 1건이 실패 없이 통과하고 `BUILD SUCCESSFUL`임을 확인했다. - 2026-07-14: 위 회귀 테스트에서 production/test source를 새로 컴파일했으며, 별도 `./gradlew :app:compileDebugKotlin`도 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반 551줄로 실패했다. Phase 1 변경 main source의 신규 위반은 0건이고, V2 위반 1건은 변경하지 않은 `ContentCommentedAudioAdapter.kt:29`의 기존 max line length 위반이다. - 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했으며 test source 보고서 위반은 0건이다. - 2026-07-14: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과해 문서 변경 후 Gradle task 구성을 확인했다. - 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - [x] **Task 1.11: Phase 0/1 리뷰의 Activity별 Access 결선 테스트 보강** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveEntryPolicyTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - On-air 라이브 입장이 로그인 가드, room detail 조회, 성인 콘텐츠 가드, 기존 입장 함수 순서로 실행되는지 함수 본문 source로 검증한다. - Creator Channel 화면 진입이 유효한 creator ID 확인 후 로그인 가드를 거치며, 거부 시 화면 조립 전에 종료되는지 검증한다. - Creator Channel AI 채팅이 성인 콘텐츠 가드 통과 후에만 채팅방 생성을 요청하는지 클릭 listener 범위에서 검증한다. - 검증 기준: - 각 함수 밖의 다른 `ensureV2Access` 문자열로 테스트가 통과하지 않는다. - 각 가드 문자열을 제거한 변이 source에서는 대응 테스트가 실패한다. - production 코드는 변경하지 않고 관련 source test와 기존 Access 회귀 테스트가 통과한다. - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveEntryPolicyTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --rerun-tasks ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks git diff --check HEAD ``` - 기대 결과: 변이 source RED 확인 후 실제 source 기준 대상 테스트와 관련 회귀 테스트, 컴파일, test source ktlint, whitespace 검증이 통과한다. - RED 검증 기록: - 2026-07-14: 세 결선 테스트에서 각 대상 가드 문자열을 제거한 변이 source를 사용해 실행한 결과, On-air 로그인/성인 결선 1건과 Creator Channel 진입/AI 채팅 결선 2건이 각각 실패하고 `BUILD FAILED`가 발생함을 확인했다. - GREEN 검증 기록: - 2026-07-14: 변이를 제거하고 실제 production source를 대상으로 신규 결선 테스트 3건을 `--rerun-tasks`로 실행해 모두 통과하고 `BUILD SUCCESSFUL`임을 확인했다. - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.access.*" --tests "kr.co.vividnext.sodalive.v2.main.*" --tests "kr.co.vividnext.sodalive.v2.live.onair.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --rerun-tasks`를 실행해 관련 회귀 테스트 503건이 실패, 오류, 스킵 없이 통과했다. - 2026-07-14: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과해 test source 신규 위반이 없음을 확인했다. - 2026-07-14: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과해 문서 변경 후 Gradle task 구성을 확인했다. - 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - 2026-07-14: Task 1.11에서는 production 코드를 추가 변경하지 않고 기존 source test 클래스 두 곳과 계획 문서만 보강했다. ### Phase 2: Content 공통 Action - [x] **Task 2.1: 오디오/시리즈 진입 호출부와 계약 확정** - 확인 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: ID, 성인 여부, Activity result, route source, 기존 extra 차이를 목록화하고 Action 계약을 확정한다. - 검증 기준: - 화면 전용 UI model을 받지 않는 최소 command/result 계약이 정해진다. - Activity result 또는 호출 화면 후처리가 있는 경로는 Content result adapter와 단일 handler 필요 여부가 결정된다. - 확인 결과: - 공개 입력은 `audioContentId` 또는 `seriesId`와 호출부가 이미 알고 있는 성인 콘텐츠 여부만 받는 `AudioDetail`/`SeriesDetail` command로 제한한다. Home/Content/Creator 전용 UI model과 Response는 Action에 전달하지 않는다. - Action 결과는 유효하지 않은 ID의 `Ignored`, Access가 거부된 `Blocked`, 허용된 오디오/시리즈 navigation 결과로 구분한다. `ContentActionHandler`는 허용 결과만 기존 `Constants.EXTRA_AUDIO_CONTENT_ID` 또는 `Constants.EXTRA_SERIES_ID`로 변환한다. - Home banner의 series와 최근 활동의 audio는 성인 여부를 제공하지 않으므로 기존처럼 `AccessRequirement.Login`을 사용한다. Home의 event/creator/link와 community route는 Content Action 범위에 포함하지 않는다. - Content 추천 카드와 전체 탭은 `showAdultBadge`를 알고 있는 경우 이를 전달한다. 댓글/랭킹/원작 series/banner처럼 성인 여부가 없는 경로는 로그인만 확인한다. - Content Overview audio, Creator Channel Home audio, Audio tab, Live replay, Series tab은 성인 여부를 보유하므로 command에 전달한다. Creator Channel schedule audio/live replay와 Home series는 성인 여부가 없어 로그인만 확인한다. - 상세 진입은 모두 일반 `startActivity`이며 `ActivityResultLauncher`, route source extra, 호출 화면 후처리가 없다. 따라서 별도 Content result adapter나 feature composition handler를 추가하지 않는다. - `MainV2Activity`의 audio notification/deeplink/mini player와 series deeplink는 Phase 1에서 route 소비 순서와 boolean 반환 계약을 별도로 고정한 시스템 route이므로, PRD에 합의된 Home/Content/Content Overview/Creator Channel 전환 범위에는 포함하지 않는다. - 검증 기록: - 2026-07-14: `rg`와 대상 source 확인으로 V2 상세 Activity 직접 참조, ID/extra, 성인 여부 보유 여부, Activity result 사용 여부를 대조해 위 command/result 계약과 전환 범위를 확정했다. - 2026-07-14: 격리 worktree에서 변경 전 `./gradlew :app:testDebugUnitTest`를 실행해 전체 debug 단위 테스트가 `BUILD SUCCESSFUL`로 통과하는 기준선을 확인했다. 기존 dependency/manifest/Kotlin deprecation 경고는 있었지만 실패는 없었다. - [x] **Task 2.2: Content Action 순수 계약 테스트 및 구현** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentAction.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionCommand.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionTest.kt` - 검증 대상: invalid ID, audio/series route, 성인 Access 요구사항 전달, 허용/차단 결과. - 검증 기준: feature UI model과 DTO 의존 없이 입력·출력이 테스트된다. - RED 검증 기록: - 2026-07-14: `ContentActionTest`를 먼저 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.ContentActionTest"`를 실행해 `ContentAction`, `ContentActionCommand`, `ContentActionResult`, `ContentActionHandler` 미정의 컴파일 오류와 `BUILD FAILED`를 확인했다. - GREEN 검증 기록: - 2026-07-14: `ContentActionCommand`, `ContentAction`, `ContentActionResult`, `ContentActionHandler`를 최소 구현하고 같은 단일 테스트 명령을 재실행해 7개 테스트와 빌드가 통과했다. Action은 ID/성인 여부와 Access 함수만 입력받고 feature UI model, DTO, Android UI에 의존하지 않는다. - [x] **Task 2.3: Content navigation adapter 구현 및 전체 호출부 전환** - 생성/수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/ContentActionHandler.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/ui/CreatorChannelSeriesAdapter.kt` - 위 호출부 계약을 검증하는 기존 source/layout 테스트 - 작업: 기존 extra와 Access UX를 보존하면서 오디오/시리즈 상세 진입을 한 곳으로 통합한다. - 검증 기준: 대상 UI에서 상세 Activity Intent 직접 생성이 제거되고 Content Action을 사용한다. - RED 검증 기록: - 2026-07-14: 기존 source/layout 테스트를 새 Content Action 결선 계약으로 먼저 변경하고 관련 6개 테스트 클래스 207건을 `--rerun-tasks`로 실행했다. 대상 화면의 직접 상세 Intent, series banner Intent, ID-only callback이 남아 있어 예상한 13건이 실패하고 `BUILD FAILED`가 발생함을 확인했다. - GREEN 검증 기록: - 2026-07-14: Home, Content, Content Overview, Creator Channel 호출부를 `handleContentAction`으로 전환하고 대상 feature/model source의 `AudioContentDetailActivity`/`SeriesDetailActivity` 직접 참조를 제거했다. 상세 Activity와 기존 ID extra 생성은 `ContentActionHandler`만 소유한다. - 2026-07-14: Content Overview와 Creator Channel Audio/Live replay/Series tab callback이 기존 UI model의 `showAdultBadge`를 command까지 전달하도록 연결했다. 성인 정보를 제공하지 않는 Home, schedule, Creator Home series 경로는 기존 Login 요구사항을 유지했다. - 2026-07-14: 같은 6개 테스트 클래스 207건을 `--rerun-tasks`로 재실행해 실패, 오류, 스킵 없이 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: 결선 GREEN 중 발견한 기존 전체 탭 source assertion은 다른 helper의 `openSeriesDetail(seriesId)` 문자열에 우연히 의존하고 있었다. 실제 계약인 `openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))`를 검증하도록 강화한 뒤 단일 테스트와 207건 결선을 다시 통과시켰다. - [x] **Task 2.4: Content Phase 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck git diff --check ``` - 수동 확인: Home, Content, Content Overview, Creator Channel에서 audio/series 상세 진입과 성인 차단 UX가 기존과 동일하다. - 검증 기록: - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*"`, `--tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `--tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, 추가 Home 회귀 `--tests "kr.co.vividnext.sodalive.v2.main.home.*"`를 각각 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. Content 묶음은 99개 테스트가 통과했다. - 2026-07-14: Phase 1에서 작성된 `ContentMainFragmentLoginGuardSourceTest`가 이전 화면 내부 `ensureV2Access` 구현을 고정해 Content 묶음 3건이 실패했다. 실제 Access 계약은 `ContentMainFragment -> ContentAction -> ContentActionHandler` 순으로 이동했으므로 invalid ID, 성인 요구사항, Access-before-navigation을 새 소유자에서 검증하도록 테스트를 갱신한 뒤 단일 클래스와 Content 99건을 다시 통과시켰다. - 2026-07-14: `./gradlew :app:testDebugUnitTest`를 실행해 전체 debug 단위 테스트가 44초에 `BUILD SUCCESSFUL`로 통과했고, `./gradlew :app:compileDebugKotlin`도 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했고 test source 리포트는 0건이다. `./gradlew :app:ktlintCheck`는 이번 변경과 무관한 기존 main source 기준선 위반 550줄로 실패했으며, 리포트에서 Phase 2 변경 Kotlin 파일의 위반은 0건임을 확인했다. 레거시 파일은 수정하지 않았다. - 2026-07-14: 대상 Home, Content, Content Overview, Creator Channel 운영 source에서 `AudioContentDetailActivity`, `SeriesDetailActivity` 및 상세 ID extra를 검색한 결과 잔존이 없고, `ContentActionHandler`만 기존 Activity/extra 생성을 소유함을 확인했다. Phase 1에서 계약을 고정한 `MainV2Activity` 시스템 route는 범위대로 유지했다. - 2026-07-14: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다. `adb devices`는 연결된 기기가 없음을 반환해 실기기 수동 탭 검증은 수행하지 못했고, 대신 Robolectric handler 테스트로 허용/차단과 기존 extra를 검증했다. - 2026-07-14: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. ### Phase 3: Live 공통 Action - [x] **Task 3.1: 기존 Live 흐름 차이와 레거시 의존 목록 확정** - 확인 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 작업: 상세, 입장, 예약, 결제, 비밀번호, 오디오 중단, Activity result, refresh callback, navigation 차이를 기록한다. - 검증 기준: - Live 공통 정책과 화면별 후처리가 구분된다. - 라이브 생성 결과를 포함한 raw Activity result의 Live 결과 변환과 feature 단일 handler가 확정된다. - 확인 결과: - Home은 로그인 후 상세를 조회해 `isAdult`를 확인하고 기존 coordinator에 상세를 전달하며, Creator Channel 현재 라이브는 응답의 `isAdult`를 먼저 확인한 뒤 coordinator가 상세를 다시 조회한다. On-air는 로그인, 상세 조회, channel 유효성, 성인 접근을 확인한 뒤 Activity 안에 복제된 입장 분기를 실행한다. - 공통 입장 결정 순서는 channel 없음의 상세 표시, 관리자 직접 입장, 무료/결제 완료 방의 비밀번호 또는 직접 입장, 미결제 유료 비밀번호 또는 결제 Dialog다. 오디오 서비스 중단과 입장 성공 후 `LiveRoomActivity` 이동도 세 흐름에서 동일하다. - 일정 상세의 예약, 수정, 시작, 취소는 기존 coordinator만 소유하며 레거시 `LiveViewModel`, Dialog, Activity를 호출한다. 이 동작은 Live 소유 coordinator로 함께 이동하되 정책 판단과 화면 adapter를 추가로 분리하지 않는다. - On-air는 channel이 없으면 공통 오류 Toast를 표시하고, Home/Creator Channel은 라이브 상세를 표시한다. 이 차이는 공통 입장 정책이 아닌 호출 화면의 unavailable callback으로 유지한다. - Creator Channel의 라이브 생성 raw Activity result는 성공 여부, room ID, channel name을 `LiveCreationResult.Enter`, `Created`, `RefreshOnly`, `Ignored`로 변환한다. Activity의 `handleLiveCreationResult` 단일 handler가 성공 결과의 Home refresh 후 즉시 입장 또는 생성 안내를 결정한다. 실패/cancel은 `Ignored`, channel은 있지만 room ID가 유효하지 않은 성공 결과는 `RefreshOnly`로 변환한다. - 별도 observer/SharedFlow는 추가하지 않는다. Live 생성 결과의 소비자는 Creator Channel Activity 하나이고 Home/On-air refresh 요구도 각각 현재 화면 callback으로 충분하다. - 검증 기록: - 2026-07-14: 대상 운영 코드와 source test를 대조해 위 공통 정책, 레거시 의존, Activity result 및 화면별 후처리 차이를 확정했다. - 2026-07-14: 구현 전 `./gradlew :app:testDebugUnitTest`를 실행해 전체 debug 단위 테스트 기준선이 `BUILD SUCCESSFUL`임을 확인했다. - [x] **Task 3.2: Live 입장 결정 계약과 단위 테스트 작성** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveEntryDecision.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveEntryPolicy.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveEntryPolicyTest.kt` - 검증 대상: 관리자, 무료/결제 완료, 비밀번호, 유료, 상세 표시 분기. - 검증 기준: Activity/Dialog 없이 기존 분기 순서를 순수 입력·출력으로 검증한다. - RED 검증 기록: - 2026-07-14: `LiveEntryPolicyTest`를 먼저 추가하고 단일 테스트를 실행해 `LiveEntryDecision`, `LiveEntryContext`, `LiveEntryPolicy` 미정의 컴파일 오류와 `BUILD FAILED`를 확인했다. - GREEN 검증 기록: - 2026-07-14: channel 없음, 관리자, 무료/결제 완료 공개·비밀번호 방, 미결제 유료 공개·비밀번호 방만 표현하는 순수 정책을 최소 구현하고 같은 단일 테스트를 재실행해 `BUILD SUCCESSFUL`을 확인했다. - [x] **Task 3.3: Live Action/Coordinator 단일 구현과 호환 adapter 구성** - 생성/이동/수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveCreationResult.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt` - 작업: - 기존 Coordinator 동작을 Live 소유 진입점으로 옮기고 필요한 동안 기존 이름은 얇은 adapter로 유지한다. - 하위 Activity가 반환하는 Live 결과는 명시적 result로 변환하고 호출 feature의 단일 handler로 전달한다. - 검증 기준: 새 구현이 Creator Channel feature를 import하지 않고 레거시 Live 기능만 adapter로 호출한다. - RED 검증 기록: - 2026-07-14: `LiveActionsTest`와 `LiveActionCoordinatorSourceTest`를 먼저 추가했다. 결과 타입/변환 함수 미정의 컴파일 실패를 확인한 뒤 최소 결과 계약을 추가했고, 새 coordinator 파일과 세 화면 결선 및 Creator Channel handler가 없어 source 결선 테스트 3건이 실패함을 확인했다. - 2026-07-14: diff 검토에서 channel은 있지만 room ID가 유효하지 않은 기존 무동작 edge를 발견해 `Ignored` 기대 테스트로 변경하고 실제 `Created` 반환 실패를 재현했다. 이 당시 계약은 후속 Task 3.6에서 성공 결과의 Home refresh를 보장하도록 `RefreshOnly`로 변경했다. - GREEN 검증 기록: - 2026-07-14: 기존 coordinator를 `v2.live.action.LiveActionCoordinator`로 이동하고 `LiveEntryPolicy` 결과에 따라 기존 `LiveViewModel`, Dialog, Activity를 호출하도록 전환했다. 모든 호출부를 함께 전환해 이전 이름의 호환 adapter는 남길 필요가 없었으며 feature 역의존도 제거했다. - 2026-07-14: `LiveCreationResult`와 `resolveLiveCreationResult`를 구현하고 Creator Channel의 `handleLiveCreationResult` 단일 handler가 `Ignored`, `Created`, `Enter` 후처리를 소유하도록 연결했다. invalid room ID는 기존처럼 `Ignored`로 보존한 뒤 결과 테스트를 통과시켰다. 이 동작은 후속 Task 3.6에서 `RefreshOnly`로 변경했다. - [x] **Task 3.4: Home/Creator Channel/On-air 호출부 전환과 중복 제거** - 수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 검증 기준: 세 화면이 동일 Live Action을 사용하고 On-air의 중복 입장 조건이 제거된다. - 검증 기록: - 2026-07-14: Home은 lifecycle 확인 callback과 함께 공통 coordinator를 사용하고, Creator Channel은 현재 라이브의 알려진 성인 여부를 전달하며 일정 상세도 같은 coordinator를 사용하도록 전환했다. - 2026-07-14: On-air Activity의 상세 이후 관리자/무료/결제/비밀번호 분기와 Dialog/navigation 구현을 제거하고 공통 coordinator에 unavailable Toast callback만 전달했다. 이에 따라 단일 boolean helper와 파일도 제거했다. - 2026-07-14: 신규 결선 테스트와 기존 Home login guard, Creator Channel, On-air source 테스트를 함께 실행해 `BUILD SUCCESSFUL`을 확인했다. - [x] **Task 3.5: Live Phase 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck git diff --check ``` - 수동 확인: 무료, 유료, 비밀번호, 관리자, 상세 표시, 예약/취소 흐름과 화면별 refresh 동작이 기존과 동일하다. - 검증 기록: - 2026-07-14: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*" --rerun-tasks`를 실행해 Live 관련 테스트가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: 최종 변경 후 `./gradlew :app:testDebugUnitTest --rerun-tasks`를 실행해 전체 debug 단위 테스트와 production/test 컴파일이 1분 43초에 `BUILD SUCCESSFUL`로 통과했다. 기존 deprecation 및 manifest namespace 경고는 있었지만 실패는 없었다. - 2026-07-14: `./gradlew :app:compileDebugKotlin`은 별도 실행에서도 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-14: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했으며, filename 위반을 수정한 뒤 main source 보고서 550줄에서 Phase 3 변경 Kotlin 파일 위반이 0건임을 확인했다. - 2026-07-14: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했고 `git diff --check HEAD`도 whitespace 오류 없이 통과했다. - 2026-07-14: `adb devices`에 연결된 기기/에뮬레이터가 없어 무료, 유료, 비밀번호, 관리자, 상세, 예약/취소의 실제 Dialog/Activity 수동 탭 검증은 수행하지 못했다. 해당 분기는 순수 정책 테스트와 기존 레거시 결선 source 테스트로 회귀 검증했다. - [x] **Task 3.6: Live 진입·생성 결과 회귀와 리뷰 발견사항 보완** - 수정 대상 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveCreationResult.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionsTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 정리 대상 파일: - `docs/20260715_라이브_진입과_생성_결과_회귀_수정/prd.md` - `docs/20260715_라이브_진입과_생성_결과_회귀_수정/plan-task.md` - 작업: - unavailable을 성인 접근 확인과 오디오 중단보다 먼저 처리하되, 상세 fallback은 호출자 성인 힌트와 상세 응답을 OR로 합성한 접근 조건을 정상 진입과 함께 사용한다. - 라이브 생성 성공 후 즉시 입장할 room ID가 유효하지 않아도 `RefreshOnly`로 Home refresh를 보장한다. - 신규 source test의 ktlint 위반 3건을 고치고 test source 검증 게이트를 복구한다. - 연속 작업으로 별도 생성된 2026-07-15 문서의 요구사항과 검증 기록을 본 PRD와 Phase 3에 이관한 뒤 중복 문서를 제거한다. - 검증 기준: - 합성 성인 접근 조건이 정상 진입과 unavailable 상세 fallback에서 동일하게 사용된다. - `LiveActionCoordinatorSourceTest`와 Live action 관련 테스트가 통과한다. - `:app:ktlintTestSourceSetCheck`가 통과하고 Phase 3 변경 Kotlin 파일의 신규 main source 위반이 없다. - 별도 2026-07-15 문서가 남지 않고 현행 생성 결과 계약이 구현과 일치한다. - 이관한 구현·검증 기록: - 2026-07-15: 성공했지만 channel 정보가 있고 room ID가 유효하지 않은 결과를 `RefreshOnly`로 기대하는 테스트를 먼저 추가해 타입 미정의 컴파일 오류로 RED를 확인했다. - 2026-07-15: `LiveCreationResult.RefreshOnly`를 추가하고 Creator Channel의 단일 handler가 해당 결과에서 `homeActionDelegate?.refreshHome()`만 실행하도록 구현했다. - 2026-07-15: `ShowDetail`을 성인 접근 확인과 오디오 중단보다 먼저 처리하도록 source test와 구현을 보완했다. source 문자열을 실제 구현 호출 형태로 조정한 뒤 테스트가 통과했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.*"`는 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`는 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:assembleDebug`는 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: 리뷰에서 상세 조회 전 성인 접근 확인이 실행되는 순서 문제와 callback 없는 `ShowDetail` fallback이 성인 접근 확인 없이 열리는 문제가 확인되었다. 로그인만 먼저 확인하고, fallback은 `roomDetail.isAdult`로 성인 접근을 확인하도록 보완한 뒤 Live action 테스트, Creator Channel source test, assembleDebug가 통과했다. - 2026-07-15: 보안 재리뷰에서 호출자 힌트가 `false`여도 상세 응답이 `true`이면 성인 접근을 요구해야 하는 문제가 확인되었다. 정상 진입 조건을 `requiresAdultContentAccess == true || roomDetail.isAdult`로 바꾸는 테스트에서 기존 구현의 assertion 2건 실패로 RED를 확인한 뒤 보완했고, Live action 테스트, Creator Channel source test, assembleDebug가 통과했다. - 리뷰 발견 및 RED 검증 기록: - 2026-07-15: Creator Channel 목록의 `isAdult == true`와 unavailable 상세 응답의 `isAdult == false`가 다를 때 fallback이 `roomDetail.isAdult`만 사용해 호출자 힌트를 잃는 P1 회귀를 코드 경로 대조로 확인했다. - 2026-07-15: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`를 실행해 `LiveActionCoordinatorSourceTest.kt:79`의 `max-line-length` 1건과 `argument-list-wrapping` 2건, 총 3건으로 `BUILD FAILED`를 재현했다. - 2026-07-15: 합성 성인 접근 조건의 단일 계산과 정상 진입·fallback 재사용을 요구하도록 `LiveActionCoordinatorSourceTest`를 먼저 변경했다. 대상 테스트를 실행해 5개 중 기존 구현과 불일치한 3개가 실패하고 `BUILD FAILED`가 되는 RED를 확인했다. - GREEN 및 회귀 검증 기록: - 2026-07-15: `requiresAdultContentAccess == true || roomDetail.isAdult`를 `requiresAdultAccess`로 한 번 계산하고 정상 진입 guard와 unavailable 상세 fallback에 함께 전달하도록 수정했다. - 2026-07-15: source test가 정상 진입과 fallback 함수 구간을 분리해 동일 성인 조건을 검증하고, unavailable 처리 후 `return@handleRoomDetail`, callback의 `return`, 성인 guard, 상세 표시 순서를 고정하도록 보강했다. `RefreshOnly` 분기의 Home refresh와 invalid room ID `null`, `0L`, 음수도 함께 검증했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.*"`와 최종 `LiveActionCoordinatorSourceTest` 단일 실행은 각각 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: 최종 `./gradlew :app:testDebugUnitTest --rerun-tasks`는 39개 작업을 모두 실행해 3분 20초에 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: 최종 `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했고 test source 리포트는 0건이다. - 2026-07-15: `./gradlew :app:assembleDebug`는 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:ktlintMainSourceSetCheck --rerun-tasks`는 기존 main source 기준선 550건으로 실패했으며, 갱신된 리포트에서 Phase 3 변경 Kotlin 파일의 위반은 0건임을 확인했다. - 2026-07-15: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: 별도 `docs/20260715_라이브_진입과_생성_결과_회귀_수정/`의 두 문서 내용을 본 PRD와 Task 3.6으로 이관한 뒤 중복 문서 파일을 제거했다. - 2026-07-15: 독립 코드 재리뷰에서 정상·fallback 성인 guard, 두 unavailable short-circuit, `RefreshOnly` refresh 계약을 재확인했고 Critical/Important 이슈 없이 Ready 판정을 받았다. - 2026-07-15: 최종 `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - [x] **Task 3.7: Live coordinator lifecycle·중복 표시·계약 리뷰 반영** - 수정 대상 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorUiGateTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveEntryPolicyTest.kt` - `docs/agent-guides/build-test-style.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - 비동기 `enterRoom` 성공 callback에서 `LiveRoomActivity`를 열기 직전에 lifecycle predicate를 다시 검사한다. - 라이브 상세 bottom sheet는 Activity/호출 화면 predicate, `FragmentManager.isStateSaved`, 안정적인 tag의 기존 fragment 존재 여부를 모두 통과할 때만 표시한다. - `channelName` 없음과 본인 manager 조건이 동시에 있을 때 `ShowDetail`이 manager 직접 입장보다 우선함을 실행형 정책 테스트로 고정한다. - `LiveActionCoordinator`가 presentation adapter로서 `refreshHome` callback을 주입받는 예외와 순수 도메인 계약의 callback 금지를 문서에서 명확히 구분한다. - 검증 기준: - `canHandleResult` 기본값은 Activity 종료/파괴 상태를 반영하고, Fragment 호출부의 더 엄격한 predicate override는 유지된다. - 모든 비동기 UI 후처리 직전에는 `canHandleResult`가 재검사된다. - 상세 sheet 중복 방지는 새 fragment 인스턴스의 `isAdded`가 아니라 안정적인 tag와 표시 가능 상태를 기준으로 동작한다. - 정책 테스트와 coordinator gate/source 테스트, 컴파일, test source ktlint, whitespace 검증이 통과한다. - RED 검증 기록: - 2026-07-15: lifecycle/detail gate 실행형 테스트와 source test를 먼저 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveEntryPolicyTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest" --rerun-tasks`를 실행해 `runLiveActionUiSideEffect`, `canShowLiveRoomDetail` 미정의 컴파일 오류로 `BUILD FAILED`를 확인했다. - GREEN 및 회귀 검증 기록: - 2026-07-15: `LiveActionCoordinator`의 기본 `canHandleResult`를 Activity 종료/파괴 상태 기반으로 바꾸고, `canHandleUiResult()`에서 Activity 상태, `FragmentManager.isStateSaved`, 호출 화면 predicate를 합성하도록 보완했다. - 2026-07-15: 상세 sheet는 `LIVE_ROOM_DETAIL_TAG`로 `findFragmentByTag`를 먼저 확인하고, 같은 tag로 `show`하도록 수정했다. 새 `LiveRoomDetailFragment` 인스턴스의 `isAdded`와 nullable `tag` 기반 중복 방지는 제거했다. - 2026-07-15: `enterRoom` 성공 callback과 `startLive` 성공 callback은 `runOnUiThread` 내부에서 `runLiveActionUiSideEffect(::canHandleUiResult)`를 통과한 뒤에만 `openLiveRoom(roomId)`를 호출하도록 보강했다. 비밀번호/결제/예약/수정/시작/취소 UI side effect도 표시 또는 실행 직전 `canHandleUiResult()`를 검사한다. - 2026-07-15: `LiveEntryPolicyTest`에 `channelName == null`과 본인 manager 조건이 동시에 있을 때 `LiveEntryDecision.ShowDetail`을 반환하는 실행형 회귀 테스트를 추가했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveEntryPolicyTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-15: 최종 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.*" --rerun-tasks`를 실행해 Live action 테스트 묶음이 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew :app:ktlintMainSourceSetCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했으며, ktlint main/test 리포트에서 이번 변경 Kotlin 파일명 검색 결과는 0건이다. - 2026-07-15: `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했고 `git diff --check HEAD`는 whitespace 오류 없이 통과했다. - 2026-07-15: `adb devices`는 연결된 기기/에뮬레이터가 없음을 반환해 실제 탭 기반 Dialog/Activity 수동 검증은 수행하지 못했다. 대신 lifecycle/detail gate 실행형 단위 테스트와 source order 테스트로 회귀를 고정했다. - 리뷰 게이트 후속 보완 기록: - 2026-07-15: 리뷰 게이트에서 `DialogFragment.show()`가 비동기 `commit()`을 사용해 첫 transaction 반영 전 연속 호출 시 `findFragmentByTag`가 중복을 막지 못할 수 있다는 차단 이슈와, unavailable/adult guard/audio 순서가 source test에만 의존한다는 차단 이슈를 확인했다. - 2026-07-15: `LiveActionCoordinatorSourceTest`를 먼저 `showNow(fragmentManager, LIVE_ROOM_DETAIL_TAG)` 기대값으로 바꾸고, `LiveActionCoordinatorUiGateTest`에 `runLiveEntryDecisionFlow` 실행형 순서 테스트를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest" --rerun-tasks`를 실행해 `runLiveEntryDecisionFlow` 미정의 컴파일 오류로 `BUILD FAILED`를 확인했다. - 2026-07-15: 상세 sheet 표시를 `showNow(fragmentManager, LIVE_ROOM_DETAIL_TAG)`로 바꿔 pending transaction 경쟁 구간을 제거하고, 입장 분기 순서를 Android 비의존 `runLiveEntryDecisionFlow`로 분리해 `ShowDetail -> unavailable`, `Enter -> adult guard -> audio/entry`, adult guard 거부 시 audio/entry 미실행을 실행형 테스트로 고정했다. - 2026-07-15: 후속 보완 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest" --rerun-tasks`를 재실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-15: 후속 보완 최종 확인으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.*" --rerun-tasks`를 재실행해 39개 작업이 모두 실행되고 `BUILD SUCCESSFUL`로 종료됨을 확인했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --rerun-tasks` 전체 debug 단위 테스트를 재실행해 39개 작업이 모두 실행되고 `BUILD SUCCESSFUL`로 종료됨을 확인했다. - 2026-07-15: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`, `./gradlew tasks --all`은 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다. - 2026-07-15: `./gradlew :app:ktlintMainSourceSetCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했으며, 갱신된 ktlint main 리포트에서 이번 변경 파일명 검색 결과는 0건이다. - 2026-07-15: `adb devices`는 연결된 기기/에뮬레이터가 없음을 반환해 실제 탭 기반 수동 QA는 수행하지 못했다. 대신 Live action 대상 실행형 테스트, 전체 debug 단위 테스트, 컴파일, ktlint test source, whitespace 검증으로 대체 확인했다. - 보안 리뷰 후속 보완 기록: - 2026-07-15: 재리뷰 게이트에서 Creator Channel 일정 경로가 `showLiveRoomDetail(roomId)`를 직접 호출할 때 공개 상세 진입점 자체에는 로그인·성인 접근 확인이 없어 성인 라이브 상세를 열 수 있다는 보안 차단 이슈를 확인했다. - 2026-07-15: `LiveActionCoordinatorSourceTest`에 공개 `showLiveRoomDetail(roomId)`가 `AccessRequirement.Login`, `liveViewModel.getRoomDetail(roomId)`, `AccessRequirement.forAdultContent(roomDetail.isAdult)`를 거친 뒤 내부 표시 함수로 이동해야 한다는 source test를 먼저 추가했다. 생산 코드 변경 전 단일 source test 실행에서 해당 테스트가 실패해 RED를 확인했다. - 2026-07-15: 공개 `showLiveRoomDetail(roomId)`는 로그인 확인 후 room detail을 조회하고 `roomDetail.isAdult` 기준 성인 접근을 통과한 뒤 `showLiveRoomDetailContent(roomId)`를 호출하도록 분리했다. 이미 성인 가드를 통과한 unavailable fallback은 공개 진입점 재호출 대신 내부 표시 함수만 호출하도록 수정했다. - 2026-07-15: 보안 보완 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorSourceTest" --rerun-tasks`와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.*" --rerun-tasks`가 모두 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: 전체 `./gradlew :app:testDebugUnitTest --rerun-tasks`는 최초 재실행에서 이번 변경과 직접 관련 없는 `CreatorChannelHomeViewModelTest` 1건이 실패했으나, 해당 단일 테스트 재실행과 전체 재실행이 모두 `BUILD SUCCESSFUL`로 통과해 일시적 격리 실패로 기록한다. - 2026-07-15: 보안 보완 후 `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다. `./gradlew :app:ktlintMainSourceSetCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했으며 변경 파일명 검색 결과는 0건이다. - [x] **Task 3.8: Phase 3 실기기 수동 회귀 테스트** - 확인 대상 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt` - 대상 화면: - `HomeMainFragment` 추천 라이브 - `CreatorChannelActivity` 현재 라이브, 일정 상세, owner 라이브 생성 - `HomeOnAirLiveActivity` 라이브 목록 - 완료 기준: - 아래 필수 시나리오가 모두 기대 결과와 일치하면 각 항목을 `[x]`로 변경한다. - 재현 데이터나 외부 환경이 없어 실행하지 못한 항목은 체크하지 않고 검증 기록에 `BLOCKED`와 사유를 남긴다. - 최종 수동 검증에서 사용자가 테스트 불가능 또는 의미 없음으로 판단한 항목은 미체크 상태를 유지하고 `SKIPPED`로 기록하며 완료 판정 대상에서 제외한다. - 기대 결과와 다른 항목은 체크하지 않고 화면, 계정 조건, room ID, 실제 결과, 로그 또는 캡처 위치를 검증 기록에 남긴다. #### 사전 준비 - [x] **M3-P01: 테스트 환경 준비** - 절차: 최신 debug 빌드를 실기기 또는 에뮬레이터에 설치하고 네트워크 연결, 알림/오디오 재생, 로그인 화면 이동이 가능한지 확인한다. - 기대 결과: 앱을 실행해 Home, Creator Channel, On-air 화면에 진입할 수 있고 라이브 상세/입장 API 응답을 받을 수 있다. - [x] **M3-P02: 접근 상태별 계정 준비** - 절차: 로그아웃 상태, 국내 미인증 계정, 본인인증 완료·성인 콘텐츠 표시 비활성 계정, 성인 콘텐츠 접근 허용 일반 계정, 라이브 관리자 계정을 준비한다. - 기대 결과: 각 계정의 로그인, `isAuth`, 성인 콘텐츠 표시 설정, 관리자 여부를 테스트 전에 구분할 수 있다. - [x] **M3-P03: 라이브 분기별 데이터 준비** - 절차: 아래 조건을 각각 만족하는 현재 라이브 또는 예약 라이브의 room ID와 노출 화면을 기록한다. | 데이터 | 조건 | 주요 검증 | | --- | --- | --- | | `R1` | Home 추천, Creator Channel, On-air에 노출되는 무료 공개 현재 라이브 | 세 화면 공통 진입 | | `R2` | 무료 비밀번호 현재 라이브 | 결제액 없는 비밀번호 Dialog | | `R3` | 결제 완료 유료 공개 현재 라이브 | 결제 없는 직접 입장 | | `R4` | 결제 완료 유료 비밀번호 현재 라이브 | 결제액 없는 비밀번호 Dialog | | `R5` | 미결제 유료 공개 현재 라이브 | 결제 Dialog | | `R6` | 미결제 유료 비밀번호 현재 라이브 | 가격이 표시되는 비밀번호 Dialog | | `R7` | 관리자 본인의 유료·비밀번호 현재 라이브 | 관리자 직접 입장 우선순위 | | `R8` | 각 대상 화면에 노출 가능한 `channelName` null 또는 blank 라이브 | 화면별 unavailable 처리 | | `R9` | 성인 현재 라이브 | 본인인증/표시 설정 가드 | | `R10` | 일반 사용자가 예약 가능한 무료·유료·비밀번호 예약 라이브 | 예약 분기 | - 기대 결과: 각 데이터의 가격, 결제 여부, 비밀번호 여부, 성인 여부, 관리자 ID, `channelName` 조건을 QA 환경에서 확인할 수 있다. #### 화면 결선과 접근 가드 - [x] **M3-01: 세 화면의 무료 공개 라이브 진입** - 절차: 접근 허용 일반 계정으로 `R1`을 Home 추천, Creator Channel 현재 라이브, On-air 목록에서 각각 한 번 선택한다. - 기대 결과: 세 경로 모두 기존 로딩 표시 후 같은 room ID의 `LiveRoomActivity`로 이동하며 중복 상세 조회 화면이나 추가 Dialog가 나타나지 않는다. - [x] **M3-02: 로그아웃 상태의 라이브 선택** - 절차: 로그아웃 상태에서 Home 추천 라이브를 선택하고, Home의 On-air 더보기 진입도 시도한다. - 기대 결과: 기존 `LoginActivity`가 열리고 라이브 상세 조회, 오디오 중단, `LiveRoomActivity` 이동은 실행되지 않는다. - [ ] **M3-03: 국내 미인증 계정의 성인 라이브 선택** - 절차: 국내 미인증 계정으로 `R9`를 Home 추천 또는 Creator Channel 현재 라이브에서 선택한다. - 기대 결과: 로그인 확인과 상세 조회 뒤 기존 V2 본인인증 안내가 표시되고 오디오 중단, 비밀번호/결제 Dialog, 라이브 입장은 실행되지 않는다. - [ ] **M3-04: 성인 콘텐츠 표시가 비활성인 계정의 성인 라이브 선택** - 절차: 본인인증 완료·성인 콘텐츠 표시 비활성 계정으로 `R9`를 선택한다. - 기대 결과: `ContentSettingsActivity`가 성인 콘텐츠 안내 상태로 열리고 라이브 입장과 오디오 중단은 실행되지 않는다. - [x] **M3-05: 성인 콘텐츠 접근 허용 계정의 성인 라이브 선택** - 절차: 성인 콘텐츠 접근 허용 계정으로 `R9`를 선택한다. - 기대 결과: 추가 접근 안내 없이 `R9`의 입장 정책에 맞는 직접 입장, 비밀번호 Dialog 또는 결제 Dialog가 실행된다. - [ ] **M3-06: 호출자 성인 힌트와 상세 응답 불일치** - 절차: QA fixture에서 Creator Channel 목록 `isAdult=true`·상세 `isAdult=false`인 방과 목록 `isAdult=false`·상세 `isAdult=true`인 방을 각각 선택한다. - 기대 결과: 두 경우 모두 성인 콘텐츠 접근 조건을 요구하며, 접근이 거부되면 상세 표시와 라이브 입장이 실행되지 않는다. #### Live 입장 분기 - 공통 절차: 각 항목 실행 전에 오디오 콘텐츠를 재생하고, 접근 허용 일반 계정 또는 명시된 관리자 계정으로 대상 라이브를 선택한다. | 완료 | 시나리오 | 기대 결과 | |-----| --- | --- | | [x] | **M3-07: 관리자 본인의 유료·비밀번호 현재 라이브** | `R7` 선택 시 비밀번호/결제 Dialog 없이 오디오가 중단되고 `LiveRoomActivity`로 이동한다. | | [x] | **M3-08: 무료 공개 현재 라이브** | `R1` 선택 시 오디오가 중단되고 `LiveRoomActivity`로 이동한다. | | [x] | **M3-09: 무료 또는 결제 완료 비밀번호 현재 라이브** | `R2`와 `R4` 선택 시 결제액 없는 비밀번호 Dialog가 표시되고 올바른 비밀번호 입력 성공 후 입장한다. | | [x] | **M3-10: 결제 완료 유료 공개 현재 라이브** | `R3` 선택 시 결제 Dialog 없이 오디오가 중단되고 바로 입장한다. | | [x] | **M3-11: 미결제 유료 공개 현재 라이브** | `R5` 선택 시 오디오가 중단되고 기존 금액·시간 문구의 결제 Dialog가 표시되며, 취소 시 현재 화면을 유지하고 확인 성공 시 입장한다. | | [x] | **M3-12: 미결제 유료 비밀번호 현재 라이브** | `R6` 선택 시 결제 Dialog 대신 가격이 표시되는 비밀번호 Dialog가 나타나고 올바른 비밀번호 입력 성공 후 입장한다. | | [x] | **M3-13: 비밀번호 또는 입장 API 실패** | 잘못된 비밀번호 또는 QA 실패 응답에서 기존 오류 안내가 표시되고 `LiveRoomActivity`는 열리지 않으며 로딩 표시가 종료된다. | #### unavailable과 예약 라이브 상세 - [x] **M3-14: Home과 Creator Channel의 unavailable 상세 fallback** - 절차: 오디오를 재생한 상태에서 `R8`을 Home 추천과 Creator Channel 현재 라이브에서 각각 선택한다. - 기대 결과: 성인 접근 조건을 통과한 뒤 라이브 상세 bottom sheet가 표시되고 오디오는 계속 재생된다. - [ ] **M3-15: On-air의 unavailable 처리** - 절차: 오디오를 재생한 상태에서 On-air 목록에 노출된 `R8`을 선택한다. - 기대 결과: 기존 공통 오류 Toast만 표시되고 성인 안내, 라이브 상세, 비밀번호/결제 Dialog, 오디오 중단, 라이브 입장은 실행되지 않는다. - [x] **M3-16: 예약 라이브 상세의 로그인·성인 접근 확인** - 절차: Creator Channel 일정에서 일반 예약 라이브와 성인 예약 라이브를 각각 선택하고, 성인 예약 라이브는 접근 거부 계정과 허용 계정으로 나누어 확인한다. - 기대 결과: 일반 예약 라이브는 로그인 후 상세가 표시되고, 성인 예약 라이브는 성인 접근 허용 시에만 상세가 표시된다. - [x] **M3-17: 라이브 상세 중복 표시 방지** - 절차: Creator Channel 일정의 같은 예약 라이브를 빠르게 연속 선택한다. - 기대 결과: 같은 `LiveRoomDetailFragment`가 한 번만 표시되며 중복 bottom sheet나 `FragmentManager` 예외가 발생하지 않는다. - [x] **M3-18: 일반 사용자의 무료·유료·비밀번호 예약** - 절차: 일반 계정으로 `R10`의 무료 공개, 미결제 유료 공개, 비밀번호 예약 라이브를 각각 열어 예약을 진행한다. - 기대 결과: 무료 공개는 바로 예약되고, 유료 공개는 기존 결제 Dialog를 거치며, 비밀번호 방은 기존 비밀번호 Dialog를 거친다. 성공 후 `LiveReservationCompleteActivity`가 열리고 Creator Channel Home이 새로고침된다. - [x] **M3-19: 관리자 예약 라이브 수정** - 절차: 관리자 계정으로 본인 예약 라이브 상세에서 수정을 선택한다. - 기대 결과: 기존 room detail extra를 가진 `LiveRoomEditActivity`가 열린다. - [x] **M3-20: 관리자 예약 라이브 시작** - 절차: 관리자 계정으로 본인 예약 라이브 상세에서 시작을 선택하고 API 성공까지 기다린다. - 기대 결과: Creator Channel Home이 새로고침되고 해당 room ID의 `LiveRoomActivity`로 이동한다. - [x] **M3-21: 관리자 예약 라이브 취소** - 절차: 관리자 계정으로 본인 예약 라이브 상세에서 취소를 선택해 사유를 입력하고 완료한다. - 기대 결과: 기존 취소 성공 Toast가 표시되고 Creator Channel Home이 새로고침되며 취소된 일정 상태가 반영된다. #### 라이브 생성 결과 - [x] **M3-22: 예약 라이브 생성 완료** - 절차: Creator Channel owner FAB에서 즉시 시작되지 않는 예약 라이브를 생성한다. - 기대 결과: Creator Channel Home이 새로고침되고 기존 생성 완료 Toast가 표시되며 `LiveRoomActivity`는 열리지 않는다. - [x] **M3-23: 즉시 시작 라이브 생성 완료** - 절차: Creator Channel owner FAB에서 channel과 유효한 room ID가 반환되는 즉시 시작 라이브를 생성한다. - 기대 결과: Creator Channel Home이 새로고침되고 생성된 room ID의 입장 흐름을 거쳐 `LiveRoomActivity`로 이동한다. - [ ] **M3-24: 유효한 room ID가 없는 생성 성공 결과** - 절차: QA backend에서 channel은 반환하지만 room ID가 null, `0L` 또는 음수인 성공 응답으로 라이브 생성을 완료한다. - 기대 결과: Creator Channel Home만 새로고침되고 생성 Toast, 비밀번호/결제 Dialog, `LiveRoomActivity` 이동은 실행되지 않는다. - [x] **M3-25: 라이브 생성 취소** - 절차: 라이브 생성 화면을 결과 없이 닫는다. - 기대 결과: Creator Channel Home 새로고침, 생성 Toast, 라이브 입장이 실행되지 않는다. - [ ] **M3-26: 라이브 생성 실패 응답** - 절차: QA backend에서 라이브 생성 실패 응답을 반환하도록 설정하고 생성 요청을 완료한다. - 기대 결과: 기존 생성 실패 안내만 표시되고 Creator Channel Home 새로고침, 생성 완료 Toast, 라이브 입장은 실행되지 않는다. #### lifecycle과 오류 회귀 - [x] **M3-27: 상세 조회 중 호출 화면 이탈** - 절차: 느린 네트워크에서 라이브를 선택한 직후 Home Fragment를 벗어나는 경우와 Activity를 종료하는 경우를 각각 실행하고 상세 응답을 기다린다. - 기대 결과: 이탈한 화면 위에 Toast, Dialog, bottom sheet, `LiveRoomActivity`가 늦게 표시되지 않고 앱이 종료되지 않는다. - [x] **M3-28: 입장 또는 라이브 시작 요청 중 화면 이탈** - 절차: 비밀번호/결제 확인 또는 관리자 라이브 시작 직후 요청이 완료되기 전에 Activity 종료와 앱 백그라운드 전환을 각각 실행한다. - 기대 결과: lifecycle이 유효하지 않은 동안 `LiveRoomActivity`가 늦게 열리지 않고 `FragmentManager`/Activity state 예외가 발생하지 않는다. - [ ] **M3-29: 상세 조회 API 실패** - 절차: QA 실패 응답 또는 네트워크 차단 상태에서 Home, Creator Channel, On-air 중 한 경로의 라이브를 선택한다. - 기대 결과: 기존 라이브 상세 조회 오류 안내가 표시되고 오디오 중단, Dialog, 상세 bottom sheet, 라이브 입장은 실행되지 않으며 로딩 표시가 종료된다. - [ ] **M3-30: 수동 테스트 종료 후 화면 상태 확인** - 절차: 네트워크를 복구하고 Home, Creator Channel, On-air를 다시 열어 목록 로딩과 라이브 선택을 한 번씩 수행한다. - 기대 결과: 이전 실패/취소/화면 이탈로 인한 고정 loading, 중복 Dialog, 중복 상세, 잘못된 Home 상태가 남지 않고 정상 동작한다. #### 수동 검증 기록 형식 - 각 실행 결과는 다음 형식으로 본 Task 아래에 누적한다. - `YYYY-MM-DD / M3-번호 / 기기·OS / 계정 조건 / room ID / PASS·FAILED·BLOCKED·SKIPPED / 실제 결과와 증빙 위치` - 전체 항목 완료 후 여러 화면에 걸친 최종 결과는 문서 최하단 `Verification Log`에도 요약한다. - 체크리스트 작성 검증 기록: - 2026-07-15: Phase 3 요구사항과 구현 분기를 대조해 사전 준비 3개와 수동 시나리오 `M3-01`~`M3-30`을 작성했다. 시나리오 번호 중복과 placeholder가 없음을 확인했고, `./gradlew tasks --all`은 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다. - 사용자 수동 검증 기록: - 2026-07-15 / `M3-P01`~`M3-P03` / 기기·OS·계정·room ID 상세 미제공 / PASS / 사용자가 사전 준비 3개 항목을 모두 완료로 확인했다. - 2026-07-15 / `M3-01`, `M3-02`, `M3-05`, `M3-07`~`M3-14`, `M3-16`~`M3-23`, `M3-25`, `M3-27`, `M3-28` / 기기·OS·계정·room ID 상세 미제공 / PASS / 사용자가 체크한 22개 시나리오를 기대 결과와 일치한 것으로 확인했다. - 2026-07-15 / `M3-03`, `M3-04`, `M3-06`, `M3-15`, `M3-24`, `M3-26`, `M3-29`, `M3-30` / 기기·OS·계정·room ID 상세 미제공 / SKIPPED / 사용자가 테스트 불가능 또는 의미 없음으로 판단했으며, 항목은 미체크 상태로 유지했다. - 2026-07-15: 사전 준비 3개와 시나리오 22개는 `PASS`, 시나리오 8개는 `SKIPPED`, `FAILED`는 0개로 판정되어 Task 3.8을 완료 처리했다. ### Phase 4: Creator와 Community 공통 Action - [x] **Task 4.1: Creator Channel 진입 호출부와 계약 확정** - 확인 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/` - 검증 기준: `creatorId`, 로그인 요구 여부, route source, 기존 extra 차이가 정리된다. - 확인 결과: - Home 추천/랭킹/AI 캐릭터와 Content banner의 Creator Channel 이동은 `creatorId`만 있으면 충분하며 기존 `CreatorChannelActivity.EXTRA_CREATOR_ID` extra는 action handler가 유지한다. - `MainV2Activity`의 deeplink/system route는 Phase 1에서 별도 순서와 반환 계약을 고정한 경로이므로 Phase 4 사용자 액션 전환 범위에서 제외한다. - 모델 helper는 feature UI model에서 route ID만 반환하고 Creator Channel Activity Intent 생성은 Creator Action handler로 이동한다. - 검증 기록: - 2026-07-15: `HomeMainFragment`, `HomeRecommendationUiModels`, `ContentMainFragment`, `AudioRecommendationsUiModels`, `MainV2Activity` 직접 확인과 `rg` 검색으로 Creator Channel 진입 호출부를 확정했다. - [x] **Task 4.2: Creator Action 구현과 호출부 전환** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/action/CreatorAction.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/action/CreatorActionTest.kt` - 검증 기준: 유효 ID와 `AccessRequirement.Login`을 Action에서 처리하고 feature UI model을 입력받지 않으며, Handler가 Access 실행기와 Creator Channel navigation을 조합한다. - RED 검증 기록: - 2026-07-15: `CreatorActionTest`를 먼저 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*"`를 실행해 `CreatorAction`, `CreatorActionCommand`, `CreatorActionResult`, `CreatorActionHandler` 미정의 컴파일 오류로 RED를 확인했다. - 2026-07-15: source 전환 assertion을 추가한 뒤 같은 명령에서 `CreatorActionTest > 대상 화면은 Creator Channel Activity 대신 Creator Action을 사용한다` 실패로 기존 직접 Intent 잔존 RED를 확인했다. - GREEN 검증 기록: - 2026-07-15: `CreatorActionCommand.Profile`, `CreatorAction`, `CreatorActionHandler`, Activity/Fragment `handleCreatorAction`을 추가하고 Home/Content 호출부를 전환했다. Handler만 기존 `CreatorChannelActivity.newIntent`와 `EXTRA_CREATOR_ID` extra를 소유한다. - 2026-07-15: Creator/Community/Chat action 테스트 묶음과 Home/Content/Creator source 관련 테스트 묶음이 각각 `BUILD SUCCESSFUL`로 통과했다. - [x] **Task 4.3: Community 게시글 진입 계약 구현과 호출부 전환** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityAction.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityActionTest.kt` - 수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 검증 기준: 로그인, ID 검사, Activity result 차이를 보존하고 공통 게시글 진입점을 사용한다. - RED 검증 기록: - 2026-07-15: `CommunityActionTest`를 먼저 추가하고 action 테스트 묶음을 실행해 `CommunityAction`, `CommunityActionCommand`, `CommunityActionResult`, `CommunityActionHandler` 미정의 컴파일 오류로 RED를 확인했다. - 2026-07-15: source 전환 assertion 추가 후 `CommunityActionTest > 대상 화면은 Community Detail Activity 대신 Community Action을 사용한다` 실패로 Home/Creator 직접 상세 Intent 잔존 RED를 확인했다. - GREEN 검증 기록: - 2026-07-15: `CommunityActionCommand.PostDetail`, `CommunityAction`, `CommunityActionHandler`를 추가하고 Home의 following news/recent activity/popular community 및 Creator Channel community post click을 `handleCommunityAction`으로 전환했다. - 2026-07-15: Creator Channel의 `ActivityResultLauncher` 계약 보존을 위해 `launchIntent` 주입 경계를 두었고, action/result에는 Fragment refresh callback을 넣지 않았다. - [x] **Task 4.4: Community 변경 결과와 레거시 ActivityResult adapter 구현** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChange.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityActivityResultMapper.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChangeTest.kt` - 작업: - 작성, 수정, 삭제, 고정 변경을 `Created`, `Updated`, `Deleted`, `PinChanged`로 표현한다. - 레거시 작성/수정 Activity의 `RESULT_OK`를 명시적인 Community 변경 결과로 변환한다. - 결과 계약에는 Home/Community 탭 refresh callback 또는 Fragment 참조를 포함하지 않는다. - 검증 기준: legacy Activity result는 result code와 source로 변경 종류를 판정하고 `CommunityChange`의 post ID payload를 단위 테스트한다. delete/pin mutation 성공 경로의 실제 post ID wiring은 후속 Task 6.4에서 별도 회귀 검증한다. - RED 검증 기록: - 2026-07-15: `CommunityChangeTest`를 먼저 추가하고 action 테스트 묶음을 실행해 `CommunityChange`, `CommunityActivityResultSource`, `resolveCommunityActivityResult` 미정의 컴파일 오류로 RED를 확인했다. - GREEN 검증 기록: - 2026-07-15: `CommunityChange.Ignored/Created/Updated/Deleted/PinChanged`와 `resolveCommunityActivityResult`를 추가했다. Write `RESULT_OK`는 `Created`, Modify/Detail `RESULT_OK`는 `Updated`, 실패/취소는 `Ignored`로 변환한다. - [x] **Task 4.5: Creator Channel Community 변경 composition handler 통합** - 수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: - `handleCommunityChange(change)` 단일 함수에서 Home/Community projection 갱신을 조합한다. - Community 작성, 수정, 삭제, 고정 변경 성공 경로가 위 함수를 사용하도록 전환한다. - 기존 `ActivityResultLauncher`, Activity result 수신 시점, Fragment initial load 동작은 유지한다. - 검증 기준: - Community Action/결과가 Creator Channel Home 또는 Community Fragment를 알지 않는다. - Activity 내부의 개별 성공 경로에 `refreshHome()`과 `refreshCreatorChannelCommunity()` 조합이 반복되지 않는다. - 현재 Activity 내부 전파를 위해 전역 EventBus/observer singleton을 추가하지 않는다. - 검증 기록: - 2026-07-15: `CreatorChannelActivity`의 community write/modify/detail launcher가 raw result를 `CommunityChange`로 변환한 뒤 `handleCommunityChange`로 전달하도록 통합했다. - 2026-07-15: pin/delete 성공 경로도 각각 `CommunityChange.PinChanged`, `CommunityChange.Deleted`를 거쳐 단일 handler에서 Community projection refresh를 결정하게 했다. `Created`만 Home과 Community를 함께 갱신하고 `Updated/Deleted/PinChanged`는 Community만 갱신한다. - 2026-07-15: Activity 밖 소비자가 없고 Creator Channel Activity 내부 projection 조합으로 충분해 `SharedFlow`, EventBus, singleton observer는 추가하지 않았다. - [x] **Task 4.6: Creator/Community Phase 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck git diff --check ``` - 검증 기록: - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*"`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: Home/Content/Chat/Creator source 회귀 묶음 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `rg`로 대상 호출부를 검색해 `CreatorChannelActivity.newIntent`, `CreatorChannelCommunityDetailActivity.newIntent`는 action handler 및 Phase 1에서 제외한 MainV2Activity system/deeplink route에만 남음을 확인했다. - 2026-07-15: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했고, `./gradlew tasks --all`도 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했으며 test source 보고서는 0건이다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만 갱신된 main/test ktlint 리포트에서 Phase 4 변경 파일명 검색 결과는 0건이다. ### Phase 5: Chat 공통 Action - [x] **Task 5.1: Chat/DM 진입 유형과 결과 계약 확정** - 확인 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: 기존 room 진입, creator 기반 DM 생성, AI/DM 타입, owner/non-owner, Activity result 차이를 구분한다. - 검증 기준: - 서로 다른 동작을 하나의 nullable/boolean 다목적 함수로 합치지 않는다. - 호출 화면 후처리가 필요한 Activity result는 명시적 Chat 결과와 feature 단일 handler로 연결한다. - 확인 결과: - Chat list와 Home following chat은 기존 room ID로 AI legacy `ChatRoomActivity` 또는 V2 `DmChatRoomActivity`에 진입한다. - Creator Channel AI chat은 `AccessRequirement.AdultContent` 통과 후 기존 `CreatorChannelHomeViewModel.createChatRoom(characterId)` API 소유를 유지하고, 성공 event의 room ID만 Chat Action으로 전달한다. - Creator Channel DM은 owner의 `MainV2Activity.newChatDmIntent`와 non-owner의 creator ID 기반 `DmChatRoomActivity` 진입을 분리한다. - 현재 Chat/DM 하위 Activity는 result를 반환하지 않으므로 ActivityResult adapter는 추가하지 않는다. - 검증 기록: - 2026-07-15: ChatMain/Home/Creator/MainV2/DM Activity와 관련 tests를 확인해 `AiRoom`, `DmRoom`, `DmCreator`, `OwnerDmList` command 계약을 확정했다. MainV2Activity deeplink/system route는 기존 계약대로 제외했다. - [x] **Task 5.2: Chat Action 계약과 단위 테스트 작성** - 생성 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/ChatAction.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/ChatActionCommand.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/chat/action/ChatActionTest.kt` - 검증 기준: room/creator 식별자와 진입 유형이 명시적이고 Home/Creator UI model에 의존하지 않으며, 유효 ID 확인 뒤 `AccessRequirement.Login`을 Action이 소유한다. - RED 검증 기록: - 2026-07-15: `ChatActionTest`를 먼저 추가하고 action 테스트 묶음을 실행해 `ChatAction`, `ChatActionCommand`, `ChatActionResult`, `ChatActionHandler` 미정의 컴파일 오류로 RED를 확인했다. - 2026-07-15: source 전환 assertion 추가 후 `ChatActionTest > 대상 화면은 채팅방 Activity 대신 Chat Action을 사용한다` 실패로 직접 chat Activity helper 잔존 RED를 확인했다. - GREEN 검증 기록: - 2026-07-15: `ChatActionCommand.AiRoom/DmRoom/DmCreator/OwnerDmList`, `ChatAction`, `ChatActionHandler`, Activity/Fragment `handleChatAction`을 추가했다. Handler만 기존 `ChatRoomActivity`, `DmChatRoomActivity`, `MainV2Activity.newChatDmIntent`를 소유한다. - [x] **Task 5.3: Home/Chat/Creator Channel 호출부 전환** - 수정 예정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 검증 기준: Chat Action이 로그인 정책을 소유해 호출 화면의 중복 login guard는 제거하고, Creator Channel의 `AdultContent` 사전 조건과 room 생성 API, 생성 성공 후 이동, owner/non-owner 분기는 유지한다. - 검증 기록: - 2026-07-15: `HomeMainFragment.openFollowingChat`은 기존 로그인 guard를 유지한 채 `ChatActionCommand.AiRoom/DmRoom`으로 전환했다. - 2026-07-15: `ChatMainFragment.onChatRoomClick`은 기존 AI/DM 분기를 유지하되 navigation을 `handleChatAction`으로 위임했다. - 2026-07-15: `CreatorChannelActivity`의 AI chat 생성 API는 `homeActionDelegate?.createChatRoom(characterId)`에 그대로 남기고, 생성 완료 room ID와 owner/non-owner DM navigation만 `ChatAction`으로 전환했다. - 2026-07-16: 첫 번째 기록의 화면-local login guard 유지는 후속 P1에서 대체되었다. 최종 계약은 `ChatAction`이 `AccessRequirement.Login`을 소유하고 `HomeMainFragment.openFollowingChat`은 guard를 중복 실행하지 않는 것이다. - [x] **Task 5.4: Chat Phase 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*" ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck git diff --check ``` - 검증 기록: - 2026-07-15: Chat Action 테스트와 Home/Chat/Creator source 회귀 묶음이 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `rg` 검색 결과 대상 화면의 직접 `ChatRoomActivity.newIntent`, `DmChatRoomActivity.newIntentByRoomId`, `DmChatRoomActivity.newIntentByCreatorId` 호출은 제거됐고, handler 및 Phase 1에서 제외한 `MainV2Activity` deeplink route만 남았다. - 2026-07-15: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했고, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만 갱신된 ktlint 리포트에서 Phase 5 변경 파일명 검색 결과는 0건이다. - 2026-07-15: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. ### Phase 6: 최종 소유 경계와 문서·코드 동기화 - [x] **Task 6.1: 최종 소유 경계를 PRD와 계획 문서에 고정** - 수정 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - Access는 접근 판단과 로그인/본인인증/성인 설정 UX 실행을 소유한다. - 도메인 Action은 안정적인 입력 검사, 필요한 `AccessRequirement` 선택, Access 실행 요청과 명시적 결과를 소유한다. - Action Handler는 Access 실행기 주입과 레거시 navigation adapter를 소유한다. - feature는 UI model -> command 변환, 화면별 query/API와 생성·mutation API, `ActivityResult` 및 refresh/callback/projection 조합을 소유한다. - 도메인 관련 코드 전체가 아니라 여러 호출부에서 같아야 하는 유효성·접근 정책·결과 계약만 Action으로 이관하고, 실제 Android 이동은 Handler에 둔다. - 보호된 도메인 Action 호출부의 중복 Access guard 제거 원칙과 직접 Access 사용 예외를 명시한다. - `AppDI.kt`는 실제 외부 의존성, 공유 생명주기 또는 구현 교체가 필요한 경우에만 조립 경계로 사용하고 상태 없는 Action/Handler의 DI 등록을 강제하지 않는다. - 검증 기준: PRD와 계획 문서가 같은 네 계층 소유 경계와 이관/예외 기준을 사용한다. - 검증 기록: - 2026-07-16: 합의한 최종 소유 경계를 두 문서에 명시했고, 도메인 전체 이관과 navigation-only 이관을 모두 배제했다. 이번 Task에서는 문서만 수정하고 운영/테스트 코드는 변경하지 않았다. - [x] **Task 6.2: Phase 1~5의 규범 계약과 실제 파일명 동기화** - 수정 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 작업: - Phase 1 Access와 Phase 2/3 Content·Live 소유 경계는 유지한다. - Phase 4 Creator Action에 Login/Blocked 계약을 명시하고 Creator/Community Action 파일명을 실제 singular 이름으로 정정한다. - Phase 5 Chat Action에 Login 소유를 명시하고 화면-local 중복 guard 제거를 규범 계약으로 확정한다. - Creator Channel AI Chat의 `AdultContent` 사전 조건과 `createChatRoom(characterId)` API는 feature에, 생성 성공 room navigation은 Chat Action에 유지한다. - 기존 2026-07-15 검증 기록은 삭제하지 않고, 후속 P1에서 대체된 login guard 기록에 최종 계약을 덧붙인다. - 합의된 반복 정책 대상과 parent-child/same-feature/system/deeplink/notification/목록/단일 owner 예외를 구분한다. - 검증 기준: Phase 1~5 본문, Review Follow-up, PRD 카탈로그가 서로 모순되지 않고 실제 파일명을 가리킨다. - 검증 기록: - 2026-07-16: Creator/Community/Chat Action 파일명, Creator/Chat Login 계약, Creator AI Chat 생성 소유권, 직접 의존 예외를 문서에 동기화했다. 과거 검증 기록은 이력으로 보존했다. - [x] **Task 6.3: 최종 소유 경계 기준 코드 재점검과 최소 보완** - 확인 범위: - `app/src/main/java/kr/co/vividnext/sodalive/v2/access/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/content/action/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/action/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/` - `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/` - 해당 Action 호출 Activity/Fragment와 `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - Action이 안정적인 입력 검사와 접근 요구를 소유하고 Handler가 Access/navigation을 조합하는지 확인한다. - 보호된 Action 호출부에 같은 Access guard가 중복되거나, Action/Handler에 화면별 query/API/refresh callback이 유입된 곳이 있는지 확인한다. - `AppDI.kt` 등록은 실제 조립 필요성이 확인된 경우에만 추가하고, 현재 구조가 최종 소유 경계와 일치하면 변경하지 않는다. - 검증 기준: 발견된 불일치만 최소 수정하고, 합의된 예외나 단일 owner 기능을 형식적인 Action/DI로 이관하지 않는다. - 검증 기록: - 2026-07-16: Access, Content, Live, Creator, Community, Chat Action/Handler와 호출 Activity/Fragment, `AppDI.kt`를 재점검했다. Action은 ID/command 검사와 접근 요구 선택을 소유하고 Handler는 Access 실행기와 navigation adapter를 조합하는 구조를 유지한다. - 2026-07-16: 보호된 Creator/Community/Chat 호출부의 중복 guard와 Action/Handler에 feature query/API/refresh callback이 유입된 위치를 검색했다. `LiveActionCoordinator`는 문서화된 presentation adapter 예외로 `refreshHome` callback을 주입받고, Community/Chat/Creator/Content Action 결과 타입에는 화면별 callback이 없다. - 2026-07-16: `AppDI.kt`에 상태 없는 Action/Handler 등록을 추가할 필요가 없음을 확인했다. 실제 불일치 보완은 Task 6.4의 Community mutation wiring 회귀 테스트 추가로 제한했다. - [x] **Task 6.4: Community delete/pin mutation 성공 경로 wiring 회귀 테스트 보강** - 수정 예정 파일: - `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` - 작업: - pin 성공 경로가 실제 `item.postId`로 `handleCommunityChange(CommunityChange.PinChanged(...))`를 호출하는지 검증한다. - delete 성공 경로가 실제 `item.postId`로 `handleCommunityChange(CommunityChange.Deleted(...))`를 호출하는지 검증한다. - `Created`는 Home/Community, `Updated`/`Deleted`/`PinChanged`는 Community projection으로 연결되는 composition 계약을 함께 고정한다. - 검증 기준: `CommunityChange` 객체의 payload 보유만이 아니라 mutation 성공 경로에서 실제 post ID 전달과 단일 handler 연결이 회귀 테스트로 보호된다. - RED/GREEN 검증 기록: - 2026-07-16: `CreatorChannelActivitySourceTest`에 pin/delete 성공 경로가 실제 `item.postId`로 `CommunityChange.PinChanged`/`Deleted`를 생성하고 `handleCommunityChange`로만 전달하는지 검증하는 source test 2건과, `handleCommunityChange`의 projection 갱신 소유권 테스트 1건을 추가했다. - 2026-07-16: 새 테스트는 현재 구현을 회귀 고정하는 characterization 성격이므로 별도 production 변경 없이 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: Kotlin LSP는 환경에 `kotlin-ls`가 설치되어 있지 않아 실행할 수 없었고, Gradle test/compile로 대체 검증했다. - [x] **Task 6.5: 소유 경계 동기화 집중 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" --rerun-tasks ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks ./gradlew :app:ktlintCheck --rerun-tasks git diff --check HEAD ``` - 검증 기준: focused 테스트와 컴파일, test source ktlint, whitespace 검증이 통과한다. 전체 main ktlint가 기존 기준선으로 실패하면 Phase 6 변경 파일의 신규 위반이 없음을 리포트에서 확인하고 실패 위치를 기록한다. - 검증 기록: - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`는 통과했다. - 2026-07-16: `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했다. `app/build/reports/ktlint/ktlintMainSourceSetCheck`, `ktlintTestSourceSetCheck` 리포트에서 Phase 6/7 변경 파일명 검색 결과는 0건이다. ### Phase 7: 최종 의존 방향 및 통합 검증 - 상태: Phase 6의 코드 점검과 집중 검증 완료 후 최종 기준으로 다시 실행한다. 아래 2026-07-15 검증 기록은 번호 변경 전 Phase 6의 기준선 이력으로 보존하며, Task 7.1~7.5는 모두 재검증 대상으로 재개방한다. - [x] **Task 7.1: feature 간 직접 navigation/Coordinator 의존 재검사** - 확인 범위: `app/src/main/java/kr/co/vividnext/sodalive/v2/**/*.kt` - 실행 명령: ```bash rg -n '^import kr\.co\.vividnext\.sodalive\.v2\.(main|creator|live)\..*(Activity|Fragment|Coordinator)' app/src/main/java/kr/co/vividnext/sodalive/v2 --glob '*.kt' ``` - 검증 기준: 합의된 반복 navigation/정책의 직접 의존은 Action/Handler로 대체된다. parent-child Fragment composition, same-feature child flow, `MainV2Activity` system/deeplink/audio notification route, Home on-air 목록 화면 진입과 단일 owner 기능은 예외 근거가 기록되어 있으면 유지한다. - 검증 기록: - 2026-07-15: 직접 feature Activity/Fragment/Coordinator import와 destination Activity `newIntent`/`Intent(...)` 호출을 검색했다. 남은 직접 destination 생성은 `ContentActionHandler`, `CreatorActionHandler`, `CommunityActionHandler`, `ChatActionHandler`, `LiveActionCoordinator`, parent-child Fragment composition, same-feature child flow, Phase 1에서 제외한 `MainV2Activity` system/deeplink/audio notification route로 분류했다. - 2026-07-15: `HomeMainFragment`의 `HomeOnAirLiveActivity.newIntent`는 on-air 목록 화면 자체 진입이고, `MainV2Activity` 직접 route는 기존 deeplink/system 계약 보존 대상이므로 Phase 6에서 Action으로 옮기지 않는다. - 2026-07-16: 동일 import 검색을 재실행했다. 남은 항목은 Action/Handler, `MainV2Activity` system/deeplink/audio notification route, Home on-air 목록 화면 진입, parent-child Fragment composition, same-feature child flow로 분류되어 추가 이관 대상은 없었다. - [x] **Task 7.2: 이전 helper와 중복 함수 제거** - 삭제/수정 대상: 각 Phase에서 호환을 위해 남긴 adapter, 이전 `ensure...`, 중복 `open...`, 직접 route helper. - 검증 기준: 모든 호출부가 새 공개 진입점을 사용하는 것이 확인된 구현만 제거한다. - 검증 기록: - 2026-07-15: `ensureMainV2NavigationAllowed`, `ensureLoginAndAdultAuth`, `MainV2LoginGuard`, `isLoggedIn(`, `showLoginActivity(` 검색 결과 운영 코드에는 레거시 `MyPageFragment` 호환을 위한 `MainV2Activity.showLoginActivity()`만 남았다. 해당 메서드는 Phase 1에서 의도적으로 유지한 호환 진입점이므로 삭제하지 않는다. - 2026-07-15: 남은 `openAudioContentDetail`, `openSeriesDetail`, `openCreatorProfile`, `openFollowingChat`, `enterLiveRoom`은 UI model을 explicit command 또는 coordinator 호출로 변환하는 로컬 adapter이거나 기존 목록 화면 진입 wrapper라 동작 보존을 위해 유지한다. - 2026-07-16: 이전 helper와 중복 함수 검색을 재실행했다. `MainV2Activity.showLoginActivity()`는 레거시 MyPage 호환 진입점으로 유지하고, 나머지 `open...`/`enter...` 함수는 UI model을 command로 바꾸거나 기존 목록 화면을 여는 로컬 adapter라 삭제하지 않았다. - [x] **Task 7.3: 미추출 도메인 재평가** - 확인 대상: FanTalk, Donation, Schedule, Ranking, Discovery. - 작업: 실제 반복 호출과 동일 정책이 생겼는지 확인하고 추출 또는 현행 유지 근거를 기록한다. - 검증 기준: 형식적 Action 추가 없이 단일 사용 기능은 기존 feature에 유지한다. - 검증 기록: - 2026-07-15: FanTalk는 Creator Channel 단일 owner에서 write/detail result 후 FanTalk와 Home projection을 함께 갱신하는 흐름이며, 별도 독립 호출부가 없어 Action 추출 대상에서 제외했다. - 2026-07-15: Donation, Schedule은 Creator Channel 내부 tab/host composition과 UI projection 성격이 강하고 동일 navigation/action 정책을 공유하는 독립 호출부가 없어 현행 feature 소유로 유지한다. - 2026-07-15: Ranking/Discovery는 현재 Content/Home의 집계·표시 흐름 또는 widget 성격으로 남아 있고, 이미 Content Action이 상세 진입만 소유하므로 추가 도메인 Action을 만들지 않는다. - 2026-07-16: FanTalk, Donation, Schedule, Ranking, Discovery 관련 호출부를 다시 검색했다. 새 반복 navigation/접근 정책은 확인되지 않았고, 단일 owner 또는 화면별 집계/표시 흐름으로 남기는 기존 판단을 유지한다. - [x] **Task 7.4: 결과 전달 방식과 observer 필요성 재검사** - 확인 대상: Content, Live, Community, Chat 및 후속 mutation 흐름. - 작업: - 하위 Activity 결과가 `ActivityResult -> 명시적 결과 -> feature 단일 handler`로 연결되는지 확인한다. - 동일한 raw result 해석과 refresh 조합이 여러 호출부에 남아 있는지 확인한다. - `SharedFlow`/observer 도입 지점이 있다면 Activity 밖의 독립 소비자, lifecycle, replay, 중복 처리 근거를 기록한다. - 검증 기준: 직접 결과 전달로 충분한 흐름에 전역 EventBus 또는 singleton observer가 추가되지 않는다. - 검증 기록: - 2026-07-15: `ActivityResult`, `RESULT_OK`, `resolve*ActivityResult`, `SharedFlow`, `EventBus`, `observeForever` 계열 검색으로 결과 전달 방식을 재검사했다. Community는 `resolveCommunityActivityResult`와 `handleCommunityChange`, Live 생성은 `resolveLiveCreationResult`와 `handleLiveCreationResult`를 통해 명시적 결과와 단일 handler로 연결된다. - 2026-07-15: FanTalk의 raw `RESULT_OK` 처리는 Creator Channel 내부 단일 owner 흐름이고 Phase 6에서 추출하지 않기로 한 도메인이므로 유지한다. `BannerView`의 `lifecycleObserver`는 UI lifecycle local observer이며 전역 event/observer가 아니다. - 2026-07-15: 신규 `SharedFlow`, EventBus, singleton observer, `observeForever` 기반 전파는 추가되지 않았음을 확인했다. - 2026-07-16: 결과 전달과 observer 검색을 재실행했다. Community와 Live 생성은 명시적 결과와 단일 handler를 유지하고, FanTalk raw `RESULT_OK`는 단일 owner 예외로 남았다. 새 `SharedFlow`, EventBus, singleton observer, production `observeForever` 전파는 확인되지 않았다. - [x] **Task 7.5: 통합 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*" ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintCheck git diff --check ``` - 수동 확인: - 로그인/본인인증/성인 설정 차단과 허용 흐름 - Home, Content, Creator Channel, On-air의 오디오/시리즈/라이브 진입 - Creator Channel 및 Community 상세 진입 - Chat/DM 생성과 기존 room 진입 - 기존 Activity result와 새로고침 동작 - 기대 결과: `v2` 전체 관련 테스트와 컴파일, test source ktlint, whitespace 검증이 통과한다. 전체 main ktlint가 기존 기준선으로 실패하면 Phase 6/7 변경 파일의 신규 위반이 없음을 확인하고 기록한다. - 검증 기록: - 2026-07-15: Action 통합 테스트 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.content.action.*" --tests "kr.co.vividnext.sodalive.v2.live.action.*" --tests "kr.co.vividnext.sodalive.v2.creator.action.*" --tests "kr.co.vividnext.sodalive.v2.community.action.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" --rerun-tasks`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*" --rerun-tasks`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-15: `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했고 test source 리포트는 0건이다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만, main/test ktlint 리포트에서 현재 변경 Kotlin 파일명 검색 결과는 0건이다. - 2026-07-15: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. Kotlin LSP는 환경에 `kotlin-ls`가 없어 Gradle compile/test로 대체 검증했다. - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*" --rerun-tasks`를 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`는 통과했다. - 2026-07-16: `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했다. main/test ktlint 리포트에서 Phase 6/7 변경 Kotlin 파일명 검색 결과는 0건이다. ### Phase 8: 푸시/딥링크의 도메인 Action 연결 - 상태: 사용자 후속 요구로 기존 system/deeplink 예외를 재개방한다. 새 범용 router는 만들지 않고 기존 payload 해석 위치에서 이미 분리된 Action을 호출한다. - [x] **Task 8.1: 푸시/딥링크 계약과 우선순위 문서화** - 수정 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 확정 계약: - DM deep link room은 Chat Action, 그 외 room은 Live 진입으로 처리하고 channel ID보다 우선한다. - 진행 중 Live는 기존 입장 정책을 실행하고 예약/미진행 Live는 레거시 Live Detail을 표시한다. - Community `postId`가 있으면 PostDetail Action을 실행하고, 없을 때만 creator profile로 fallback한다. - Community `deep_link_sub5`/path ID는 기존 `creatorId`이므로 `routeByDeepLinkValue("community")`는 Creator fallback이며, 별도 query/canonical `postId`를 ID로 대체 해석하지 않는다. - 현재 DM은 `${URISCHEME}://chat/{roomId}`를 Chat Action으로 처리한다. legacy `message_id`는 room ID로 재해석하지 않고 과거 알림 호환 route로만 유지한다. - Content/Series/Creator/Community/DM은 기존 Action을 직접 재사용하며 새 `PushRouteAction`을 만들지 않는다. - message/audition/audio player 등 Action이 없는 system-only route는 현행 처리를 유지한다. - 검증 기록: - 2026-07-16: 사용자가 `DeepLinkActivity`의 최소 변경을 승인했고, Live 및 Community payload 우선순위와 fallback 계약을 PRD와 본 계획에 반영했다. - [x] **Task 8.2: 푸시/딥링크 라우팅 회귀 테스트를 RED로 추가** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/main/DeepLinkActivitySourceTest.kt` - 검증 기준: - Main과 DeepLink 진입점이 Content/Creator/Community/Chat Action을 호출한다. - non-DM room이 channel보다 먼저 Live로 라우팅되고, Community post가 creator fallback보다 먼저 처리된다. - audio detail notification이 Content Action을 사용한다. - 구현 전 focused 테스트가 새 기대 조건으로 실패하고, 실패 원인이 아직 연결되지 않은 Action/Live route임을 확인한다. - 검증 기록: - 2026-07-16: `MainV2ActivitySourceTest`와 `DeepLinkActivitySourceTest`에 DM/Content/Series/Creator/Community Action 연결, non-DM Live 우선, Community post 우선, audio detail notification Action 연결 기대 조건을 추가했다. - 2026-07-16: 운영 코드 변경 전에 두 테스트 클래스를 `--rerun-tasks`로 실행했고 15개 중 신규 기대 조건 8개가 실패했다. 실패 지점은 기존 직접 Activity 이동, `MainV2Activity`의 non-DM room 분기 누락, Community post 우선 분기 누락으로 확인되어 의도한 RED 상태다. - [x] **Task 8.3: `MainV2Activity` cold start/onNewIntent Action 연결** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - 작업: - DM, Content, Series, Creator, Community route를 기존 도메인 Action에 연결한다. - non-DM room을 channel보다 먼저 `LiveActionCoordinator.enterLiveRoom`에 연결한다. - Community post를 creator fallback보다 먼저 처리한다. - audio detail notification을 Content Action에 연결하고 system-only route는 유지한다. - 검증 기준: focused source 테스트와 `LiveEntryPolicyTest`가 통과한다. - 검증 기록: - 2026-07-16: DM, Content, Series, Creator, Community 경로를 기존 Action에 연결하고, non-DM room을 channel보다 먼저 `LiveActionCoordinator.enterLiveRoom`으로 처리하도록 보완했다. `deep_link_value=live` fallback과 audio detail notification도 각각 Live coordinator와 Content Action에 연결했다. - 2026-07-16: Live coordinator는 기존 `LiveEntryPolicy`를 그대로 사용하므로 channel 정보가 없는 예약/미진행 방은 레거시 Live Detail을 표시하고, 진행 중 방은 기존 무료/유료/비밀번호 정책에 따라 입장한다. - [x] **Task 8.4: 승인된 `DeepLinkActivity` foreground Action 연결** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt` - 작업: - foreground Content, Series, Creator, Community, DM 직접 navigation을 기존 Action 호출로 교체한다. - Live는 현재 Main 전달 계약을 유지하고 Main의 Live Action 진입점에서 처리한다. - Community post 우선 및 creator fallback 계약을 적용한다. - FCM payload 보존, foreground Live broadcast, system-only route는 변경하지 않는다. - 검증 기준: DeepLink source 테스트와 FCM payload 회귀 테스트가 통과한다. - 검증 기록: - 2026-07-16: 승인 범위 안에서 `DeepLinkActivity`의 foreground DM, Content, Series, Creator, Community 직접 navigation을 기존 Action 호출로 교체했다. Live의 Main 전달, foreground LiveRoom broadcast, message/audition/payment route는 유지했다. - 2026-07-16: Community `postId` 분기를 creator fallback보다 앞에 두어 PostDetail Action을 우선하고, post ID가 없을 때만 Creator Action으로 이동하도록 연결했다. - [x] **Task 8.5: 집중 및 최종 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --tests "kr.co.vividnext.sodalive.main.DeepLinkActivitySourceTest" --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.LiveEntryPolicyTest" --rerun-tasks ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks ./gradlew :app:ktlintCheck --rerun-tasks git diff --check HEAD ``` - 수동 확인 항목: - 진행 중 Live push는 라이브에 입장한다. - 예약 Live push는 레거시 Live Detail을 표시한다. - Community push는 post ID가 있으면 게시글 상세, 없으면 Creator Channel을 표시한다. - cold start, foreground, `onNewIntent`, 앱 내 알림 목록 경로가 동일 우선순위를 사용한다. - 실행 상태: 연결 기기가 검증 직전 해제되어 실제 알림 터치/화면 표시 확인은 이번 실행에서 수행하지 못했다. 라우팅 계약과 우선순위는 아래 자동화 검증으로 확인했고 실기기 시나리오는 잔여 수동 확인 항목으로 기록한다. - 기대 결과: focused/전체 V2 테스트, 컴파일, test ktlint, whitespace 검증이 통과한다. 전체 main ktlint가 기존 기준선으로 실패하면 이번 변경 파일의 신규 위반이 없음을 리포트에서 확인해 기록한다. - 검증 기록: - 2026-07-16: RED 확인 후 `MainV2ActivitySourceTest`와 `DeepLinkActivitySourceTest`를 재실행해 15개 전체가 통과했다. - 2026-07-16: 위 두 테스트와 `SodaFirebaseMessagingServiceSourceTest`, `LiveEntryPolicyTest` 집중 묶음을 `--rerun-tasks`로 실행해 `BUILD SUCCESSFUL`을 확인했다. FCM의 기존 deep-link/room payload 보존과 Live의 상세/입장 정책이 함께 통과했다. - 2026-07-16: 후속 리뷰에서 Community `deep_link_sub5`가 legacy `creatorId`, 별도 `postId` query가 게시글 ID라는 기존 계약을 재확인했다. 따라서 상위 post Action 우선 분기와 `routeByDeepLinkValue("community")`의 Creator fallback을 유지했다. - 2026-07-16: 현재 DM 발행 계약은 `${URISCHEME}://chat/{roomId}`이고 Chat Action으로 연결되어 있음을 확인했다. legacy `message_id`는 DM room ID 근거가 없어 재해석하지 않고 과거 알림 호환 경로로만 유지하기로 결정했다. - 2026-07-16: `MainV2Activity`가 raw deep-link URL의 `postId` query를 canonical `EXTRA_COMMUNITY_POST_ID`로 정규화한다는 테스트를 먼저 추가해 11개 중 1개 실패를 확인하고, 매핑을 보완한 뒤 11개 전체 통과를 확인했다. - 2026-07-16: 전체 V2 테스트 첫 실행에서 기존 `MainV2AudioNotificationRouteSourceTest`의 직접 Activity 기대 1건이 실패해 Content Action 기대 조건으로 정정했고, 단일 테스트와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*" --rerun-tasks` 재실행이 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: 최종 집중 묶음에 `MainV2ActivitySourceTest`, `MainV2AudioNotificationRouteSourceTest`, `DeepLinkActivitySourceTest`, `SodaFirebaseMessagingServiceSourceTest`, `LiveEntryPolicyTest`를 포함해 `BUILD SUCCESSFUL`을 확인했다. 이후 Community fallback과 legacy message 계약을 명시한 두 source 테스트도 재실행해 통과했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`과 `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 550건으로 실패했지만 `DeepLinkActivity.kt`, `MainV2Activity.kt`는 main ktlint 리포트 검색 결과 0건이다. - 2026-07-16: `git diff --check HEAD`는 whitespace 오류 없이 통과했다. 코드 리뷰에서 푸시/딥링크 Action 연결 범위의 P0~P2 발견 사항은 없었다. - 2026-07-16: `adb devices`에서 기기 1대를 처음 확인했으나 설치 상태 확인 전에 연결이 해제됐고 재조회 결과 기기가 없어 실기기 알림 터치 검증은 수행하지 못했다. ### Phase 9: Live 푸시 목적지 판단 로딩 표시 - 상태: `LiveViewModel.getRoomDetail()`은 비동기 처리 중 `isLoading`, 실패 시 `toastLiveData`를 발행하지만 `MainV2Activity`가 두 상태를 관찰하지 않아 푸시 진입 중 진행·실패 여부가 보이지 않는 문제를 보완한다. - 후속 결정: Task 9.1~9.4의 `screen_live_loading` 문구 계약과 검증 기록은 당시 실행 이력으로 유지한다. 현재 계약은 cold start 딥링크의 공통 1초 대기와 Live 추가 조회가 같은 문구 없는 spinner를 공유하는 Task 9.5 이후 내용이다. - [x] **Task 9.1: Live 푸시 로딩 UX 계약 문서화** - 수정 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 확정 계약: - `MainV2Activity`에서 Live room detail 조회 중 기존 `LoadingDialog`와 `R.string.screen_live_loading` 문구를 표시한다. - 조회 성공·실패로 `LiveViewModel.isLoading`이 `false`가 되면 로딩을 해제한다. - 조회 실패 메시지는 `LiveViewModel.toastLiveData`를 관찰해 기존 Toast 방식으로 표시한다. - 별도 네트워크 판단이 없는 다른 도메인 route까지 범용 push loader로 확장하지 않는다. - 검증 기록: - 2026-07-16: `HomeMainFragment`, `HomeOnAirLiveActivity`, `CreatorChannelActivity`는 동일 `LiveViewModel` 상태를 `LoadingDialog`와 Toast에 연결하지만 `MainV2Activity`만 관찰하지 않는 차이를 확인했고, 이를 원인으로 확정했다. - 2026-07-16: 사용자가 기존 `LoadingDialog`/Toast 패턴을 Live 푸시 판단 구간에 적용하는 최소 설계를 승인했다. - [x] **Task 9.2: `MainV2Activity` Live 진입 상태 회귀 테스트 RED 추가** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` - 검증 기준: - `MainV2Activity`가 `LiveViewModel.isLoading`과 `toastLiveData`를 관찰한다. - loading `true`에는 `screen_live_loading` 문구로 `LoadingDialog`를 표시하고 `false`에는 해제한다. - 구현 전 focused 테스트가 아직 observer가 없다는 이유로 실패한다. - 검증 기록: - 2026-07-16: `MainV2ActivitySourceTest`에 Live 푸시 판단 중 LoadingDialog 표시·해제와 실패 Toast observer 기대 조건을 추가했다. - 2026-07-16: 운영 코드 변경 전에 focused 테스트를 `--rerun-tasks`로 실행해 12개 중 새 테스트 1개가 `setupLiveEntryObservers()` 부재로 실패하는 의도한 RED 상태를 확인했다. - [x] **Task 9.3: `MainV2Activity` Live 푸시 로딩·실패 안내 연결** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - 작업: - 기존 `LoadingDialog`를 재사용한다. - `setupView()`에서 Live 진입 상태 observer를 등록해 cold start와 `onNewIntent` 경로에 공통 적용한다. - Live Action/정책, payload 우선순위, 다른 도메인 route는 변경하지 않는다. - 검증 기준: Task 9.2 focused 테스트와 기존 푸시/Live 회귀 테스트가 통과한다. - 검증 기록: - 2026-07-16: `MainV2Activity.setupLiveEntryObservers()`를 추가해 `LiveViewModel.isLoading`이 `true`이면 기존 `LoadingDialog`에 `R.string.screen_live_loading`을 표시하고 `false`이면 해제하도록 연결했다. - 2026-07-16: `LiveViewModel.toastLiveData`를 기존 `BaseActivity.showToast`에 연결했으며 Live Action, payload 우선순위, 다른 도메인 route는 변경하지 않았다. - 2026-07-16: 구현 후 `MainV2ActivitySourceTest` 12개 전체가 `BUILD SUCCESSFUL`로 통과했다. - [x] **Task 9.4: 집중 및 최종 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --rerun-tasks ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.*" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks git diff --check HEAD ``` - 수동 확인 항목: - Live 푸시를 터치하면 상태 판단 중 `라이브를 불러오고 있습니다.` 로딩이 표시된다. - 진행 중 Live 입장 또는 예약 Live Detail 표시 직전에 로딩이 해제된다. - Live 조회 실패 시 로딩이 해제되고 오류 Toast가 표시된다. - 기대 결과: focused/Live 회귀 테스트, 컴파일, test ktlint, whitespace 검증이 통과한다. - 검증 기록: - 2026-07-16: `MainV2ActivitySourceTest` focused 테스트를 `--rerun-tasks`로 실행해 12개 전체가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: `MainV2ActivitySourceTest`, `SodaFirebaseMessagingServiceSourceTest`, `v2.live.action.*` 회귀 묶음을 `--rerun-tasks`로 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `./gradlew tasks --all`, `git diff --check HEAD`가 통과했다. test ktlint 보고서는 0건이다. - 2026-07-16: `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 550건으로 실패했지만, 현재 생성된 main ktlint 보고서에서 `MainV2Activity.kt` 검색 결과는 0건이다. - 2026-07-16: `LiveViewModel.getRoomDetail()`이 성공 callback보다 먼저 `isLoading=false`를 발행하는 순서와 `MainV2Activity` 생명주기 observer 등록 위치를 재검토했으며, 이번 로딩 연결 범위에서 P0~P2 추가 발견 사항은 없었다. - 2026-07-16: `adb devices` 결과 연결 기기가 없어 Live 푸시 터치, 진행/예약 분기, 실패 Toast의 실기기 수동 확인은 수행하지 못했다. 자동 회귀 검증은 완료했고 수동 시나리오는 연결 기기와 유효한 Live payload가 준비되면 확인할 잔여 항목으로 기록한다. - [x] **Task 9.5: 딥링크 공통 대기 로딩 UX 계약 보완** - 수정 파일: - `docs/20260714_V2_공통_접근_가드와_도메인_액션/prd.md` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 확정 계약: - cold start에서 `Constants.EXTRA_DATA` 또는 audio notification route가 있으면 기존 1초 지연 동안 문구 없는 `LoadingDialog`를 표시한다. - Live route는 공통 대기 종료 후 room detail 조회가 끝날 때까지 같은 spinner를 유지한다. - 다른 도메인 route는 공통 대기 종료 시 spinner를 해제하고 기존 Action으로 이동한다. - 일반 앱 실행과 지연이 없는 `onNewIntent`에 인위적인 1초 대기를 추가하지 않는다. - Live 조회 실패 Toast 계약은 유지한다. - 검증 기록: - 2026-07-16: 모든 cold start route가 `MainV2Activity.onCreate()`의 기존 1초 지연 블록을 공유하고, Live만 이후 `LiveViewModel.getRoomDetail()` 네트워크 조회를 수행함을 확인했다. - 2026-07-16: 사용자가 도메인별 문구 대신 공통 1초 대기와 Live 추가 조회에 문구 없는 spinner를 사용하는 권장 방향을 승인했다. - [x] **Task 9.6: 공통 대기와 Live 조회 로딩 상태 결합** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` - 작업: - cold start route 대기 상태와 Live 조회 상태를 별도 Boolean으로 관리한다. - 두 상태 중 하나라도 진행 중이면 기존 `LoadingDialog.show(screenWidth)`를 호출하고 둘 다 종료되면 해제한다. - `R.string.screen_live_loading` 문구 의존을 제거하고 새 문자열 리소스는 추가하지 않는다. - 검증 기준: - 일반 앱 실행에는 공통 로딩 상태가 활성화되지 않는다. - cold start 딥링크와 audio notification route는 1초 대기 동안 로딩 상태가 활성화된다. - Live 조회가 시작되면 공통 대기 종료 시점에도 로딩이 유지된다. - focused 테스트에서 구현 전 RED, 구현 후 GREEN을 확인한다. - RED 검증 기록: - 2026-07-16: 기존 Live 전용 문구 테스트를 cold start 공통 대기, Live 연속 로딩, 문구 없는 spinner 계약으로 변경했다. - 2026-07-16: 운영 코드 변경 전에 `MainV2ActivitySourceTest`를 `--rerun-tasks`로 실행해 12개 중 새 계약 테스트 1개가 `updateRouteLoadingDialog()` 부재로 실패하는 의도한 RED 상태를 확인했다. - GREEN 검증 기록: - 2026-07-16: `MainV2Activity`에 `isDeferredRouteLoading`과 `isLiveEntryLoading`을 추가하고 `updateRouteLoadingDialog()`에서 두 상태를 결합했다. - 2026-07-16: cold start의 `Constants.EXTRA_DATA` 또는 audio notification route가 있으면 기존 1초 지연 전에 공통 상태를 활성화하고, route 실행 후 비활성화하도록 연결했다. - 2026-07-16: Live observer는 별도 조회 상태만 갱신하며, `LoadingDialog.show(screenWidth)`를 사용해 `screen_live_loading` 문구 의존과 신규 문자열 추가 없이 같은 spinner를 공유한다. - 2026-07-16: 구현 후 `MainV2ActivitySourceTest` 12개 전체가 `BUILD SUCCESSFUL`로 통과했다. - [x] **Task 9.7: 집중 및 최종 회귀 검증** - 실행 명령: ```bash ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --rerun-tasks ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.MainV2AudioNotificationRouteSourceTest" --tests "kr.co.vividnext.sodalive.fcm.SodaFirebaseMessagingServiceSourceTest" --tests "kr.co.vividnext.sodalive.v2.live.action.*" --rerun-tasks ./gradlew :app:compileDebugKotlin ./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks git diff --check HEAD ``` - 수동 확인 항목: - Content, Community, Creator, DM 등 cold start 푸시를 터치하면 공통 1초 대기 동안 문구 없는 spinner가 표시되고 목적지 이동 시 해제된다. - Live cold start 푸시는 공통 대기에서 room detail 조회까지 spinner가 끊기지 않는다. - 일반 앱 실행과 지연 없는 non-Live `onNewIntent`에는 불필요한 spinner가 표시되지 않는다. - 기대 결과: focused/route/FCM/Live 회귀 테스트, 컴파일, test ktlint, whitespace 검증이 통과한다. - 검증 기록: - 2026-07-16: `MainV2ActivitySourceTest` focused 테스트를 `--rerun-tasks`로 실행해 12개 전체가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: `MainV2ActivitySourceTest`, `MainV2AudioNotificationRouteSourceTest`, `SodaFirebaseMessagingServiceSourceTest`, `v2.live.action.*` 회귀 묶음을 `--rerun-tasks`로 실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `./gradlew tasks --all`, `git diff --check HEAD`가 통과했다. test ktlint 보고서는 0건이다. - 2026-07-16: test ktlint 첫 실행은 sandbox의 Gradle wrapper lock 접근 제한으로 시작되지 않았고, 동일 명령을 승인된 캐시 접근으로 재실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-07-16: `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 550건으로 실패했지만, 현재 main ktlint 보고서에서 `MainV2Activity.kt` 검색 결과는 0건이다. - 2026-07-16: cold start route 상태와 Live 조회 상태의 전환 순서, 일반 실행의 초기 `false` 조건, `onNewIntent` 비지연 경로를 재검토했으며 이번 후속 범위에서 P0~P2 추가 발견 사항은 없었다. - 2026-07-16: `adb devices` 결과 연결 기기가 없어 도메인별 cold start 푸시와 Live 연속 spinner의 실기기 수동 확인은 수행하지 못했다. 자동 회귀 검증은 완료했고 수동 시나리오는 잔여 확인 항목으로 기록한다. ### Historical Review Follow-up: Action 계약 보완 - 이 절은 Phase 6 추가 전에 완료한 P1/P2/P3 이력이다. P2에서 남은 mutation 성공 경로 wiring 검증은 Task 6.4에서 별도 재개한다. - `P0`~`P3`는 Phase가 아니라 리뷰 이슈 우선순위다. `P0`은 즉시 차단해야 하는 치명적 문제, `P1`은 병합 전 해결할 높은 우선순위 문제, `P2`는 중요하지만 후속 보완 가능한 문제, `P3`는 낮은 우선순위의 정리·개선 사항을 뜻한다. - [x] **P1: Chat Action 로그인 접근 정책 소유** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/ChatAction.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/ChatActionHandler.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/chat/action/ChatActionTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt` - 검증 기록: - 2026-07-16: `ChatAction`이 유효 ID 확인 후 `AccessRequirement.Login`을 검사하고 차단 시 `ChatActionResult.Blocked(AccessRequirement.Login)`을 반환하도록 보완했다. `ChatActionHandler`의 Activity/Fragment 확장 함수는 `ensureV2Access`를 주입하며, `Blocked`와 `Ignored`에서는 Activity를 시작하지 않는다. - 2026-07-16: `HomeMainFragment.openFollowingChat`의 화면-local login guard는 제거하고 `handleChatAction(ChatActionCommand.AiRoom/DmRoom)` 호출만 남겨 Chat Action이 동일 정책을 소유하게 했다. - [x] **P2: Community ActivityResult 계약 문서와 테스트 정정** - 수정 파일: - `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityChangeTest.kt` - `docs/20260714_V2_공통_접근_가드와_도메인_액션/plan-task.md` - 검증 기록: - 2026-07-16: 기존 write/modify/detail Activity result는 post ID extra를 반환하지 않으므로 `resolveCommunityActivityResult(source, resultCode)`는 `RESULT_OK`와 source만 명시적 변경으로 변환하는 계약으로 유지했다. - 2026-07-16: `CommunityChangeTest`의 post ID 검증은 동일 객체끼리 비교하지 않고 `Deleted.postId`, `PinChanged.postId` payload를 직접 확인하도록 정정했다. post ID 유효성은 상세 진입 `CommunityAction`과 delete/pin 성공 경로에서 다룬다. - [x] **P3: 미사용 AI Character Intent helper 제거** - 수정 파일: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 검증 기록: - 2026-07-16: 운영 호출부 없이 항상 `null`을 반환하던 `HomeRecommendationAiCharacterRoute.toHomeRecommendationAiCharacterIntent()`와 해당 테스트/import를 제거했다. AI Character route는 `creatorId` 보존과 invalid ID 무시만 검증하고, 실제 이동은 `HomeMainFragment`의 `handleCreatorAction(CreatorActionCommand.Profile(...))`가 담당한다. - [x] **리뷰 보완 검증** - 검증 기록: - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.ChatActionTest" --rerun-tasks`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.CommunityChangeTest" --rerun-tasks`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --rerun-tasks`와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLoginGuardSourceTest" --rerun-tasks`가 각각 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: Chat 관련 회귀 묶음 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --rerun-tasks`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-07-16: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`는 통과했다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만, main/test ktlint 리포트에서 이번 리뷰 보완 변경 파일명 검색 결과는 0건이다. Kotlin LSP는 환경에 `kotlin-ls`가 없어 Gradle compile/test로 대체 검증했다. ## Verification Log - 2026-07-14: 사용자 요청에 따라 이번 단계에서는 PRD와 구현 계획 문서만 작성했다. 운영 코드, 테스트 코드, DI는 아직 변경하지 않았고 Gradle 검증은 실행하지 않았다. - 2026-07-14: 문서 범위는 Access부터 도메인별로 순차 전환하는 하나의 연속 작업으로 정의했다. 화면별 최초 집계 API는 유지하고 반복 action만 단일 소유 진입점으로 통합한다. - 2026-07-14: 문서 작성 후 `./gradlew tasks --all`을 실행해 계획에 사용하는 `:app:testDebugUnitTest`, `:app:compileDebugKotlin`, `:app:ktlintCheck` 작업이 존재함을 확인했으며 명령은 `BUILD SUCCESSFUL`로 종료되었다. `git diff --check`도 whitespace 오류 없이 통과했다. - 2026-07-14: Community 업로드 후 Home/Community 탭 갱신 설계를 구체화했다. 초기 구현은 전역 observer 대신 `CommunityChange` 결과와 Creator Channel의 단일 `handleCommunityChange` composition handler를 사용하며, 레거시 `ActivityResult`는 `v2` adapter에서 변경 결과로 변환한다. - 2026-07-14: 사용자 확인에 따라 Activity result가 필요한 모든 도메인 Action에 공통 결과 전달 원칙을 확장했다. 기본 방식은 `ActivityResult -> 명시적 도메인 결과 -> feature 단일 handler`이며, Action/Repository 직접 결과도 동일 handler로 합류한다. `SharedFlow`/observer는 Activity 밖의 독립 소비자와 lifecycle/replay 요구가 확인될 때만 도입한다. - 2026-07-14: Phase 0/1 리뷰 답변을 반영해 Legacy MyPage 호출부와 로그인 token 정책 확인, FanTalk source assertion 수동 삭제 사유를 기록했다. 문서 변경 후 `./gradlew tasks --all`은 `BUILD SUCCESSFUL`, `git diff --check HEAD`는 무출력으로 통과했다. - 2026-07-14: Phase 2 Content Action 전환 후 전체 `:app:testDebugUnitTest`, `:app:compileDebugKotlin`, `tasks --all`, `git diff --check HEAD`가 통과했다. 전체 ktlint는 기존 main source 550줄 기준선 위반으로 실패했지만 Phase 2 변경 Kotlin 파일과 test source의 신규 위반은 0건이다. 연결된 Android 기기가 없어 실기기 수동 탭 검증은 후속 확인 항목으로 남겼다. - 2026-07-15: 사용자 수동 테스트 결과 Phase 3 사전 준비 3개와 시나리오 22개를 `PASS`로 확인했다. 미체크 시나리오 `M3-03`, `M3-04`, `M3-06`, `M3-15`, `M3-24`, `M3-26`, `M3-29`, `M3-30`은 사용자 판단에 따라 테스트 불가능 또는 의미 없음으로 보아 `SKIPPED` 처리했으며, `FAILED`는 0개다. 모든 항목의 판정이 완료되어 Task 3.8과 Phase 3 수동 회귀 검증을 완료 처리했다. - 2026-07-15: Phase 4/5 Creator/Community/Chat Action 전환 후 action 테스트 묶음, Home/Content/Chat/Creator source 회귀 묶음, 넓은 관련 테스트 묶음, `:app:compileDebugKotlin`, `tasks --all`, `git diff --check HEAD`가 통과했다. `:app:ktlintTestSourceSetCheck --rerun-tasks`는 통과했고 test source 보고서는 0건이다. `:app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만 Phase 4/5 변경 Kotlin 파일의 신규 ktlint 위반은 리포트 검색 결과 0건이다. Kotlin LSP는 환경에 `kotlin-ls`가 없어 Gradle compile/test로 대체 검증했다. - 2026-07-15: Phase 6 의존 방향/결과 전달/미추출 도메인 재검사를 완료했다. 추가 코드 삭제나 신규 Action 추출은 필요하지 않았고, `MainV2Activity` system/deeplink route와 `MainV2Activity.showLoginActivity()` 호환 진입점, parent-child composition, FanTalk 단일 owner raw result 처리는 의도적으로 유지했다. Action 통합 테스트, `v2.*` 전체 단위 테스트, `:app:compileDebugKotlin`, `:app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`는 통과했다. `:app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만 현재 변경 Kotlin 파일의 신규 ktlint 위반은 0건이다. - 2026-07-16: 코드 리뷰 보완으로 Chat Action 로그인 접근 정책 소유, Community ActivityResult 계약 문서/테스트 정정, 미사용 AI Character Intent helper 제거를 완료했다. 관련 focused 테스트, Chat 회귀 묶음, `:app:compileDebugKotlin`, `:app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`는 통과했다. `:app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 위반으로 실패했지만 리뷰 보완 변경 Kotlin 파일의 신규 ktlint 위반은 0건이다. - 2026-07-16: 최종 소유 경계를 기준으로 PRD와 Phase 1~5 규범 문구를 동기화하고 별도 Phase 6을 추가했다. 기존 최종 점검 Phase 6은 과거 검증 기록을 보존한 채 Phase 7로 이동하고 Task 7.1~7.5를 모두 재개방했다. 이번 단계에서는 두 문서만 수정했으며 운영/테스트 코드는 변경하지 않았고, 코드 재점검·Community mutation wiring 테스트·집중/최종 검증은 Task 6.3~7.5에 미완료로 남겼다. - 2026-07-16: 두 문서에 대해 `git diff --check HEAD`가 무출력으로 통과했다. Phase 0~7 번호와 Task 6/7 체크 상태를 검색해 새 Phase 6의 문서 Task 6.1~6.2만 완료, Task 6.3~7.5는 미완료임을 확인했고, 정정 대상이던 Creator/Community/Chat 복수형 파일명과 이전 카탈로그 문구는 더 이상 남지 않음을 확인했다. 문서만 수정하는 단계이므로 Gradle 검증은 실행하지 않았다. - 2026-07-16: 직전 두 항목은 Phase 6/7 후속 작업을 시작하기 전 재개방 시점의 기록으로 유지한다. 이후 Task 6.3~7.5와 코드 리뷰 보완을 완료했으며, 최종 재검증에서 `./gradlew :app:testDebugUnitTest --rerun-tasks`, `./gradlew :app:assembleDebug`, `./gradlew :app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check cc8d061b`, `git diff --check HEAD`가 통과했다. `./gradlew :app:ktlintCheck --rerun-tasks`는 기존 main source 기준선 550건으로 실패했지만 `v2` 경로와 test/androidTest source의 신규 위반은 0건이며, 기능 코드에서 P0~P2 수준의 리뷰 발견 사항은 없었다. - 2026-07-16: Phase 8에서 `DeepLinkActivity`와 `MainV2Activity`의 DM/Content/Series/Creator/Community route를 기존 Action으로 연결하고 Live를 `LiveActionCoordinator`에 연결했다. Community는 별도 `postId`가 있으면 PostDetail Action을 우선하고, legacy `community/{creatorId}` 또는 `deep_link_sub5`만 있으면 Creator Action으로 fallback한다. 현재 DM은 `chat/{roomId}`만 Chat Action으로 처리하고 legacy `message`는 과거 호환 경로로 유지한다. - 2026-07-16: Phase 8 최종 검증에서 focused route/FCM/Live 테스트, 전체 `v2.*` 테스트, `:app:compileDebugKotlin`, `:app:ktlintTestSourceSetCheck --rerun-tasks`, `git diff --check HEAD`가 통과했다. 전체 main ktlint는 기존 기준선 550건으로 실패했지만 Phase 8 production 변경 파일의 신규 위반은 0건이고 코드 리뷰의 P0~P2 발견 사항은 없었다. 연결 기기가 검증 도중 해제되어 실제 푸시 터치 수동 시나리오는 잔여 확인 항목으로 남겼다. - 2026-07-16: Phase 9에서 `MainV2Activity`가 Live 푸시/딥링크 목적지 판단에 사용하는 `LiveViewModel.isLoading`과 `toastLiveData`를 관찰하도록 연결했다. 조회 중에는 기존 `LoadingDialog`에 `라이브를 불러오고 있습니다.`를 표시하고, 성공·실패 종료 시 해제하며 실패 메시지는 Toast로 표시한다. 다른 동기식 도메인 route에는 범용 로딩을 추가하지 않았다. - 2026-07-16: Phase 9 최종 검증에서 focused 12개 테스트, FCM/Live Action 회귀 묶음, `:app:compileDebugKotlin`, `:app:ktlintTestSourceSetCheck --rerun-tasks`, `tasks --all`, `git diff --check HEAD`가 통과했다. 전체 main ktlint는 기존 기준선 550건으로 실패했지만 `MainV2Activity.kt`의 신규 위반은 0건이다. 연결 기기가 없어 실제 Live 푸시 터치 수동 시나리오는 잔여 확인 항목으로 남겼다. - 2026-07-16: Phase 9 후속 결정으로 Live 전용 `라이브를 불러오고 있습니다.` 문구를 제거하고, cold start 푸시/딥링크의 기존 1초 대기와 Live 추가 room detail 조회가 하나의 문구 없는 `LoadingDialog` spinner를 공유하도록 변경했다. 일반 앱 실행과 지연 없는 non-Live `onNewIntent`에는 공통 대기를 추가하지 않았다. - 2026-07-16: Phase 9 후속 최종 검증에서 focused 12개 테스트, route/오디오 알림/FCM/Live Action 회귀 묶음, `:app:compileDebugKotlin`, `:app:ktlintTestSourceSetCheck --rerun-tasks`, `tasks --all`, `git diff --check HEAD`가 통과했다. 전체 main ktlint는 기존 기준선 550건으로 실패했지만 `MainV2Activity.kt`의 신규 위반은 0건이다. 연결 기기가 없어 실기기 수동 시나리오는 잔여 확인 항목으로 남겼다.