# PRD: V2 공통 접근 가드와 도메인 액션 ## 1. Overview `v2` 화면에 분산된 로그인·성인 콘텐츠 접근 검사와 반복 사용자 동작을 단일 소유 함수로 통합하고, 각 화면은 화면별 조회 결과를 표시한 뒤 공통 기능을 조합해 사용하도록 정리한다. --- ## 2. Problem 현재 `v2`는 동일한 접근 조건과 화면 이동 동작을 여러 화면에서 각각 구현하고 있다. - 로그인, 국내 사용자 본인인증, 성인 콘텐츠 표시 설정 검사가 `MainV2LoginGuard`, `CreatorChannelActivity`, `HomeOnAirLiveActivity`에 중복되어 있다. - 일부 화면은 `SharedPreferenceManager.token`을 직접 확인하고, 일부 화면은 별도 `ensure...` 함수를 사용해 동일 정책의 변경 지점이 분산되어 있다. - 오디오 콘텐츠, 시리즈, 크리에이터 채널, 커뮤니티 게시글, 채팅방 등의 진입에서 ID 검사, 접근 검사, `Intent` 생성이 화면마다 반복된다. - Home이 `CreatorChannelLiveCoordinator`를 직접 사용하는 등 공통 기능이 실제 소유 도메인이 아닌 최초 구현 화면 패키지에 위치한다. - 동일 기능의 새 진입 화면이 추가되면 기존 구현을 재사용하기보다 조건과 이동 코드를 다시 작성할 가능성이 있다. - 호출부가 접근 가드 적용을 기억해야 하므로 진입 경로에 따라 보호 정책이 누락될 수 있다. --- ## 3. Goals - 로그인 및 성인 콘텐츠 접근 판단을 `v2` 공통 Access 구현 한 곳에서 소유한다. - Activity와 Fragment가 동일한 Access 진입점을 사용한다. - 기존 로그인 화면, 본인인증, 성인 콘텐츠 설정 이동 동작과 back stack/extra 계약을 유지한다. - 둘 이상의 `v2` 화면에서 반복되는 사용자 동작은 해당 도메인의 단일 Action 진입점으로 통합한다. - 도메인 Action은 화면 전용 UI model 대신 안정적인 ID, 값 객체 또는 명시적인 command를 입력받는다. - 도메인 Action의 결과가 호출 화면에 따라 달리 표현되어야 하면 명시적인 result 계약으로 반환한다. - Home, Content, Creator Channel, On-air 등 feature는 화면별 최초 집계 API와 UI 구성을 유지하면서 공통 Action을 조합한다. - 새 화면은 기존 도메인 Action을 호출해 동일 정책과 동작을 재구현하지 않고 사용할 수 있어야 한다. - 푸시와 딥링크로 유입된 도메인 이동도 일반 화면과 같은 Action 및 Live 진입 정책을 사용한다. - 푸시와 딥링크로 Live 상태를 조회해 목적지를 판단하는 동안 사용자에게 진행 상태와 실패 결과를 표시한다. - 각 Phase는 기존 동작을 보존하고 독립적으로 테스트·컴파일 가능한 상태로 완료한다. - 하위 Activity가 반환하는 도메인 변경은 `ActivityResult`를 명시적 결과로 변환한 뒤 feature의 단일 handler에서 후처리한다. --- ## 4. Non-Goals - `v2` 전체 파일을 한 번에 이동하거나 일괄 패키지 재구성하지 않는다. - Gradle 멀티 모듈 전환은 포함하지 않는다. - 서버 API, 공개 DTO 스키마, 데이터베이스 계약은 변경하지 않는다. - Home 추천, Creator Channel 홈과 같은 화면별 집계 API를 여러 개의 도메인 API 호출로 강제 분해하지 않는다. - 한 번만 사용되고 별도 정책이 없는 화면 전용 동작에 형식적인 Action, UseCase, Repository 인터페이스를 추가하지 않는다. - UI 레이아웃, 문구, 디자인, 화면 전환 UX를 변경하지 않는다. - 본인인증 SDK, 로그인 API, 콘텐츠 구매, 라이브 결제 등 레거시 기능 자체를 수정하지 않는다. - 레거시 파일을 공통화를 위해 직접 수정하지 않는다. 단, V2 푸시/딥링크 진입점으로 계속 사용하는 `DeepLinkActivity`는 사용자 승인에 따라 기존 payload 계약을 Action에 연결하는 최소 변경을 허용한다. - FanTalk, Donation, Schedule 등 현재 재사용 수요가 확인되지 않은 기능을 추측으로 공통화하지 않는다. - Creator Channel 내부의 Community 변경 전파를 위해 전역 EventBus 또는 application singleton observer를 도입하지 않는다. - 직접적인 요청자와 결과 처리자가 명확한 흐름을 일괄적으로 `SharedFlow` 또는 observer 기반으로 변경하지 않는다. --- ## 5. Target Users - Home, Content, Creator Channel, On-air, Chat 등 `v2` 화면을 이용하는 사용자 - 동일 기능을 새로운 `v2` 화면에 연결해야 하는 Android 개발자 - 로그인·성인 접근·콘텐츠 진입 정책을 변경하거나 검증하는 유지보수 담당자 --- ## 6. User Stories - 비로그인 사용자는 어느 `v2` 화면에서 보호 기능을 선택하더라도 동일한 로그인 안내를 받고 싶다. - 성인 콘텐츠 접근 조건을 충족하지 않은 사용자는 진입 화면과 무관하게 동일한 인증 또는 설정 안내를 받고 싶다. - 로그인 및 접근 조건을 충족한 사용자는 기존과 동일하게 콘텐츠, 라이브, 커뮤니티, 채팅 기능으로 이동하고 싶다. - 개발자는 라이브 입장, 오디오 콘텐츠 열기, 커뮤니티 게시글 열기와 같은 동작을 한 번 구현한 뒤 여러 화면에서 호출하고 싶다. - 개발자는 공통 정책 변경 시 모든 Activity와 Fragment를 검색해 개별 조건문을 수정하지 않고 싶다. - 개발자는 화면별 집계 API와 UI 구성을 유지하면서 도메인별 공통 동작만 조합하고 싶다. --- ## 7. Core Features ### Feature A: V2 공통 Access 계약 로그인과 성인 콘텐츠 접근 조건을 화면과 분리된 공통 계약으로 정의한다. #### Requirements - Access 요구사항은 최소 `Login`, `AdultContent`를 명시적으로 표현한다. - `AdultContent`는 로그인 확인 후 국가/본인인증, 성인 콘텐츠 표시 설정 순서로 평가한다. - 로그인 여부는 기존 `MainV2Activity.isLoggedIn()`과 동일하게 `SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10` 기준을 사용한다. - `countryCode.ifBlank { "KR" } == "KR"`이면 국내 사용자로 판단한다. - 국내 사용자이고 `SharedPreferenceManager.isAuth == false`이면 본인인증 필요 결과를 반환한다. - `SharedPreferenceManager.isAdultContentVisible == false`이면 성인 콘텐츠 설정 필요 결과를 반환한다. - Access 판단 결과는 최소 `Allowed`, `LoginRequired`, `AdultVerificationRequired`, `AdultContentSettingRequired`를 구분한다. - 정책 판단 로직은 Activity, Fragment, Dialog, `Intent`에 의존하지 않는 단위 테스트 가능한 형태로 작성한다. - 세션 값 접근은 주입 가능한 snapshot/provider 경계를 두어 테스트에서 전역 상태에 직접 의존하지 않게 한다. #### Edge Cases - blank 국가 코드는 기존 정책과 동일하게 `KR`로 처리한다. - 국외 사용자는 국내 본인인증 조건을 건너뛰고 성인 콘텐츠 표시 설정을 확인한다. - 로그인만 필요한 동작은 성인 인증과 콘텐츠 표시 설정을 검사하지 않는다. - invalid ID 또는 지원하지 않는 route는 기존처럼 Access 실행 전에 종료할 수 있다. ### Feature B: V2 공통 Access 실행기 Access 판단 결과를 기존 로그인, 본인인증, 설정 이동 UX에 연결하는 재사용 진입점을 제공한다. #### Requirements - Activity와 Fragment가 동일한 `V2AccessGuard` 공개 진입점을 사용할 수 있어야 한다. - 허용된 경우에만 전달된 동작을 정확히 한 번 실행한다. - 미로그인인 경우 기존 `LoginActivity` 진입 계약과 호출 화면의 extra/back stack 동작을 유지한다. - 본인인증이 필요한 경우 기존 `V2ModalDialog`, `Auth.auth`, `MyPageViewModel.authVerify` 흐름을 재사용한다. - 성인 콘텐츠 표시 설정이 필요한 경우 `ContentSettingsActivity`에 `Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE = true`를 전달한다. - `MainV2LoginGuard`, `CreatorChannelActivity.ensureLoginAndAdultAuth`, `HomeOnAirLiveActivity.ensureLoginAndAdultAuth`의 중복 판단 구현을 제거한다. - `v2` UI의 직접 로그인 검사 중 화면 진입 자체를 막는 검사와 개별 행동을 막는 검사를 구분해 기존 종료 여부를 유지한다. #### Edge Cases - 인증 callback이 Activity 종료 이후 전달되는 경우 안전하게 UI 후처리를 생략한다. - 로그인 화면 이동 시 호출 Activity가 보존하던 기존 `intent.extras` 전달 계약을 유지한다. - 화면 진입 guard와 클릭 행동 guard가 같은 Access 정책을 사용하더라도 화면 종료 여부는 호출 feature의 기존 UX를 유지한다. ### Feature C: 도메인 Action 카탈로그와 단일 소유권 반복 동작의 호출부, 현재 차이, 소유 도메인, 입력·출력 계약을 구현 전에 확정한다. #### Requirements - 각 Action 후보는 최소 두 개의 실제 호출부가 있거나 여러 화면에서 반드시 동일해야 하는 정책을 포함해야 한다. - Action별로 소유 도메인, 호출 화면, 입력, 결과, UI side effect, 레거시 의존성을 기록한다. - 화면 전용 집계 query와 도메인 공통 action을 구분한다. - 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 | 소유자 | 단일 동작 후보 | 주요 호출 화면 | 공통 계약 방향 | |---|---|---|---| | Access | 로그인/성인 콘텐츠 접근 확인 | Home, Content, Creator Channel, On-air, Chat | `AccessRequirement` -> `AccessDecision` | | 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` -> 진입/차단/무시 결과 | | Community | 게시글 작성/수정/삭제/고정 변경 결과 | Creator Channel Home/Community projection | mutation -> `CommunityChange` | | Chat | DM/채팅방 진입 | Home, Chat, Creator Channel | room/creator 식별자 -> 진입/차단/무시 결과 | #### Result Propagation Policy - 하위 Activity의 완료 결과로 도메인 변경을 전달하는 기존 흐름은 `ActivityResultLauncher` 생명주기 계약을 유지한다. - raw `RESULT_OK`, nullable extra, 화면별 boolean은 `v2` adapter에서 명시적인 도메인 Action 결과 또는 change 타입으로 변환한다. - feature는 변환된 결과를 `handleContentChange`, `handleLiveChange`, `handleCommunityChange`, `handleChatChange`와 같은 의미 있는 단일 handler에 전달한다. - 단일 handler는 현재 feature가 조합하는 projection 갱신, Activity result 재전달, navigation, 화면별 후처리를 결정한다. - 순수 도메인 정책과 결과 타입은 `refreshHome`, `refreshTab`, Fragment 참조처럼 호출 화면의 구성을 나타내는 callback을 입력으로 받지 않는다. Android Dialog/Activity를 직접 조립하는 presentation adapter는 레거시 UI side effect를 연결하기 위해 호출 화면 후처리 callback을 주입받을 수 있으나, 해당 callback은 도메인 정책과 결과 타입으로 전파하지 않는다. - Repository 또는 Action이 현재 화면 안에서 직접 반환하는 비동기 결과는 불필요하게 `ActivityResult`로 변환하지 않고 같은 단일 handler에 전달한다. - 발행자와 처리자가 직접 연결되고 갱신 대상이 한 feature 내부에 있으면 명시적인 결과 전달을 기본값으로 사용한다. - Activity 밖의 서로 독립적인 소비자가 여러 개 생기거나 백그라운드 변경을 함께 관찰해야 할 때만 Activity 범위 `SharedFlow`, application event 또는 Repository Flow를 검토한다. - 전역 EventBus/singleton observer는 이벤트 범위, creator/content 식별자, replay, 중복 처리, lifecycle 요구사항이 먼저 확정되지 않으면 도입하지 않는다. ### Feature D: Content 공통 Action 오디오 콘텐츠와 시리즈 상세 진입의 공통 사전 조건과 navigation 계약을 Content가 한 번만 구현한다. #### Requirements - 오디오 콘텐츠 ID와 시리즈 ID 유효성 검사를 한 곳에서 소유한다. - 성인 여부를 호출부에서 알 수 있는 경우 공통 Access를 적용한다. - 성인 여부를 알 수 없는 기존 경로는 임의 판정하지 않고 기존 정책 또는 상세 화면 정책을 유지한다. - 기존 `Constants.EXTRA_AUDIO_CONTENT_ID`, `Constants.EXTRA_SERIES_ID` 계약을 유지한다. - Home, Content, Content Overview, Creator Channel의 해당 진입점은 Content Action을 사용한다. - Action 적용 후 대상 UI에서 `AudioContentDetailActivity`, `SeriesDetailActivity` Intent를 직접 구성하지 않는다. #### Edge Cases - ID가 0 이하이면 기존처럼 이동하지 않는다. - 접근 조건이 충족되지 않으면 상세 Activity를 시작하지 않는다. - 화면별 분석 source 또는 후처리가 존재하면 command metadata 또는 호출 결과 처리로 보존하고 도메인 규칙과 혼합하지 않는다. ### Feature E: Live 공통 Action 현재 Creator Channel과 Home에서 공유하는 Coordinator 및 On-air 중복 흐름을 Live 소유의 단일 진입점으로 정리한다. #### Requirements - `CreatorChannelLiveCoordinator`의 실제 소유권을 Live로 이동하거나 Live 공개 진입점 뒤의 호환 adapter로 전환한다. - 라이브 상세 조회, 입장 가능 여부, 무료/결제, 비밀번호, 상세 표시 분기를 한 번만 구현한다. - 기존 오디오 서비스 중단, 라이브 입장 API, 예약/결제/비밀번호 Dialog, `LiveRoomActivity` 이동 동작을 보존한다. - Home, Creator Channel, On-air가 동일한 Live 진입점을 사용한다. - Home 새로고침과 같은 호출 화면 전용 후처리는 Live 규칙과 분리된 callback/result 처리로 유지한다. `LiveActionCoordinator`는 레거시 Dialog/Activity를 조립하는 presentation adapter이므로 `refreshHome` callback을 주입받을 수 있지만, `LiveEntryPolicy`, `LiveEntryDecision`, `LiveCreationResult`에는 호출 화면 callback을 포함하지 않는다. - 라이브 생성처럼 하위 Activity가 결과를 반환하는 흐름은 raw `ActivityResult`를 명시적 Live 결과로 변환하고 Creator Channel의 단일 Live result handler에서 입장 또는 안내 후처리를 결정한다. - 라이브 상세의 channel 정보가 없어 unavailable로 분기되는 경우 성인 접근 확인과 오디오 중단보다 먼저 처리한다. - unavailable callback이 있으면 호출 화면의 후처리를 실행하고, callback이 없으면 호출자가 전달한 성인 여부와 상세 응답의 성인 여부를 OR로 합성한 동일 조건으로 접근을 확인한 뒤 상세 화면을 표시한다. - 라이브 생성 `RESULT_OK`는 즉시 입장할 수 없는 결과도 Home을 새로고침하며, channel 정보가 있지만 room ID가 유효하지 않으면 `RefreshOnly`로 변환한다. 실패 또는 취소만 `Ignored`로 변환한다. - 레거시 `LiveViewModel`, Dialog, Activity는 수정하지 않고 adapter로 사용한다. #### Edge Cases - 방 관리자 본인 입장, 무료/결제 완료, 비밀번호 방, 유료 방 조건의 기존 분기 순서를 유지한다. - 라이브 상세에 channel 정보가 없는 경우 기존처럼 상세 화면 흐름을 사용한다. - 호출자가 전달한 성인 여부와 재조회한 상세 응답의 성인 여부가 다르면 하나라도 `true`인 경우 성인 접근을 요구한다. - 입장 성공 이후 호출 화면 새로고침 여부가 달라도 입장 정책은 중복 구현하지 않는다. ### Feature F: Creator, Community, Chat 공통 Action 반복 navigation과 접근 조건을 각 소유 도메인의 단일 진입점으로 통합한다. #### Requirements - 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/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 공개 입력으로 사용하지 않는다. - 실제 중복이 확인되지 않은 follow, 알림, FanTalk, Donation mutation은 이번 Action에 추측으로 포함하지 않는다. #### Edge Cases - creator, post, room ID가 유효하지 않으면 기존처럼 이동하지 않는다. - Activity result가 필요한 호출부는 일반 `startActivity`로 단순화하지 않는다. - 채팅방 생성 API가 필요한 경로와 기존 room ID로 바로 진입하는 경로를 혼합하지 않는다. - 아직 생성되지 않은 Creator Channel Fragment는 자체 initial load로 최신 데이터를 조회하며, 현재 Activity 내부 두 projection 갱신을 위해 이벤트 replay/state 동기화 계층을 추가하지 않는다. ### Feature G: 의존 방향과 호환성 정리 공통 Action 도입 후 feature 간 직접 의존과 이전 중복 진입점을 정리한다. #### Requirements - 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`는 실제 외부 의존성, 공유 생명주기 또는 구현 교체가 필요한 계약만 조립한다. 상태 없는 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/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`, 필요한 경우 이를 조립하는 `AppDI.kt`, 본 작업 문서로 제한한다. - 레거시 파일은 직접 수정하지 않고 기존 기능을 호출하는 `v2` wrapper/adapter를 작성한다. 이번 후속 범위에서는 V2 진입점으로 유지할 `DeepLinkActivity`만 명시적 예외다. - API -> Repository -> ViewModel -> Activity/Fragment의 기존 흐름을 임의로 깨지 않는다. - 신규 테스트는 Access 판단, Action 입력·출력, route/정책 같은 순수 로직을 우선 검증한다. - 소스 문자열 테스트만으로 정책을 검증하지 않고, 가능한 범위에서 실제 입력·출력 단위 테스트를 추가한다. - Community 변경처럼 호출 화면이 갱신 대상을 결정하는 경우 도메인 결과와 feature invalidation handler를 분리하고, observer는 Activity 밖의 다수 소비자에게 전파할 요구가 생길 때만 도입한다. - 각 도메인 Phase에서 Activity result, callback, refresh 후처리를 조사하고 단일 handler가 필요한 흐름만 최소 범위로 통합한다. - 기존 source test가 이전 함수명을 고정한 경우 새 공개 계약을 검증하도록 최소 수정한다. - 각 Phase는 관련 단위 테스트, `:app:compileDebugKotlin`, `git diff --check`를 통과해야 한다. `:app:ktlintCheck`는 전체 기준선을 재실행하고, 저장소 기존 위반으로 실패하는 경우 Phase 변경 파일에 신규 ktlint 위반이 없음을 완료 gate로 삼아 실패 위치를 `plan-task.md`에 기록한다. - 테스트 클래스가 추가되면 `docs/agent-guides/build-test-style.md`에 단일 실행 예시를 추가한다. - 작업 범위가 변경되면 구현 전에 `plan-task.md`를 먼저 갱신한다. --- ## 10. Metrics - 로그인과 성인 접근 정책을 독립적으로 판단하는 구현이 `v2` 내 한 곳에만 존재한다. - `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`에 누적 기록된다. --- ## 11. Open Questions - 없음. 전체 작업은 한 번에 패키지를 재구성하지 않고 Access부터 도메인별로 순차 진행한다. - 각 도메인 Phase에서 조사 결과 실제 중복이 없거나 화면별 정책이 다른 동작은 공통 Action으로 만들지 않고 계획 문서에 근거를 기록한다.