Files
sodalive-android/docs/20260914_크리에이터의_리스너_DM_시작/plan-task.md
T

730 lines
52 KiB
Markdown

# 크리에이터의 리스너 DM 시작 구현 계획
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 완료 |
| 작성일 | `2026-09-14` |
| 요구사항 기준 | `docs/20260914_크리에이터의_리스너_DM_시작/prd.md` |
| API 기준 | 서버 `CreateUserCreatorChatRoomRequest(recipientId?, creatorId?)` 계약 제공됨 |
| 현재 Phase | Phase 4 완료 |
| 현재 활성 Goal | 없음 |
## 목표
크리에이터가 메인 대화 탭에서 리스너를 선택하고 기존 DM 생성 흐름으로 먼저 대화를 시작할 수 있게 한다.
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | `2/2` | 없음 | 없음 |
| 2 | 완료 | `4/4` | 없음 | 없음 |
| 3 | 완료 | `1/1` | 없음 | 없음 |
| 4 | 완료 | `2/2` | 없음 | 없음 |
- 동시에 하나의 미완료 goal만 운용한다.
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다.
- 사용자가 명시적으로 요청하지 않으면 goal에 token budget을 설정하지 않는다.
## 범위
### 포함
- 메인 대화 탭 우측 하단 `+` 버튼 추가 계획.
- 크리에이터 역할 사용자에게만 신규 DM 시작 진입점 노출.
- Figma `2372:23496` 기준 수신자 선택 화면.
- 최초 진입 시 기존 팔로우 리스트 API 호출.
- `GET /member/search`와 `nickname`을 사용하는 2글자 이상 500ms debounce 검색.
- 검색 결과 empty 문구와 모든 신규 문구의 다국어 리소스 처리.
- Figma `2372:23521` 기준 확인 팝업, v2 공통 다이얼로그 재사용.
- 기존 크리에이터 채널 DM 생성 API 재사용, 신규 리스너 DM은 `recipientId`, 기존 크리에이터 채널 DM은 `creatorId` request 계약 유지.
- DM 방에서 뒤로 돌아와 대화 탭이 다시 보일 때 현재 filter의 대화방 목록 첫 페이지 갱신.
- Figma `2372:23496`, `2372:23506` 기준 수신자 선택/검색 결과 화면 상단 toolbar와 `검색결과 N` 라벨 정합성 수정.
### 제외
- 이 문서 작성 단계의 코드 구현.
- 신규 백엔드 endpoint 추가.
- DM 메시지 화면, 소켓, 음성 메시지 정책 변경.
- 팔로우/검색 결과의 프로필 상세, 차단, 팔로우 토글 정책 신규 정의.
- `app/src/androidTest`, 기기·에뮬레이터 조작, 스크린샷·시각 QA.
- DM 방 내부 메시지 읽음/전송/소켓 상태 변경.
## 기술적 제약
- 기술 스택: Android, Kotlin, RxJava, Retrofit, LiveData, Koin 기반 기존 구조.
- 신규 `Activity`, `Fragment`, `ViewModel` 및 연결 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 레거시 코드는 v2 wrapper/adapter를 우선 사용한다. 기능 구현에 불가피한 경우 최소 수정만 허용한다.
- `CreateDmChatRoomRequest` 계약 변경은 `recipientId` 신규 경로와 기존 `creatorId` 크리에이터 채널 DM 경로의 회귀를 반드시 포함한다.
- 제공되지 않은 endpoint, DTO, enum, 오류 status/key를 추정하지 않는다.
- 테스트 범위는 `app/src/test`의 로직/local unit test로 한정한다.
- UI 변경 자동 테스트는 adapter 분기, mapper, formatter, presentation model, 라우팅처럼 화면 표현을 결정하는 로직만 검증한다. 레이아웃 크기·간격·constraint·visibility 직접 검증은 하지 않는다.
- 구현 Task는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서를 따른다.
- v2 대화 목록 첫 페이지 갱신은 cursor 기반 API의 `cursor = null` 요청으로 검증한다. 별도 page counter를 추가하지 않는다.
## Task TDD 작성 규칙
- [ ] **RED:** 가장 작은 실패 test를 작성한다.
- [ ] **RED 확인:** focused test를 실행해 요구 동작 미구현 때문에 발생한 의도한 assertion 실패를 확인한다.
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [ ] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 focused test·직접 영향 회귀·lint 결과를 Progress에 기록한다.
읽기 전용 계약 확인 Task는 TDD 단계를 형식적으로 만들지 않는다. 대신 `TDD 예외 사유`와 `대체 검증 방법`을 적는다.
## Phase 1. 계약·파일 지도 확정
**Phase 결과:** 구현에 필요한 기존 파일, 신규 파일 후보, 서버 제공 계약 적용 위치, 역할 판정 기준을 확정한다.
**선행조건:** `prd.md`의 확정 요구사항과 Open Questions 확인.
**Phase 완료 조건:** `P1-T1`, `P1-GATE` 완료, 확인 결과를 Progress와 Decision Log에 누적.
### 구현 항목
#### Task 1.1 계약·파일 지도 확인
**Goal 실행 `P1-T1`:** 기존 코드와 서버 제공 계약을 대조해 구현 시작 조건을 확정한다.
- **시작 조건:** `prd.md`의 `DM-001`~`DM-019`, `EXT-001` 해결 상태 확인.
- **완료 증거:** 아래 대조표가 실제 파일 경로와 결정 상태로 갱신되고, 서버 계약 적용 위치가 Decision Log에 기록된다.
- **범위 밖:** production 코드 변경, 임의 debounce 상수 적용, 서버 계약과 다른 request contract 구현.
- **TDD 예외 사유:** 동작 변경 없는 읽기 전용 계약·파일 확인 Task다.
- **대체 검증 방법:** 기존 코드 심볼과 PRD 요구사항을 대조하고 누락·차단 항목을 문서화한다.
**Files:**
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt`
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt`
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt`
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt`
- Confirm: `app/src/main/java/kr/co/vividnext/sodalive/following/FollowingCreatorRepository.kt`
- Confirm: v2 공통 다이얼로그 실제 파일 경로
- Confirm: 홈 화면 `+` 버튼 실제 파일과 drawable/style 경로
- Confirm: string resource 지원 언어 파일 경로
- Planned Create: v2 수신자 선택 화면 파일
- Planned Test: `app/src/test` 하위 DM request, 검색 상태, 라우팅 test 파일
**Interfaces:**
- Consumes: 기존 팔로우 목록 API, `UserRepository.searchUser`, `DmChatRepository.createOrGetRoom`, v2 공통 다이얼로그.
- Produces: 구현 대상 파일 지도, 서버 계약 상태, 다국어 문구 표.
- [x] 기존 크리에이터 채널 DM 생성 호출부와 request DTO를 확인한다.
- [x] 서버가 제공한 `CreateUserCreatorChatRoomRequest(recipientId?, creatorId?)` 계약을 `EXT-001`에 기록한다.
- [x] 팔로우 리스트 페이지가 사용하는 API와 응답 ID 필드를 확인한다.
- [x] 검색 결과 `GetRoomDetailUser`의 사용자 ID, 닉네임, 프로필 이미지 필드를 확인한다.
- [x] 크리에이터 역할 판정 기준과 메인 대화 탭 진입점 노출 위치를 확인한다.
- [x] v2 공통 다이얼로그 파일과 재사용 가능한 문구 주입 방식을 확인한다.
- [x] 홈 화면 `+` 버튼 리소스와 위치 구현 패턴을 확인한다.
- [x] 확정된 `OQ-001`~`OQ-005` 결정이 실제 구현 파일 지도와 충돌하지 않는지 확인한다.
### 완료 조건
- [x] `P1-T1`의 체크박스와 대체 검증 기록이 완료됐다.
- [x] `EXT-001` 해결 상태와 호출 경로별 request field 사용 기준이 구현 Task의 시작 조건에 반영됐다.
- [x] 실제 파일 경로와 예정 파일 경로가 구분돼 있다.
### 검증 방법
#### Phase 1 Gate
**Goal 실행 `P1-GATE`:** Phase 2 구현을 시작해도 되는지 문서 기준으로 판정한다.
- **시작 조건:** `P1-T1` 완료.
- **완료 증거:** PRD 요구사항, Open Questions, 외부 의존, 파일 지도 대조표 통과.
- **범위 밖:** Gate 통과를 위한 요구사항 삭제 또는 임의 확정.
수동 문서 검증:
- [x] `DM-001`~`DM-019`가 최소 하나의 Phase 2 Task에 연결돼 있다.
- [x] `OQ-001`~`OQ-005`가 모두 확정 상태로 유지된다.
- [x] `EXT-001`이 해결됐고 `P2-T1`은 신규 `recipientId`와 기존 `creatorId` 경로를 모두 검증하도록 표시돼 있다.
- [x] 레거시 파일 변경 필요 여부가 확인돼 있다.
## Phase 2. 기능 구현 계획
**Phase 결과:** 크리에이터가 메인 대화 탭에서 리스너를 선택해 DM을 시작하는 흐름을 구현한다.
**선행조건:** `P1-GATE` 완료.
**Phase 완료 조건:** `P2-T1`~`P2-T3`, `P2-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 2.1 DM 요청 계약 적용
**Goal 실행 `P2-T1`:** DM 방 생성 요청 DTO가 `recipientId`와 `creatorId`를 모두 표현하도록 계약을 적용하고 호출 경로별로 하나의 ID만 보내게 한다.
- **시작 조건:** `P1-GATE` 완료.
- **완료 증거:** 신규 리스너 DM의 `recipientId` request serialization test, 기존 크리에이터 채널 DM의 `creatorId` request serialization 회귀 test, focused test 결과 기록.
- **범위 밖:** 신규 endpoint 추가, DM 메시지 화면 수정.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatApi.kt` 또는 실제 DTO 파일
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/data/DmChatRepository.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/dm/` 하위 request/repository test
**Interfaces:**
- Consumes: 서버가 제공한 `CreateUserCreatorChatRoomRequest(recipientId: Long? = null, creatorId: Long? = null)` 호환 contract.
- Produces: 신규 리스너 DM용 `CreateDmChatRoomRequest(recipientId = selectedUserId)`, 기존 크리에이터 채널 DM용 `CreateDmChatRoomRequest(creatorId = creatorId)` contract.
- [x] **RED:** 신규 리스너 DM 생성 request를 JSON으로 직렬화했을 때 `recipientId`가 있고 `creatorId`가 없음을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 신규 `recipientId` 경로가 없어 실패함을 확인한다.
- [x] **GREEN:** DTO에 nullable `recipientId`, `creatorId`를 모두 표현하고 신규 리스너 DM 요청 생성 경로만 `recipientId`를 채우도록 최소 구현한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **RED:** 기존 크리에이터 채널 DM 생성 request가 계속 `creatorId`를 보내고 `recipientId`를 보내지 않음을 검증하는 회귀 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 DTO 변경 전후의 기대 차이를 확인한다.
- [x] **GREEN:** 기존 `createOrGetRoom(token, creatorId)` 경로가 `creatorId` contract를 유지하게 한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** 신규/기존 DM 생성 경로의 중복만 정리하고 focused test와 lint 결과를 Progress에 기록한다.
#### Task 2.2 팔로우·검색 상태 로직
**Goal 실행 `P2-T2`:** 최초 팔로우 목록과 debounce 검색 상태를 제공한다.
- **시작 조건:** `P1-GATE` 완료.
- **완료 증거:** 팔로우 최초 조회, 0~1자 팔로우 리스트 복귀, 500ms debounce 후 최종 검색어 요청, empty 상태 test 통과.
- **범위 밖:** 화면 레이아웃 속성 직접 검증, 신규 검색 endpoint 추가.
**Files:**
- Planned Create: v2 수신자 선택 ViewModel/state/mapper 파일
- Planned Modify: 기존 repository wrapper 또는 신규 v2 repository adapter
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/` 하위 검색 상태 test
**Interfaces:**
- Consumes: 기존 팔로우 목록 API, `UserRepository.searchUser(nickname, token)`.
- Produces: 팔로우 목록 상태, 검색 결과 상태, 검색 empty 상태, 로딩/오류 상태.
- [x] **RED:** 최초 진입 시 팔로우 목록 API가 1회 호출되는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 호출이 없어 실패함을 확인한다.
- [x] **GREEN:** 최초 상태에서 기존 팔로우 목록 API를 호출하는 최소 로직을 작성한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **RED:** 검색어 0~1자에서는 `/member/search` 요청 0회와 팔로우 리스트 복귀, 2자 이상 연속 입력 중에는 500ms 전 요청 0회와 500ms 후 최종 검색어 1회 요청을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 debounce/길이 조건 미구현으로 실패함을 확인한다.
- [x] **GREEN:** 500ms debounce와 길이 조건으로 검색 로직을 작성한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** empty/error/loading 상태 mapper 중 이번 Task가 만든 중복만 정리하고 focused test 결과를 Progress에 기록한다.
#### Task 2.3 진입·화면·확인 팝업 연결
**Goal 실행 `P2-T3`:** 크리에이터 전용 `+` 버튼, 신규 화면, 확인 팝업, 보내기 액션을 연결한다.
- **시작 조건:** `P2-T1`, `P2-T2` 완료.
- **완료 증거:** 역할별 진입 판단, 문구 resource mapping, 취소/보내기 action, navigation test 통과.
- **범위 밖:** 새 다이얼로그 구현, 기존 DM 방 UI 변경.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
- Planned Create: v2 수신자 선택 화면 Activity/Fragment, adapter, layout, ViewModel 연결 파일
- Modify: `app/src/main/res/values/strings.xml` 및 기존 지원 언어 string resource 파일
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/` 하위 라우팅/문구/action test
**Interfaces:**
- Consumes: Phase 2.1 DM 생성 contract, Phase 2.2 수신자 선택 상태, v2 공통 다이얼로그.
- Produces: 크리에이터 전용 진입점, 수신자 선택 화면, 확인 팝업, DM 방 이동 action.
- [x] **RED:** 크리에이터 역할이면 `+` 버튼 진입 action이 활성이고 비크리에이터면 비활성임을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 역할별 분기가 없어 실패함을 확인한다.
- [x] **GREEN:** 메인 대화 탭에 홈과 유사한 `+` 진입점을 최소 변경으로 연결한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **RED:** 사용자 선택 후 팝업 문구가 `메시지 보내기`, `%s에게 메시지를 보낼까요?`, `취소`, `보내기` resource를 사용하고 취소 시 요청 0회임을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 팝업 action 미구현으로 실패함을 확인한다.
- [x] **GREEN:** v2 공통 다이얼로그를 재사용해 문구와 취소/보내기 action을 연결한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **RED:** `보내기` 시 선택한 사용자 ID가 `recipientId`로 전달되고 성공 시 기존 DM 방 이동 action이 실행됨을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 보내기 연결 미구현으로 실패함을 확인한다.
- [x] **GREEN:** 기존 DM 생성 repository/action을 재사용해 보내기와 이동을 연결한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** 신규 화면 파일의 이번 Task 중복만 정리하고 focused test·직접 영향 회귀·lint 결과를 Progress에 기록한다.
**P2-T3 API 연결 보완:** 실제 비동기 provider 연결에 필요한 요청 실패 상태와 이전 요청 취소를 같은 Task에서 최소 추가한다. 기존 P2-T2 완료 기록은 유지한다.
- [x] **RED:** 요청 실패 2건과 늦은 팔로워/검색 응답 2건의 회귀 test를 추가하고 실패를 확인한다.
- [x] **GREEN:** 요청 오류를 오류 상태로 전달하고 검색 모드 전환 시 이전 API 구독을 취소한다.
- [x] **검증:** 수신자 focused test와 기존 DM/ChatAction 회귀를 실행한다.
**P2-T3 검증 기록 — 2026-09-14**
- 사용자 승인에 따라 `DESIGN.md` 없이 기존 v2 FAB, toolbar, typography/spacing/color, 프로필 이미지 loader, `V2ModalDialog`를 재사용했다.
- 신규 화면은 기존 repository를 ViewModel factory에서 연결한다. 팔로워는 최초 1페이지 20명, 검색은 기존 정책대로 2글자 이상 500ms 후 실행하며 전체 검색 응답을 표시한다. 이번 최소 구현에는 팔로워 추가 페이지 로딩을 넣지 않았다.
- 취소 무부작용과 확인 후 생성/이동은 요청을 confirm callback 안에만 연결하고 cancel callback을 지정하지 않는 source wiring test로 검증했다. 기기 UI 자동화는 실행하지 않았다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerSourceTest"`: RED 5개 assertion 실패 확인. 역할/신규 화면/문구/생성 연결 미구현이 원인이다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerViewModelTest"`: 오류 상태 추가 전 `isError` 부재 컴파일 실패, 이후 늦은 응답 회귀 2개 assertion 실패를 각각 확인했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.*"`: GREEN 성공. 이후 늦은 응답 회귀도 아래 최종 통합 명령에서 통과했다.
- `./gradlew :app:ktlintCheck`: 신규 test 줄바꿈 오류로 최초 실패했고 해당 줄바꿈만 수정했다. 최종 통합 실행에서 성공했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" :app:assembleDebug :app:ktlintCheck`: 최종 `BUILD SUCCESSFUL`, exit 0. 수신자 13개를 포함한 18개 suite 181개 test 실패/오류 0, APK 및 layout/resource binding 컴파일 성공, ktlint 성공.
- 앞선 test+assemble 통합 실행은 도구 제한 120초로 중단되어 600초 제한으로 재실행했고 성공했다.
- 신규 Kotlin production 비공백 LOC: Activity 89, factory 38, adapter 29. 변경 ViewModel 84, 기존 ChatMainFragment 234. 기존 Fragment는 다음 확장 시 분리 검토가 필요한 200~250 경고 구간이며 이번에는 요청 진입점만 추가했다.
- LSP diagnostics 도구는 제공되지 않아 실행하지 못했으며 Kotlin 컴파일과 ktlint로 검증했다. 외부 서비스 호출, 기기/에뮬레이터, screenshot, androidTest는 요청대로 실행하지 않았다.
### 완료 조건
- [x] `P2-T1`, `P2-T2`, `P2-T3`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] 크리에이터 전용 진입, 팔로우 목록, 검색, empty, 팝업, DM 생성 흐름이 PRD와 일치한다.
- [x] 기존 크리에이터 채널 DM 진입 회귀가 통과했다.
### 검증 방법
#### Phase 2 Gate
**Goal 실행 `P2-GATE`:** 크리에이터의 리스너 DM 시작 흐름과 기존 DM 회귀를 최종 판정한다.
- **시작 조건:** Phase 2의 모든 활성 Task goal 완료.
- **완료 증거:** 아래 focused/영향 범위 검증 통과와 Progress 기록.
- **범위 밖:** 실패와 무관한 리팩터링, 테스트 삭제·완화.
```bash
./gradlew :app:test --tests "*Dm*"
./gradlew :app:test --tests "*Chat*"
./gradlew :app:ktlintCheck
```
**Expected:** 0 exit code. DM request contract, 검색 debounce, 역할별 진입, 팝업 action, 기존 크리에이터 채널 DM 회귀가 통과한다.
전체 회귀가 필요하다고 판단되면 아래 명령을 별도로 실행하고, 생략 시 생략 근거와 focused 검증 결과를 Progress에 기록한다.
```bash
./gradlew :app:test
```
## Phase 3. DM 방 복귀 목록 갱신
**Phase 결과:** 사용자가 DM 방에서 뒤로 돌아와 대화 탭을 다시 보면 현재 filter 기준 최신 대화방 목록 첫 페이지가 표시된다.
**선행조건:** Phase 2 완료, 사용자 확인으로 채팅방 뒤로가기 시 대화 탭 복귀 방식이 기능적으로 맞다고 확정.
**Phase 완료 조건:** `P3-T1`, `P3-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 3.1 DM 방 복귀 시 대화 목록 첫 페이지 갱신
**Goal 실행 `P3-T1`:** 대화 탭이 다시 resume될 때 최초 진입 중복 호출은 피하고, 이후 복귀에서는 기존 표시 목록을 비운 뒤 현재 filter 첫 페이지를 다시 요청한다.
- **시작 조건:** `P2-GATE` 완료와 PRD `DM-020` 확정.
- **완료 증거:** Fragment source wiring RED/GREEN, ViewModel 첫 페이지 reset 회귀 확인, focused test와 영향 범위 검증 결과 기록.
- **범위 밖:** `DmChatRoomActivity` 내부 메시지 정책 변경, 신규 API 추가, 기기·에뮬레이터 UI 조작.
**Files:**
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt`
- Confirm: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainViewModelTest.kt`
**Interfaces:**
- Consumes: `ChatMainViewModel.loadFirstPage(filter)`의 `cursor = null`, 목록 초기화, stale response 방지 contract.
- Produces: 복귀 시 `selectedFilter`를 유지한 첫 페이지 재조회와 기존 표시 목록 clear wiring.
- [x] **RED:** `ChatMainFragment` source가 `onResume()`에서 첫 resume을 skip하고 이후 `cancelChatPullRefresh()`, `bindChatRooms(emptyList(), showEmpty = false)`, `viewModel.loadFirstPage(selectedFilter)`를 호출함을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 `onResume()` 복귀 갱신 wiring 미구현으로 실패함을 확인한다.
- [x] **GREEN:** `ChatMainFragment`에 view 생성 시 초기화되는 first-resume guard와 subsequent resume refresh를 최소 구현한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 성공하고, 기존 `ChatMainViewModelTest`의 첫 페이지 reset 회귀가 통과함을 확인한다.
- [x] **REFACTOR:** 신규 abstraction 없이 중복만 점검하고 focused test·직접 영향 회귀·lint 결과를 Progress에 기록한다.
### 완료 조건
- [x] `P3-T1`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] 최초 대화 탭 진입은 기존 `onViewCreated()` 첫 페이지 요청만 사용하고 중복 resume 요청을 만들지 않는다.
- [x] DM 방 복귀 시 현재 filter 기준 첫 페이지 요청이 발생하고 기존 표시 목록은 먼저 비워진다.
### 검증 방법
#### Phase 3 Gate
**Goal 실행 `P3-GATE`:** DM 방 복귀 대화 목록 갱신과 기존 채팅 회귀를 최종 판정한다.
- **시작 조건:** `P3-T1` 완료.
- **완료 증거:** 아래 focused/영향 범위 검증 통과와 Progress 기록.
- **범위 밖:** 실패와 무관한 UI 정렬, DM 방 내부 로직 수정.
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" :app:assembleDebug :app:ktlintCheck
```
**Expected:** 0 exit code. 복귀 갱신 source wiring, 첫 페이지 `cursor = null` reset, 기존 pagination/filter/action 회귀가 통과한다.
## Phase 4. 수신자 검색 화면 Figma 정합성 수정
**Phase 결과:** 수신자 선택 화면과 검색 결과 화면이 Figma `2372:23496`, `2372:23506`의 상단 검색 toolbar, 섹션/검색결과 라벨, 리스트 행 구조와 일치한다.
**선행조건:** Phase 2 기능 구현 완료, 사용자 지적으로 Figma UI 불일치와 `검색결과 0` 미표시가 확인됨.
**Phase 완료 조건:** `P4-T1`, `P4-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 4.1 수신자 선택·검색 결과 UI 정합성 수정
**Goal 실행 `P4-T1`:** 기존 수신자 선택 기능을 유지하면서 Figma 검색 toolbar 구조와 검색 결과 count 라벨을 적용한다.
- **시작 조건:** PRD `DM-021` 확정과 Figma `2372:23496`, `2372:23506` metadata/screenshot 확인.
- **완료 증거:** layout/source RED/GREEN, 검색 결과 0 count state RED/GREEN, focused test와 영향 범위 검증 결과 기록.
- **범위 밖:** 신규 API, 검색 정책 변경, DM 방 내부 화면 수정, 기기·에뮬레이터 UI 조작.
**Files:**
- Modify: `app/src/main/res/layout/activity_dm_recipient_picker.xml`
- Modify: `app/src/main/res/layout/item_dm_recipient.xml`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerActivity.kt`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerViewModel.kt`
- Modify: `app/src/main/res/values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerSourceTest.kt`
- Test: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerViewModelTest.kt`
**Interfaces:**
- Consumes: 기존 follower/search/empty/error state와 `DmRecipientAdapter` 목록 표시.
- Produces: Figma형 search toolbar, follower/search count label, 검색 결과 0건 count 표시.
- [x] **RED:** layout/source test가 `detail_toolbar` 미사용, 뒤로가기와 검색 input의 같은 toolbar row, `tv_search_result_count`, row 66dp와 profile 42dp, count string resource를 검증하도록 작성한다.
- [x] **RED 확인:** focused source test를 실행해 현재 layout/Activity/string wiring 미구현으로 실패함을 확인한다.
- [x] **RED:** ViewModel test가 검색 결과 0건에서도 `searchResultCount = 0`을 state에 담는지 검증하도록 작성한다.
- [x] **RED 확인:** focused ViewModel test를 실행해 count field 부재 또는 값 미구현으로 실패함을 확인한다.
- [x] **GREEN:** XML, Activity, ViewModel, 문자열 리소스를 최소 수정해 RED를 통과시킨다.
- [x] **GREEN 확인:** 같은 focused tests를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** 신규 abstraction 없이 중복만 점검하고 focused test·직접 영향 회귀·build/ktlint 결과를 Progress에 기록한다.
#### Task 4.2 리뷰 후속 회귀 수정
**Goal 실행 `P4-R1`:** Oracle 리뷰에서 확인된 검색 empty 위치, 레이아웃 속성 직접 검증, 대화 탭 resume guard 재생성 회귀를 최소 수정한다.
- **시작 조건:** `P4-T1` 완료 후 read-only 리뷰 결과 확인.
- **완료 증거:** 회귀 RED/GREEN, 관련 focused test와 Phase 4 Gate 재실행 결과 기록.
- **범위 밖:** Figma 치수 자동 검증 추가, 기기·에뮬레이터 UI 조작, 화면 전체 재설계.
**Files:**
- Modify: `app/src/main/res/layout/activity_dm_recipient_picker.xml`
- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerSourceTest.kt`
- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragmentLayoutTest.kt`
- Modify: `docs/20260914_크리에이터의_리스너_DM_시작/prd.md`, `plan-task.md`
- [x] **RED:** 검색 empty가 리스트 콘텐츠 영역에서 전환되는 구조와 `ChatMainFragment` view 재생성 시 resume guard 초기화를 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused tests를 실행해 현재 XML sibling 구조와 guard 초기화 부재로 실패함을 확인한다.
- [x] **GREEN:** empty/list 콘텐츠 컨테이너와 `onViewCreated()` guard 초기화를 최소 수정하고, 레이아웃 치수 직접 assertion을 제거한다.
- [x] **GREEN 확인:** 같은 focused tests를 다시 실행해 성공을 확인한다.
- [x] **REFACTOR:** 문서 상태와 issue 해결 상태를 최신화하고 Phase 4 Gate를 재실행한다.
### 완료 조건
- [x] `P4-T1`의 체크박스와 완료 증거가 모두 충족됐다.
- [x] Figma `2372:23496` 기본 화면은 한 줄 검색 toolbar와 `팔로워` 섹션 및 66dp row 구조를 사용한다.
- [x] Figma `2372:23506` 검색 화면은 검색 결과가 0건이어도 `검색결과 0`을 표시한다.
- [x] 기존 debounce, empty state, 확인 팝업, DM 생성 흐름이 유지된다.
### 검증 방법
#### Phase 4 Gate
**Goal 실행 `P4-GATE`:** Figma 수신자 검색 UI 정합성과 기존 수신자 기능 회귀를 최종 판정한다.
- **시작 조건:** `P4-T1` 완료.
- **완료 증거:** 아래 focused/영향 범위 검증 통과와 Progress 기록.
- **범위 밖:** 실패와 무관한 전체 디자인 시스템 개편.
```bash
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerViewModelTest"
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.*" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" :app:assembleDebug :app:ktlintCheck
```
**Expected:** 0 exit code. Figma source contract, `검색결과 0` state, 기존 recipient flow와 DM request 회귀가 통과한다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 없음 | 아니요 | 서버 계약 확인 |
| 2 | `P1-GATE` | `P1-T1` | 아니요 | 누락 요구사항 문서 보정 |
| 3 | `P2-T1` | `P1-GATE` | 아니요 | DTO 적용 위치 확인 |
| 4 | `P2-T2` | `P1-GATE` | `P2-T1` 이후 일부 병행 가능 | 팔로우/검색 상태 로직 확인 |
| 5 | `P2-T3` | `P2-T1`, `P2-T2` | 아니요 | 선행 contract 또는 상태 로직 완료 |
| 6 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
| 7 | `P3-T1` | `P2-GATE` | 아니요 | 복귀 refresh contract 재확인 |
| 8 | `P3-GATE` | `P3-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
| 9 | `P4-T1` | `P2-GATE` | 아니요 | Figma UI contract 재확인 |
| 10 | `P4-GATE` | `P4-T1` | 아니요 | 실패 소유 Task의 회귀 수정 goal 생성 |
```text
P1-T1 -> P1-GATE -> P2-T1 -> P2-T2 -> P2-T3 -> P2-GATE -> P3-T1 -> P3-GATE -> P4-T1 -> P4-GATE
```
## 변경 금지 항목
- 코드 구현 전 PRD와 이 계획 문서를 먼저 갱신하지 않고 범위를 변경하지 않는다.
- 제공되지 않은 서버 오류 status/key를 추정하지 않는다.
- 레거시 코드는 v2 wrapper/adapter를 우선 사용하고, 기능 구현에 불가피한 경우 최소 수정만 허용한다.
- 신규 백엔드 endpoint나 신규 다이얼로그 구현을 만들지 않는다.
- 테스트를 삭제·skip·완화하거나 타입 오류를 우회해 Gate를 통과시키지 않는다.
- token, Authorization header, 민감 식별자를 log·Toast·fixture·문서에 기록하지 않는다.
## 의사결정 및 중단 규칙
- PRD와 서버 계약이 충돌하면 `EXT-001`을 갱신하고 구현을 중단한다.
- `OQ-001`~`OQ-005` 결정과 충돌하는 구현 변경이 필요하면 PRD와 이 계획을 먼저 갱신한다.
- 안전한 최소 기본값이 문서에 없으면 임의 구현하지 않는다.
- 같은 차단 사유가 3회 반복되고 독립 문서화도 불가능하면 goal을 `blocked`로 갱신한다.
- 완료 증거와 Progress 기록까지 충족한 뒤에만 goal을 완료로 표시한다.
## Progress
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
### 문서 작성 1차 실행 — 2026-09-14
- 상태: 완료
- 무엇을: PRD와 구현 계획 초안을 생성했다.
- 왜: 사용자 요청에 따라 코드 구현 전 요구사항과 계획 문서를 준비했다.
- 어떻게:
- Figma `2372:23496` metadata/screenshot 확인 — 검색바, 팔로우 섹션, 사용자 리스트 구조 확인.
- Figma `2372:23521` metadata/screenshot 확인 — 확인 팝업 제목, 본문, 취소/보내기 구조 확인.
- 기존 코드 심볼 확인 — `ChatMainFragment`, `DmChatApi`, `DmChatRepository`, `UserApi.searchUser`, `UserRepository.searchUser`, `FollowingCreatorRepository` 근거 확인.
- 사용자 인터뷰 — 진입점은 크리에이터에게만 노출한다고 확정.
- 남은 항목: 서버 계약은 이후 `EXT-001`에서 해결됨.
- 다음 행동: 구현 요청이 들어오면 `P1-T1`부터 시작한다.
### Open Questions 확정 1차 실행 — 2026-09-14
- 상태: 완료
- 무엇을: `OQ-001`~`OQ-005`를 사용자 인터뷰로 확정하고 PRD/계획 문서에 반영했다.
- 왜: 구현 전 검색 호출 기준, 화면 상태 전환, 선택 가능 사용자 범위, 문구, 레거시 수정 허용 범위를 고정하기 위해서다.
- 어떻게:
- `OQ-001` — debounce `500ms` 확정.
- `OQ-002` — 검색어 0~1자에서 팔로우 리스트 복귀 확정.
- `OQ-003` — API 검색 결과 모두 선택 가능 확정.
- `OQ-004` — Figma 한국어 문구 최종 확정.
- `OQ-005` — 레거시 수정이 필요하면 최소 수정 허용 확정.
- 남은 항목: 서버 계약은 이후 `EXT-001`에서 해결됨.
- 다음 행동: 구현 요청이 들어오면 `P1-T1`에서 파일 지도와 서버 계약을 확인한다.
### 서버 계약 반영 1차 실행 — 2026-09-14
- 상태: 완료
- 무엇을: 서버가 `recipientId`와 기존 `creatorId`를 모두 수용하는 호환 계약을 제공한 사실을 PRD/계획 문서에 반영했다.
- 왜: `EXT-001` 차단을 해소하고 신규 리스너 DM과 기존 크리에이터 채널 DM의 request field 기준을 분리하기 위해서다.
- 어떻게:
- 신규 크리에이터→리스너 DM은 `recipientId`만 사용하도록 계획했다.
- 기존 크리에이터 채널 DM은 `creatorId`를 유지하도록 계획했다.
- `P2-T1` 테스트 기준을 신규 `recipientId` serialization과 기존 `creatorId` 회귀 검증으로 갱신했다.
- 남은 항목: 없음. 구현 요청이 들어오면 `P1-T1`에서 실제 파일 지도 확인 후 시작한다.
### P1 계약·파일 지도 확인 — 2026-09-14
- 상태: 완료
- 무엇을: 구현 전 파일 지도와 재사용 패턴을 확정했다.
- 왜: Phase 2 구현이 기존 API, role, v2 modal, FAB, string resource 관례와 충돌하지 않게 하기 위해서다.
- 어떻게:
- `CreateDmChatRoomRequest`는 `DmChatModels.kt`, DM 생성 위임은 `DmChatRepository.kt`에 있음을 확인했다.
- 기존 크리에이터 채널 DM은 `createOrGetRoom(token, creatorId)`와 `DmChatRoomViewModel`의 `creatorId` 진입 경로를 사용함을 확인했다.
- 팔로우 리스트는 `ExplorerApi.getFollowerList()`의 `items.userId/profileImage/nickname` 응답을 사용할 수 있음을 확인했다.
- 검색은 기존 `UserRepository.searchUser(nickname, token)`와 `/member/search?nickname=` 경로를 사용함을 확인했다.
- 역할 판정은 `SharedPreferenceManager.role == MemberRole.CREATOR.name`, 공통 팝업은 `V2ModalDialog` 재사용으로 확정했다.
- 검증: 코드 심볼 확인과 백그라운드 탐색 결과 대조를 완료했다.
- 남은 항목: `P2-T2` 팔로우·검색 상태 로직 구현.
### P2-T1 DM 요청 계약 적용 — 2026-09-14
- 상태: 완료
- 무엇을: `CreateDmChatRoomRequest`가 `recipientId`와 `creatorId`를 모두 표현하고, 신규 리스너 DM 생성용 repository 경로를 추가했다.
- 왜: 신규 크리에이터→리스너 DM은 `recipientId`, 기존 크리에이터 채널 DM은 `creatorId`를 보내야 하기 때문이다.
- 어떻게:
- RED: `CreateDmChatRoomRequest(recipientId = 22L)` 테스트가 `recipientId` 파라미터 부재로 컴파일 실패함을 확인했다.
- GREEN: DTO를 nullable `recipientId`, nullable `creatorId`로 변경했다.
- RED: `createOrGetRoomByRecipientId` 호출 테스트가 메서드 부재로 컴파일 실패함을 확인했다.
- GREEN: `DmChatRepository.createOrGetRoomByRecipientId(token, recipientId)`를 추가했다.
- 회귀: 기존 `createOrGetRoom(token, creatorId)`와 `creatorId` only 직렬화를 함께 검증했다.
- 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest"` 성공.
- 남은 항목: lint/전체 회귀는 Phase 2 Gate에서 실행한다.
### P2-T2 팔로우·검색 상태 로직 — 2026-09-14
- 상태: 완료
- 무엇을: 수신자 선택용 `DmRecipientPickerViewModel`과 UI state를 추가했다.
- 왜: 메인 대화 탭의 신규 DM 시작 화면에서 최초 팔로우 목록, 0~1자 복귀, 2자 이상 500ms 검색, 검색 empty 상태를 제공하기 위해서다.
- 어떻게:
- RED: 신규 ViewModel/state 부재로 최초 팔로우 목록 로드 test 컴파일 실패를 확인했다.
- GREEN: `enter()`에서 팔로우 목록 provider를 1회 호출하고 `FOLLOWERS` state를 발행했다.
- RED: `onSearchQueryChanged`와 `debounceScheduler` 부재로 검색 test 컴파일 실패를 확인했다.
- GREEN: 0~1자 입력 시 pending 검색을 취소하고 팔로우 목록으로 복귀, 2자 이상은 500ms 후 마지막 검색어만 요청하도록 구현했다.
- RED/GREEN: 검색 결과가 비어 있으면 `isSearchEmpty = true`가 되도록 상태를 추가했다.
- 검증:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerViewModelTest"` 성공.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerViewModelTest"` 성공.
- 신규 production 파일 pure LOC: `72`, 신규 test 파일 pure LOC: `115`.
- 남은 항목: `P2-T3`에서 실제 화면, repository provider, 확인 팝업, DM 방 이동 action을 연결한다.
### P3-T1 DM 방 복귀 목록 갱신 — 2026-09-14
- 상태: 완료
- 무엇을: `ChatMainFragment`가 최초 resume은 건너뛰고, 이후 resume에서 기존 표시 목록을 비운 뒤 현재 filter의 첫 페이지를 다시 요청하도록 연결했다.
- 왜: 사용자가 확인한 뒤로가기 복귀 방식은 유지하되, 복귀 후 대화 탭 목록이 최신 방 목록으로 갱신되어야 하기 때문이다.
- 어떻게:
- RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` 실행 결과 `채팅방에서 복귀하면 현재 filter 첫 페이지로 목록을 갱신한다` 1개 test가 `onResume()` 복귀 갱신 wiring 부재로 실패했다.
- GREEN: `ChatMainFragment`에 `shouldRefreshChatRoomsOnResume` guard를 추가하고 subsequent `onResume()`에서 `cancelChatPullRefresh()`, `bindChatRooms(emptyList(), showEmpty = false)`, `viewModel.loadFirstPage(selectedFilter)`를 호출했다.
- GREEN 확인: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"` 성공.
- 영향 범위 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" :app:assembleDebug :app:ktlintCheck` 성공, `BUILD SUCCESSFUL`.
- `git diff --check`: 출력 없음.
- 검증 보완:
- `ChatMainViewModel.loadFirstPage()`는 기존 구현과 test에서 `cursor = null`, `currentItems = emptyList()`, 결과 replace를 이미 보장한다.
- `ChatMainFragment.kt` pure LOC는 245로 200~250 경고 구간이다. 이번 변경은 기존 파일에 최소 lifecycle hook만 추가했고, 다음 확장 시 분리 검토가 필요하다.
- LSP diagnostics 도구는 제공되지 않아 Kotlin 컴파일과 ktlint로 대체했다.
- 남은 항목: 없음.
### P4-T1 수신자 선택·검색 결과 UI 정합성 수정 — 2026-09-14
- 상태: 완료
- 무엇을: 수신자 선택 화면의 상단 구조를 Figma형 한 줄 검색 toolbar로 바꾸고 검색 모드에서 `검색결과 N` 라벨을 표시하도록 수정했다.
- 왜: 기존 화면이 별도 `detail_toolbar` 아래 검색창을 사용해 Figma `2372:23496`, `2372:23506`과 다르고, 검색 결과가 0건일 때 `검색결과 0`이 보이지 않았기 때문이다.
- 어떻게:
- RED: `DmRecipientPickerSourceTest`에 `detail_toolbar` 미사용, `btn_back`/검색 input 동일 toolbar row, `tv_search_result_count`, toolbar 54dp, 검색 input 42dp, section 40dp, row 66dp, profile 42dp, 다국어 count string resource 검증을 추가했다.
- RED: `DmRecipientPickerViewModelTest`에 검색 성공/0건 검색 결과에서 `searchResultCount` state를 검증하는 test를 추가했고, count field 부재로 실패를 확인했다.
- GREEN: `activity_dm_recipient_picker.xml`, `item_dm_recipient.xml`, `DmRecipientPickerActivity`, `DmRecipientPickerViewModel`, `strings.xml`/`values-en`/`values-ja`를 최소 수정해 검색 toolbar와 count 표시를 연결했다.
- GREEN 보완: source contract의 count string 참조와 기존 검색 성공 expected state의 `searchResultCount` 누락을 수정했다.
- 검증:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerViewModelTest"` 성공.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.*" --tests "kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRepositoryTest" :app:assembleDebug :app:ktlintCheck` 성공, `BUILD SUCCESSFUL`.
- `git diff --check` 출력 없음.
- 변경 Kotlin 파일 pure LOC: `DmRecipientPickerActivity.kt` 93, `DmRecipientPickerViewModel.kt` 86, `DmRecipientPickerSourceTest.kt` 103, `DmRecipientPickerViewModelTest.kt` 177.
- 검증 보완:
- 기기·에뮬레이터 조작, screenshot, visual QA, `androidTest`는 사용자 명시 요청이 없어 저장소 지침대로 실행하지 않았다.
- LSP diagnostics 도구는 제공되지 않아 Kotlin 컴파일과 ktlint로 대체했다.
- 남은 항목: 없음.
### P4-R1 리뷰 후속 회귀 수정 — 2026-09-14
- 상태: 완료
- 무엇을: read-only 리뷰에서 확인된 검색 empty 위치, 레이아웃 속성 직접 검증, 대화 탭 resume guard 재생성 회귀를 수정했다.
- 왜: `DM-010` 중앙 empty state와 저장소의 레이아웃 속성 직접 검증 금지, `P3-T1`의 첫 resume 중복 요청 방지 조건을 동시에 만족해야 하기 때문이다.
- 어떻게:
- RED: `DmRecipientPickerSourceTest`에 `recipient_content_container` 안에서 `rv_recipients`와 `tv_search_empty`가 전환되는 구조 검증을 추가했다.
- RED: `ChatMainFragmentLayoutTest`에 `onViewCreated()`에서 `shouldRefreshChatRoomsOnResume = false`가 초기화되는지 검증을 추가했고, 기존 구현에서 실패를 확인했다.
- GREEN: `activity_dm_recipient_picker.xml`에 콘텐츠 `FrameLayout`을 추가하고 empty/list를 같은 영역에서 전환하도록 바꿨다.
- GREEN: `DmRecipientPickerActivity`가 empty 상태에서 `rvRecipients`를 숨기도록 연결했다.
- GREEN: `ChatMainFragment.onViewCreated()`에서 resume guard를 초기화해 같은 Fragment 인스턴스의 view 재생성 직후 첫 resume 중복 갱신을 막았다.
- REFACTOR: `DmRecipientPickerSourceTest`의 toolbar/row/profile 치수 문자열 assertion을 제거하고 구조·상태 contract 검증만 남겼다.
- 검증:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.recipient.DmRecipientPickerSourceTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest"` 최초 RED 실패 1건 확인 후 GREEN 성공.
- 변경 Kotlin 파일 pure LOC: `ChatMainFragment.kt` 246, `DmRecipientPickerActivity.kt` 94, `DmRecipientPickerSourceTest.kt` 115, `ChatMainFragmentLayoutTest.kt` 230.
- 검증 보완:
- `ChatMainFragment.kt`와 `ChatMainFragmentLayoutTest.kt`는 200~250 경고 구간이다. 이번 변경은 guard 초기화와 회귀 assertion 최소 추가이며, 다음 확장 시 분리 검토가 필요하다.
- 기기·에뮬레이터 조작, screenshot, visual QA, `androidTest`는 사용자 명시 요청이 없어 저장소 지침대로 실행하지 않았다.
- 남은 항목: Phase 4 Gate 재실행 결과를 Verification Log에 누적한다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| `2026-09-14` | DEC-001 | 확정 | 이번 세션에서는 문서만 생성하고 코드 구현은 하지 않는다. | 사용자 요청 | 전체 |
| `2026-09-14` | DEC-002 | 확정 | 메인 대화 탭의 `+` 버튼과 신규 DM 시작 화면은 크리에이터에게만 노출한다. | 사용자 인터뷰 답변 | `P2-T3`, `prd.md` |
| `2026-09-14` | DEC-003 | 확정 | 검색 API는 기존 `GET /member/search`와 `nickname` 파라미터를 사용한다. | 사용자 요청과 기존 코드 확인 | `P2-T2`, `prd.md` |
| `2026-09-14` | DEC-004 | 정정 | 서버 계약 확인 전에는 `CreateDmChatRoomRequest.creatorId`를 `recipientId`로 변경하는 방향으로 계획했다. | 사용자 요청 | `P2-T1`, `EXT-001` |
| `2026-09-14` | DEC-005 | 확정 | 검색 debounce 대기 시간은 500ms로 한다. | 사용자 인터뷰 답변 | `P2-T2`, `prd.md` |
| `2026-09-14` | DEC-006 | 확정 | 검색어가 0~1자로 줄어들면 팔로우 리스트로 복귀한다. | 사용자 인터뷰 답변 | `P2-T2`, `prd.md` |
| `2026-09-14` | DEC-007 | 확정 | `/member/search` API 결과는 앱에서 추가 필터 없이 모두 선택 가능하게 둔다. | 사용자 인터뷰 답변 | `P2-T2`, `P2-T3`, `prd.md` |
| `2026-09-14` | DEC-008 | 확정 | Figma 한국어 문구를 최종 문구로 확정하고 기존 지원 언어 리소스에 번역을 추가한다. | 사용자 인터뷰 답변 | `P2-T3`, `prd.md` |
| `2026-09-14` | DEC-009 | 확정 | 레거시 수정이 필요하면 기능에 필요한 최소 수정은 허용한다. | 사용자 인터뷰 답변 | 전체 |
| `2026-09-14` | DEC-010 | 확정 | 서버가 `recipientId`와 기존 `creatorId`를 모두 수용하는 호환 계약을 제공했다. 신규 크리에이터→리스너 DM은 `recipientId`, 기존 크리에이터 채널 DM은 `creatorId`를 사용한다. | 서버 변경 공유 | `P2-T1`, `prd.md` |
| `2026-09-14` | DEC-011 | 확정 | DM 방에서 뒤로 돌아와 대화 탭이 다시 보이면 현재 filter의 첫 페이지를 재조회한다. page 0은 cursor 기반 API의 `cursor = null`로 처리한다. | 사용자 확인 및 추가 요청 | `P3-T1`, `prd.md` |
| `2026-09-14` | DEC-012 | 확정 | 수신자 선택 화면은 Figma `2372:23496`, 검색 결과 화면은 Figma `2372:23506`과 동일한 검색 toolbar와 `검색결과 N` 라벨을 사용한다. | 사용자 지적 및 Figma 재확인 | `P4-T1`, `prd.md` |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| ISSUE-001 | High | 해결 | 서버가 기존 DM 생성 endpoint에서 `recipientId`와 기존 `creatorId`를 모두 수용하는 호환 계약을 제공했다. | `P2-T1`, `P2-T3` | 신규 리스너 DM은 `recipientId`, 기존 크리에이터 채널 DM은 `creatorId`로 구현 |
| ISSUE-002 | Medium | 해결 | 기존 검색 ViewModel에는 2글자 조건은 있으나 debounce 구현 여부는 확인되지 않았다. `OQ-001`에서 신규 기능 debounce는 500ms로 확정했다. | `P2-T2` | 500ms debounce 기준으로 신규 검색 로직 구현 |
| ISSUE-003 | Medium | 해결 | `ChatMainFragment`는 최초 진입에서만 `loadFirstPage()`를 호출하고, `DmChatRoomActivity`에서 뒤로 돌아오는 resume 시점의 대화 목록 재조회 wiring이 없다. | `P3-T1`, `P4-R1` | first-resume guard 후 subsequent resume에서 목록 clear와 첫 페이지 재조회 추가, view 재생성 시 guard 초기화 보완 |
| ISSUE-004 | Medium | 해결 | 수신자 선택 화면이 Figma와 달리 별도 `detail_toolbar` 아래 검색창을 쓰고, 검색 모드에서 0건 count인 `검색결과 0`을 표시하지 않는다. | `P4-T1`, `P4-R1` | Figma형 search toolbar와 검색 결과 count state/binding 추가, empty/list 콘텐츠 영역 전환 보완 |
## 최종 보고 형식
```markdown
구현 결과: 완료한 Phase와 사용자 흐름을 한 문장으로 작성한다.
- 변경: 주요 파일과 동작을 적는다.
- 결정: 중요한 Decision Log ID와 내용을 적는다.
- 검증:
- 실행 명령 — 성공/실패와 핵심 수치를 적는다.
- 수동 검증 — 성공/실패/불가 사유를 적는다.
- 남은 항목: 외부 의존, 후속 범위 또는 없음을 적는다.
- 문서: 갱신한 PRD/API Contract/plan/review 링크를 적는다.
```
## Verification Log
### 문서 생성 검증 — 2026-09-14
- 상태: 완료
- 무엇을: `prd.md`와 `plan-task.md`가 요청 범위, Figma 노드, 기존 API 근거, 사용자 인터뷰 결과를 포함하는지 점검했다.
- 왜: 코드 구현 없이 문서만 생성한다는 사용자 요청을 충족하기 위해서다.
- 어떻게:
- 요구사항 `DM-001`~`DM-019`와 계획 Task `P1-T1`, `P2-T1`~`P2-T3` 연결을 확인했다.
- `OQ-001`~`OQ-005`가 확정됐고, 이후 `EXT-001` 서버 계약도 해결 상태로 갱신됐는지 확인했다.
- Android local unit test만 계획하고 `androidTest`, 기기 조작, 스크린샷 QA를 제외했는지 확인했다.
- 남은 항목: 실제 구현 전 Phase 1의 계약·파일 지도 확인.
### Phase 2 Gate 검증 — 2026-09-14
- 상태: 완료
- 무엇을: 크리에이터의 리스너 DM 시작 흐름과 기존 DM/Chat 회귀를 최종 검증했다.
- 왜: `P2-T1`~`P2-T3`가 문서 요구사항과 구현 완료 조건을 모두 충족하는지 확인하기 위해서다.
- 어떻게:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" :app:assembleDebug :app:ktlintCheck`를 실행해 성공을 확인했다.
- `git diff --check`를 실행해 whitespace 오류가 없음을 확인했다.
- `./gradlew :app:testDebugUnitTest --tests "*Dm*" --tests "*Chat*" :app:assembleDebug :app:ktlintCheck`를 실행해 DM/Chat 영향 범위 회귀를 재확인했다.
- 결과:
- 최종 Gradle 명령: `BUILD SUCCESSFUL`, exit 0.
- `testDebugUnitTest`, `assembleDebug`, `ktlintCheck` 통과.
- `git diff --check` 출력 없음.
- 수동/제외 검증:
- 사용자 지시에 따라 `app/src/androidTest`, 기기·에뮬레이터 조작, screenshot, visual QA는 실행하지 않았다.
- LSP diagnostics 도구는 제공되지 않아 Kotlin 컴파일과 ktlint로 대체했다.
- 남은 항목: 없음.
### Phase 3 Gate 검증 — 2026-09-14
- 상태: 완료
- 무엇을: DM 방 복귀 시 대화 탭 목록 첫 페이지 갱신과 기존 채팅 회귀를 검증했다.
- 왜: `P3-T1`이 `DM-020`의 page 0 초기화, 기존 아이템 clear, 현재 filter 재조회 요구를 충족하는지 확인하기 위해서다.
- 어떻게:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.main.chat.ChatMainViewModelTest"`를 실행해 성공을 확인했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.chat.*" --tests "kr.co.vividnext.sodalive.v2.chat.action.*" :app:assembleDebug :app:ktlintCheck`를 실행해 성공을 확인했다.
- `git diff --check`를 실행해 whitespace 오류가 없음을 확인했다.
- 결과:
- 최종 Gradle 명령: `BUILD SUCCESSFUL`, exit 0.
- `testDebugUnitTest`, `assembleDebug`, `ktlintCheck` 통과.
- `git diff --check` 출력 없음.
- 수동/제외 검증:
- 사용자 확인으로 뒤로가기 시 대화 탭으로 복귀하는 흐름은 기능적으로 맞다고 확정했다.
- 사용자 지시에 따라 `app/src/androidTest`, 기기·에뮬레이터 조작, screenshot, visual QA는 실행하지 않았다.
- LSP diagnostics 도구는 제공되지 않아 Kotlin 컴파일과 ktlint로 대체했다.
- 남은 항목: 없음.
### Phase 4 Gate 검증 — 2026-09-14
- 상태: 완료
- 무엇을: Figma 수신자 검색 UI 정합성과 기존 수신자/DM 생성 회귀를 검증했다.
- 왜: `P4-T1`이 `DM-021`, `DEC-012`, `ISSUE-004`의 검색 toolbar와 `검색결과 0` 표시 요구를 충족하는지 확인하기 위해서다.
- 어떻게:
- focused test로 layout/source contract와 ViewModel count state를 확인했다.
- recipient 영향 범위와 기존 `DmChatRepositoryTest`, `assembleDebug`, `ktlintCheck`를 한 번에 실행했다.
- `git diff --check`로 whitespace 오류가 없음을 확인했다.
- 결과:
- 최종 Gradle 명령: `BUILD SUCCESSFUL`, exit 0.
- `testDebugUnitTest`, `assembleDebug`, `ktlintCheck` 통과.
- `git diff --check` 출력 없음.
- 수동/제외 검증:
- 사용자 지시에 따라 `app/src/androidTest`, 기기·에뮬레이터 조작, screenshot, visual QA는 실행하지 않았다.
- LSP diagnostics 도구는 제공되지 않아 Kotlin 컴파일과 ktlint로 대체했다.
- 남은 항목: 없음.