refactor(v2): 도메인 액션 경로를 통합한다

This commit is contained in:
2026-07-16 16:57:39 +09:00
parent 4373452478
commit de90f771c1
34 changed files with 1780 additions and 264 deletions

View File

@@ -1,16 +1,26 @@
# V2 공통 접근 가드와 도메인 액션 Plan/Task
## Assumptions
- 작업 범위는 `kr.co.vividnext.sodalive.v2` 하위 신규/기존 코드 이를 조립하는 `AppDI.kt`로 제한한다.
- 작업 범위는 `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` 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` 로그인/성인 접근 판단의 단일 소유자가 존재한다.
@@ -20,7 +30,9 @@
- 화면별 최초 query와 UI composition은 유지된다.
- Community mutation은 명시적 변경 결과를 반환하고 Creator Channel의 단일 composition handler가 Home/Community projection 갱신을 결정한다.
- Activity result를 사용하는 도메인 흐름은 raw result를 화면마다 해석하지 않고 명시적 결과와 단일 handler를 사용한다.
- feature 간 직접 Activity/Coordinator 의존은 공통 Action 또는 navigation 계약으로 대체된다.
- 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 카탈로그 확정
@@ -750,29 +762,47 @@
- 2026-07-15: 사전 준비 3개와 시나리오 22개는 `PASS`, 시나리오 8개는 `SKIPPED`, `FAILED`는 0개로 판정되어 Task 3.8을 완료 처리했다.
### Phase 4: Creator와 Community 공통 Action
- [ ] **Task 4.1: Creator Channel 진입 호출부와 계약 확정**
- [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 진입 호출부를 확정했다.
- [ ] **Task 4.2: Creator Action 구현과 호출부 전환**
- [x] **Task 4.2: Creator Action 구현과 호출부 전환**
- 생성 예정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/action/CreatorActions.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/action/CreatorActionsTest.kt`
- 검증 기준: 유효 ID와 Access를 한 곳에서 처리하고 feature UI model을 입력받지 않다.
- `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`로 통과했다.
- [ ] **Task 4.3: Community 게시글 진입 계약 구현과 호출부 전환**
- [x] **Task 4.3: Community 게시글 진입 계약 구현과 호출부 전환**
- 생성 예정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/community/action/CommunityActions.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/community/action/CommunityActionsTest.kt`
- `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을 넣지 않았다.
- [ ] **Task 4.4: Community 변경 결과와 레거시 ActivityResult adapter 구현**
- [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`
@@ -781,9 +811,13 @@
- 작성, 수정, 삭제, 고정 변경을 `Created`, `Updated`, `Deleted`, `PinChanged`로 표현한다.
- 레거시 작성/수정 Activity의 `RESULT_OK`를 명시적인 Community 변경 결과로 변환한다.
- 결과 계약에는 Home/Community 탭 refresh callback 또는 Fragment 참조를 포함하지 않는다.
- 검증 기준: result code, 유효/누락 post ID, 변경 종류가 화면 의존 없이 테스트된다.
- 검증 기준: 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`로 변환한다.
- [ ] **Task 4.5: Creator Channel Community 변경 composition handler 통합**
- [x] **Task 4.5: Creator Channel Community 변경 composition handler 통합**
- 수정 예정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
@@ -794,8 +828,12 @@
- 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는 추가하지 않았다.
- [ ] **Task 4.6: Creator/Community Phase 회귀 검증**
- [x] **Task 4.6: Creator/Community Phase 회귀 검증**
- 실행 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*"
@@ -804,9 +842,15 @@
./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
- [ ] **Task 5.1: Chat/DM 진입 유형과 결과 계약 확정**
- [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`
@@ -815,22 +859,39 @@
- 검증 기준:
- 서로 다른 동작을 하나의 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는 기존 계약대로 제외했다.
- [ ] **Task 5.2: Chat Action 계약과 단위 테스트 작성**
- [x] **Task 5.2: Chat Action 계약과 단위 테스트 작성**
- 생성 예정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/chat/action/ChatActions.kt`
- `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/ChatActionsTest.kt`
- 검증 기준: room/creator 식별자와 진입 유형이 명시적이고 Home/Creator UI model에 의존하지 않다.
- `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`를 소유한다.
- [ ] **Task 5.3: Home/Chat/Creator Channel 호출부 전환**
- [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`
- 검증 기준: 기존 login guard, room 생성 API, 성공 후 이동, owner/non-owner 분기 유지다.
- 검증 기준: 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를 중복 실행하지 않는 것이다.
- [ ] **Task 5.4: Chat Phase 회귀 검증**
- [x] **Task 5.4: Chat Phase 회귀 검증**
- 실행 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*"
@@ -839,34 +900,141 @@
./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: 의존 방향과 이전 구현 정리
- [ ] **Task 6.1: feature 간 직접 navigation/Coordinator 의존 재검사**
### 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'
```
- 검증 기준: composition에 필요한 부모-자식 Fragment 연결을 제외하고 도메인 Action으로 대체할 직접 의존이 남지 않는다.
- 검증 기준: 합의된 반복 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로 분류되어 추가 이관 대상은 없었다.
- [ ] **Task 6.2: 이전 helper와 중복 함수 제거**
- [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라 삭제하지 않았다.
- [ ] **Task 6.3: 미추출 도메인 재평가**
- [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 또는 화면별 집계/표시 흐름으로 남기는 기존 판단을 유지한다.
- [ ] **Task 6.4: 결과 전달 방식과 observer 필요성 재검사**
- [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` 전파는 확인되지 않았다.
- [ ] **Task 6.5: 통합 회귀 검증**
- [x] **Task 7.5: 통합 회귀 검증**
- 실행 명령:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.*"
@@ -880,7 +1048,261 @@
- Creator Channel 및 Community 상세 진입
- Chat/DM 생성과 기존 room 진입
- 기존 Activity result와 새로고침 동작
- 기대 결과: `v2` 전체 관련 테스트와 컴파일, ktlint, whitespace 검증 통과.
- 기대 결과: `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 검증은 실행하지 않았다.
@@ -891,3 +1313,15 @@
- 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건이다. 연결 기기가 없어 실기기 수동 시나리오는 잔여 확인 항목으로 남겼다.

View File

@@ -26,6 +26,8 @@
- 도메인 Action의 결과가 호출 화면에 따라 달리 표현되어야 하면 명시적인 result 계약으로 반환한다.
- Home, Content, Creator Channel, On-air 등 feature는 화면별 최초 집계 API와 UI 구성을 유지하면서 공통 Action을 조합한다.
- 새 화면은 기존 도메인 Action을 호출해 동일 정책과 동작을 재구현하지 않고 사용할 수 있어야 한다.
- 푸시와 딥링크로 유입된 도메인 이동도 일반 화면과 같은 Action 및 Live 진입 정책을 사용한다.
- 푸시와 딥링크로 Live 상태를 조회해 목적지를 판단하는 동안 사용자에게 진행 상태와 실패 결과를 표시한다.
- 각 Phase는 기존 동작을 보존하고 독립적으로 테스트·컴파일 가능한 상태로 완료한다.
- 하위 Activity가 반환하는 도메인 변경은 `ActivityResult`를 명시적 결과로 변환한 뒤 feature의 단일 handler에서 후처리한다.
@@ -39,7 +41,7 @@
- 한 번만 사용되고 별도 정책이 없는 화면 전용 동작에 형식적인 Action, UseCase, Repository 인터페이스를 추가하지 않는다.
- UI 레이아웃, 문구, 디자인, 화면 전환 UX를 변경하지 않는다.
- 본인인증 SDK, 로그인 API, 콘텐츠 구매, 라이브 결제 등 레거시 기능 자체를 수정하지 않는다.
- 레거시 파일을 공통화를 위해 직접 수정하지 않는다. 필요한 기능은 `v2` wrapper/adapter에서 호출한다.
- 레거시 파일을 공통화를 위해 직접 수정하지 않는다. 단, V2 푸시/딥링크 진입점으로 계속 사용하는 `DeepLinkActivity`는 사용자 승인에 따라 기존 payload 계약을 Action에 연결하는 최소 변경을 허용한다.
- FanTalk, Donation, Schedule 등 현재 재사용 수요가 확인되지 않은 기능을 추측으로 공통화하지 않는다.
- Creator Channel 내부의 Community 변경 전파를 위해 전역 EventBus 또는 application singleton observer를 도입하지 않는다.
- 직접적인 요청자와 결과 처리자가 명확한 흐름을 일괄적으로 `SharedFlow` 또는 observer 기반으로 변경하지 않는다.
@@ -112,6 +114,16 @@ Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연
- Action의 공개 입력에 `HomeRecommendation...UiModel`, `CreatorChannel...Response` 같은 feature 전용 타입을 사용하지 않는다.
- 동일한 이름이더라도 정책이 다른 동작은 Access와 도메인 정책으로 나누고 하나의 범용 함수에 합치지 않는다.
#### Final Ownership Boundary
- Access는 로그인 여부, 본인인증, 성인 콘텐츠 설정 등 접근 허용 판단과 기존 로그인/인증/설정 UX 실행을 소유한다.
- 도메인 Action은 안정적인 ID/command 유효성 검사, 동작에 필요한 `AccessRequirement` 선택, Access 실행 요청과 `Ignored`/`Blocked`/도메인 결과 반환을 소유한다.
- Action Handler는 Access 실행기 주입과 레거시 `Intent`/Activity/Dialog/navigation adapter를 소유하고, 호출 feature의 UI model이나 화면별 refresh를 알지 않는다.
- feature는 UI model을 command로 변환하고, 화면별 최초 query/API, 단일 화면만 소유하는 생성·mutation API, `ActivityResult` 수신, refresh/callback/projection 조합을 소유한다.
- 보호된 도메인 Action을 호출하는 화면은 같은 Access guard를 바깥에서 중복 실행하지 않는다. 공통 Access 직접 호출은 화면 진입 자체 또는 화면 전용 동작의 접근 제한에 사용한다.
- 도메인과 관련된 모든 코드를 이관하지 않는다. 둘 이상의 호출부에서 반복되거나 모든 진입점에서 같아야 하는 유효성·접근 정책·결과 계약만 도메인 Action으로 이관한다.
- 도메인 Action의 범위는 페이지 이동만이 아니다. 반복되는 사전 조건과 결과 계약은 Action이, 실제 Android 화면 이동은 Handler가 소유한다.
- parent-child Fragment composition, same-feature child flow, 도메인 Action이 없는 system-only route, Home on-air 목록 화면 진입, 단일 owner 기능은 합의된 반복 정책이 없으면 feature에 유지한다.
#### Initial Action Catalog
| 소유자 | 단일 동작 후보 | 주요 호출 화면 | 공통 계약 방향 |
|---|---|---|---|
@@ -119,10 +131,10 @@ Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연
| Content | 오디오 콘텐츠 상세 진입 | Home, Content, Content Overview, Creator Channel | `contentId`, 접근 정보 -> 진입/차단 결과 |
| Content | 시리즈 상세 진입 | Content, Creator Channel | `seriesId`, 접근 정보 -> 진입/차단 결과 |
| Live | 라이브 상세/입장 | Home, Creator Channel, On-air | `liveId` -> 상세/입장/비밀번호/결제/차단 결과 |
| Creator | 크리에이터 채널 진입 | Home, Content, Main, AI 캐릭터 route | `creatorId` -> 진입/무시 결과 |
| Community | 커뮤니티 게시글 진입 | Home, Creator Channel | `postId` -> 진입/차단 결과 |
| Creator | 크리에이터 채널 진입 | Home, Content, Main, AI 캐릭터 route | `creatorId` -> 진입/차단/무시 결과 |
| Community | 커뮤니티 게시글 진입 | Home, Creator Channel | `postId` -> 진입/차단/무시 결과 |
| Community | 게시글 작성/수정/삭제/고정 변경 결과 | Creator Channel Home/Community projection | mutation -> `CommunityChange` |
| Chat | DM/채팅방 진입 | Home, Chat, Creator Channel | room/creator 식별자 -> 생성/진입/차단 결과 |
| Chat | DM/채팅방 진입 | Home, Chat, Creator Channel | room/creator 식별자 -> 진입/차단/무시 결과 |
#### Result Propagation Policy
- 하위 Activity의 완료 결과로 도메인 변경을 전달하는 기존 흐름은 `ActivityResultLauncher` 생명주기 계약을 유지한다.
@@ -176,14 +188,16 @@ Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연
반복 navigation과 접근 조건을 각 소유 도메인의 단일 진입점으로 통합한다.
#### Requirements
- Creator Action은 유효한 `creatorId` 기준으로 Creator Channel 진입 제공한다.
- Creator Action은 유효한 `creatorId``AccessRequirement.Login` 기준으로 Creator Channel 진입 또는 명시적 차단/무시 결과를 제공한다.
- Community Action은 로그인과 유효한 `postId` 확인 후 게시글 상세 진입을 제공한다.
- Community mutation 성공은 `CommunityChange.Created`, `Updated`, `Deleted`, `PinChanged`처럼 발생한 사실을 나타내는 명시적 결과로 표현한다.
- 레거시 Community 작성/수정 Activity의 `ActivityResult.RESULT_OK``v2` adapter에서 `CommunityChange`로 변환한다.
- Community Action과 mutation 결과는 `refreshHome`, `refreshCommunityTab`처럼 호출 화면 구조를 나타내는 callback을 입력으로 받지 않는다.
- Creator Channel은 `handleCommunityChange` 단일 composition 진입점에서 Community 변경 종류에 따른 Home/Community projection 갱신을 결정한다.
- Community 작성, 수정, 삭제, 고정 변경의 성공 경로는 직접 개별 refresh를 호출하지 않고 `handleCommunityChange`를 사용한다.
- Chat Action은 room 기반 진입과 creator 기반 DM 생성/진입의 차이를 명시적인 command로 구분한다.
- Chat Action은 room 기반 진입과 creator 기반 DM/owner 목록 진입의 차이를 명시적인 command로 구분하고, 유효 ID 확인 후 `AccessRequirement.Login`을 적용한다.
- Creator Channel AI Chat의 `AccessRequirement.AdultContent` 사전 조건과 `createChatRoom(characterId)` API는 feature가 유지한다. 생성 성공 후 받은 room ID의 로그인 정책과 화면 이동은 Chat Action에 위임한다.
- Creator, Community, Chat의 보호된 Action 호출부는 같은 로그인 guard를 화면에서 중복 실행하지 않는다.
- 기존 owner/non-owner, AI/DM 채팅 타입 분기와 Activity result 계약을 유지한다.
- Chat/DM 하위 Activity 결과에 따라 호출 화면 후처리가 필요한 경우 raw result를 명시적 Chat 결과로 변환하고 호출 feature의 단일 handler에서 처리한다.
- Home의 UI model과 Creator Channel의 Response를 Action 공개 입력으로 사용하지 않는다.
@@ -199,28 +213,63 @@ Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연
공통 Action 도입 후 feature 간 직접 의존과 이전 중복 진입점을 정리한다.
#### Requirements
- feature는 공통 Access 또는 소유 도메인 Action 호출할 수 있다.
- feature는 화면 진입 자체와 화면 전용 동작에는 공통 Access를 직접 호출할 수 있고, 합의된 보호 도메인 동작에는 소유 도메인 Action 호출다.
- 도메인 Action은 Home, Content Main, Creator Channel 같은 호출 feature를 import하지 않는다.
- data 구현은 Retrofit DTO와 레거시 API를 알고, 도메인 정책은 Retrofit 및 Android UI를 알지 않도록 유지한다.
- Android UI가 필요한 navigation/Dialog wrapper는 정책과 분리된 application/presentation action으로 둔다.
- 직접적인 Activity 결과 흐름은 domain/application event로 우회하지 않고 `ActivityResult -> 명시적 결과 -> feature handler` 의존 방향을 유지한다.
- `AppDI.kt`새 계약과 구현을 조립하되 레거시 등록을 불필요하게 변경하지 않는다.
- `AppDI.kt`실제 외부 의존성, 공유 생명주기 또는 구현 교체가 필요한 계약만 조립한다. 상태 없는 Action/Handler는 호출 경계에서 직접 조합할 수 있으며 DI 등록 자체를 완료 조건으로 삼지 않는다.
- 모든 호출부 전환이 끝난 이전 helper와 중복 함수만 제거한다.
- package 이동은 Action 전환 후 필요성이 확인된 파일에 한해 별도 Task로 수행한다.
- feature 간 직접 Activity/Coordinator 의존은 합의된 반복 navigation/정책 대상만 Action/Handler로 대체한다. parent-child composition, same-feature child flow, system/deeplink/notification route, 목록 화면 진입과 단일 owner 기능은 예외 근거를 기록하고 유지할 수 있다.
### Feature H: 푸시/딥링크의 도메인 Action 연결
푸시와 딥링크 payload 해석은 기존 진입점에 유지하고, 해석된 도메인 command의 실행은 이미 분리된 Action과 Live 진입 정책에 위임한다.
#### Requirements
- FCM 알림과 앱 내 알림 목록은 기존처럼 `DeepLinkActivity`로 진입하며, 별도의 범용 `PushRouteAction`이나 EventBus를 추가하지 않는다.
- `DeepLinkActivity`가 foreground에서 직접 처리하는 오디오, 시리즈, 크리에이터, 커뮤니티 게시글, DM 이동은 각각 Content, Creator, Community, Chat Action을 사용한다.
- `DeepLinkActivity`에서 Main으로 전달한 payload와 앱 cold start payload는 `MainV2Activity`가 같은 도메인 Action으로 처리한다.
- 채팅 deep-link 값과 유효한 room ID가 함께 있으면 DM Action을 먼저 실행하고, 그 외 유효한 room ID는 channel/content ID보다 먼저 Live 진입으로 처리한다.
- Live 진입은 `LiveActionCoordinator.enterLiveRoom(roomId)`를 사용한다. 조회 결과 현재 진행 중인 라이브이면 기존 무료/유료/비밀번호 정책에 따라 입장하고, 예약 또는 즉시 입장할 channel 정보가 없으면 레거시 Live Detail 화면을 표시한다.
- Community payload에 유효한 `postId`가 있으면 creator ID보다 우선하여 `CommunityActionCommand.PostDetail(postId)`를 실행한다.
- Community payload의 `postId`는 서버 실제 사용상 항상 전달되는 값으로 보되 선택적 계약은 유지한다. 유효한 `postId`가 없고 creator ID가 있으면 `CreatorActionCommand.Profile(creatorId)`로 Creator Channel을 표시한다.
- Community 딥링크의 `deep_link_sub5``${URISCHEME}://community/{id}` path ID는 기존 계약의 `creatorId`다. `postId`는 query/canonical extra로 별도 정규화하므로, `routeByDeepLinkValue("community")`는 post ID가 없는 레거시 Creator fallback으로 유지한다.
- 현재 DM 푸시 계약은 `${URISCHEME}://chat/{roomId}`이며 Chat Action의 `DmRoom`으로 처리한다. 기존 `message`/`message_id`는 DM room ID로 재해석하지 않고 과거 알림 수신 호환용 legacy Message route로만 유지한다.
- audio detail notification은 Content Action을 사용하고, 도메인 상세 이동이 아닌 audio player 화면 route는 기존 Access/system route를 유지한다.
- 현재 서버에서 더 이상 발행하지 않는 legacy message, audition, payment callback처럼 대응 도메인 Action이 없는 system-only route는 기존 직접 처리를 유지한다.
- `LiveRoomActivity`가 foreground일 때 broadcast로 payload를 전달하는 기존 특수 경로와 `Intent` flag/extra 계약을 유지한다.
- `MainV2Activity` cold start에서 `Constants.EXTRA_DATA` 또는 audio notification route가 있으면 기존 1초 지연 처리 동안 `LoadingDialog`를 문구 없이 표시한다.
- Live route는 공통 1초 대기 종료 후에도 room detail 조회가 진행 중이면 같은 문구 없는 로딩을 유지한다.
- Live 상태 조회가 완료되거나 실패하면 로딩을 해제하고, `LiveViewModel.toastLiveData`의 실패 메시지를 기존 Toast 방식으로 표시한다.
- Content, Creator, Community, Chat처럼 목적지 판단 자체에 별도 네트워크 조회가 없는 route는 공통 1초 대기가 끝나면 로딩을 해제하고 목적지로 이동한다.
- `onNewIntent`처럼 기존 1초 지연이 없는 route에 인위적인 공통 대기를 추가하지 않는다. 단, Live의 실제 room detail 조회 로딩은 동일하게 표시한다.
#### Edge Cases
- 0 이하이거나 파싱할 수 없는 ID는 기존처럼 이동하지 않는다.
- Live payload에 room ID와 channel ID가 모두 있어도 room ID를 우선해 Creator Channel로 잘못 이동하지 않는다.
- Community payload에 post ID와 creator ID가 모두 있어도 게시글 상세를 우선한다.
- Community post ID가 없거나 유효하지 않은 경우에만 creator ID fallback을 사용한다.
- `DeepLinkActivity`가 Main으로 Live payload를 전달하는 경우 cold start와 `onNewIntent` 모두 동일한 Live Action 진입점을 사용한다.
- 일반 앱 실행에는 공통 딥링크 로딩을 표시하지 않는다.
- 공통 1초 대기가 끝나는 시점에 Live room detail 조회가 진행 중이면 로딩을 중간에 해제하지 않는다.
- Live 상태 조회가 실패해 목적지로 이동하지 못해도 로딩이 남아 있지 않고 실패 안내가 표시된다.
---
## 8. UX / UI Expectations
- 로그인, 본인인증, 성인 콘텐츠 설정 안내의 표시 순서와 문구를 유지한다.
- 허용된 사용자의 콘텐츠, 라이브, 커뮤니티, 채팅 진입 결과는 기존과 같아야 한다.
- 푸시/딥링크에서도 일반 화면과 같은 도메인 접근 정책을 적용하며, 진행 중 Live와 예약 Live의 기존 입장/상세 UX를 유지한다.
- cold start 푸시/딥링크의 공통 1초 대기와 Live의 추가 상태 조회 중에는 도메인에 종속되지 않은 문구 없는 spinner를 표시한다.
- 기존 Dialog 크기, Activity flag, Intent extra, Activity result 처리와 화면 새로고침 동작을 유지한다.
- 리팩토링 자체로 신규 화면, 버튼, Toast, loading UI를 추가하지 않는다.
---
## 9. Technical Constraints
- 변경 범위는 `app/src/main/java/kr/co/vividnext/sodalive/v2`, 대응 `app/src/test/.../v2`, `AppDI.kt`, 본 작업 문서로 제한한다.
- 레거시 파일은 직접 수정하지 않고 기존 기능을 호출하는 `v2` wrapper/adapter를 작성한다.
- 변경 범위는 `app/src/main/java/kr/co/vividnext/sodalive/v2`, 대응 테스트, 사용자 승인을 받은 `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`, 필요한 경우 이를 조립하는 `AppDI.kt`, 본 작업 문서로 제한한다.
- 레거시 파일은 직접 수정하지 않고 기존 기능을 호출하는 `v2` wrapper/adapter를 작성한다. 이번 후속 범위에서는 V2 진입점으로 유지할 `DeepLinkActivity`만 명시적 예외다.
- API -> Repository -> ViewModel -> Activity/Fragment의 기존 흐름을 임의로 깨지 않는다.
- 신규 테스트는 Access 판단, Action 입력·출력, route/정책 같은 순수 로직을 우선 검증한다.
- 소스 문자열 테스트만으로 정책을 검증하지 않고, 가능한 범위에서 실제 입력·출력 단위 테스트를 추가한다.
@@ -238,9 +287,14 @@ Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연
- `CreatorChannelActivity``HomeOnAirLiveActivity`에 화면 전용 `ensureLoginAndAdultAuth` 구현이 남지 않는다.
- 대상 UI 호출부에서 `SharedPreferenceManager.token`으로 개별 행동 접근을 판단하지 않는다.
- 오디오, 시리즈, 라이브, 크리에이터, 커뮤니티, 채팅의 합의된 호출부가 각각 단일 Action 진입점을 사용한다.
- 보호된 Creator, Community, Chat Action 호출부에 동일한 화면-local 로그인 guard가 중복되지 않는다.
- Creator Channel의 Community 작성, 수정, 삭제, 고정 변경 성공 경로가 명시적 `CommunityChange`를 거쳐 하나의 composition handler에서 projection을 갱신한다.
- 공통 Action 공개 계약이 feature 전용 UI model 또는 DTO에 의존하지 않는다.
- 기존 Intent extra, Activity result, 로그인/인증/설정 UX를 검증하는 회귀 테스트가 통과한다.
- 푸시/딥링크의 Content, Creator, Community, Chat 이동은 해당 Action을 사용하고 Live 이동은 `LiveActionCoordinator`를 사용한다.
- room ID가 있는 Live payload는 channel ID보다 우선하고, Community post ID는 creator ID보다 우선한다.
- Main의 cold start 푸시/딥링크 공통 1초 대기 중 문구 없는 로딩이 표시되고, Live는 추가 상태 조회까지 끊김 없이 유지되며 성공·실패 종료 시 해제된다.
- Live 상태 조회 실패 메시지가 사용자에게 노출된다.
- 각 Phase 완료 시 중복 제거 전후 호출부 목록과 검증 결과가 `plan-task.md`에 누적 기록된다.
---

View File

@@ -96,6 +96,17 @@ Live Action Phase 테스트 예시:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinatorUiGateTest"
```
Creator/Community Action Phase 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.action.*"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.community.action.*"
```
Chat Action Phase 테스트 예시:
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.chat.action.*"
```
참고:
- 신규 Kotlin 테스트 메서드는 backtick 함수명을 사용하고, 테스트명은 한글 문장으로 작성한다.
- 기존 영어 테스트명을 수정하지 않는 최소 변경 상황을 제외하고, 새 테스트명에 영어 문장을 사용하지 않는다.