diff --git a/docs/20260914_크리에이터의_리스너_DM_시작/plan-task.md b/docs/20260914_크리에이터의_리스너_DM_시작/plan-task.md new file mode 100644 index 00000000..3020749f --- /dev/null +++ b/docs/20260914_크리에이터의_리스너_DM_시작/plan-task.md @@ -0,0 +1,729 @@ +# 크리에이터의 리스너 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로 대체했다. +- 남은 항목: 없음.