Files
sodalive-ios/docs/20260914_크리에이터_DM_리스너_선택/prd.md
T

283 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 크리에이터 DM 리스너 선택 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 검토 중 |
| 작성일 | `2026-09-14` |
| 최종 수정일 | `2026-09-14` |
| 대상 제품 | 크리에이터가 리스너에게 먼저 DM 보내기 |
| 작성자·결정권자 | 제품 결정: 요청자 / 문서 작성: Sisyphus |
| 관련 API Contract | 이 문서 §11 |
| 관련 구현 계획 | `docs/20260914_크리에이터_DM_리스너_선택/plan-task.md` |
| 관련 review | 없음 |
### 요구사항 상태
| 상태 | 의미 | 구현 처리 |
|---|---|---|
| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Task와 완료 증거로 추적 |
| 미결 | 제품·UX·운영 결정이 더 필요함 | 구현 전 질문 또는 API 확인 필요 |
| 제외 | 현재 릴리스에서 구현하지 않기로 결정 | Decision Log에 기록 |
## 1. Overview
현재 리스너는 크리에이터 채널에서 크리에이터에게 바로 DM을 시작할 수 있지만, 크리에이터가 리스너에게 먼저 DM을 시작하는 진입점은 없다. 이 기능은 메인 대화 탭에서 크리에이터가 팔로우 리스트 또는 닉네임 검색으로 리스너를 선택하고, 확인 팝업 후 기존 유저-크리에이터 DM 방 생성 API로 DM 방에 진입하게 한다.
## 2. Problem Statement
현재 사용자는 다음 문제를 겪는다.
- 크리에이터가 리스너에게 선제적으로 1:1 DM을 시작할 수 없다.
- 기존 DM 생성 흐름은 크리에이터 채널에서 리스너가 크리에이터에게 보내는 방향을 기준으로 구성되어 있다.
- 크리에이터가 리스너를 찾기 위해 별도 화면이나 외부 수단을 사용해야 한다.
문제를 해결했다는 판단은 크리에이터 계정이 메인 대화 탭에서 리스너를 선택해 기존 DM 방 생성 API를 호출하고, 생성된 DM 방으로 진입하는 것으로 한다.
## 3. Goals
### 3.1 제품 목표
- 크리에이터가 메인 대화 탭에서 리스너에게 먼저 DM을 시작할 수 있다.
- 초기 화면은 기존 팔로우 전체 리스트 페이지와 동일한 API를 사용한다.
- 검색은 2글자 이상일 때만 `GET /member/search`를 500ms debounce 후 호출한다.
- 선택한 유저에게 메시지를 보낼지 확인하는 다이얼로그를 보여준다.
- 보내기 시 기존 유저-크리에이터 DM 방 생성 API를 재사용한다.
### 3.2 UX 목표
- 메인 대화 탭 우측 하단에 홈의 플로팅 `+` 버튼과 같은 진입점을 제공한다.
- 새 화면은 Figma `2372:23496` 기준의 검색 바, 팔로우 섹션, 유저 리스트 구조를 따른다.
- 확인 팝업은 Figma `2372:23521` 기준 문구와 V2 공통 `SodaV2ActionModal`을 재사용한다.
- 검색 결과가 없을 때 화면 중앙에 `사용자가 없어요.\n다른 이름으로 다시 검색해 주세요.`를 다국어 문구로 표시한다.
## 4. Non-Goals
- 새 채팅 프로토콜, WebSocket 전송 방식, 메시지 본문 전송 기능은 만들지 않는다.
- 기존 채팅방 UI(`UserCreatorChatRoomView`)의 메시지 표시·전송 UX를 변경하지 않는다.
- 팔로우 리스트 API의 서버 계약을 새로 정의하지 않는다. 기존 API를 재사용한다.
- 검색 API의 서버 응답 필드, 필터링 정책, 차단 유저 제외 정책을 클라이언트에서 추정하지 않는다.
- 새 공통 모달 컴포넌트를 만들지 않는다.
## 5. Target Users and Permissions
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|---|---|---|---|
| 크리에이터 | 리스너에게 먼저 DM 시작 | 대화 탭 진입, 팔로우 리스트 조회, 검색, 유저 선택, 확인 후 DM 방 진입 | iOS 앱 |
| 리스너 | 크리에이터가 시작한 DM 수신 | 기존 DM 방에서 메시지 확인 | iOS 앱 |
### 5.2 권한
- 인증 주체: 로그인된 사용자.
- 허용 역할: `MemberRole.CREATOR`인 크리에이터.
- 거부 조건: 비로그인 또는 크리에이터가 아닌 사용자는 메인 대화 탭의 신규 `+` 진입점을 보지 않는다.
- 리소스 소유권: 방 생성 권한과 recipient 검증은 서버의 `/api/v2/user-creator-chat/rooms/create` 정책을 따른다.
## 6. 핵심 사용자 흐름
1. 크리에이터가 메인 대화 탭에 진입한다.
2. 우측 하단 `+` 버튼을 터치한다.
3. 새 유저 선택 화면이 열린다.
4. 초기 상태에서 기존 팔로우 전체 리스트 API의 결과를 보여준다.
5. 검색창에 2글자 이상 입력하면 500ms 동안 추가 입력이 멈춘 뒤 `GET /member/search?nickname={query}`를 호출한다.
6. 검색 결과가 없으면 중앙에 empty 문구를 표시한다.
7. 유저를 터치하면 `메시지 보내기` 확인 팝업을 표시한다.
8. 보내기를 터치하면 기존 DM 방 생성 API를 호출한다.
9. 성공 시 반환된 `roomId``AppStep.userCreatorChatRoom(roomId:)`에 진입한다.
## 7. 정보 구조와 라우팅
```text
MainView / MainTab.chat
MainChatView
+ floating action button
-> AppStep.userCreatorChatRecipientSearch
UserCreatorChatRecipientSearchView
-> AppStep.userCreatorChatRoom(roomId: Int)
```
- 새 라우트 이름은 구현 시 코드 스타일에 맞춰 조정할 수 있으나, 역할은 “크리에이터가 DM을 시작할 리스너 선택 화면” 하나로 제한한다.
- 기존 `AppStep.userCreatorChatCreator(creatorId:)`는 리스너가 크리에이터 채널에서 DM을 시작하는 흐름으로 유지한다.
- 새 흐름은 유저 선택 후 `recipientId` 기반으로 방을 생성한다.
## 8. 기능 요구사항
### 8.1 진입점
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `CDM-ENTRY-001` | 확정 | 메인 대화 탭 우측 하단에 홈처럼 `+` 플로팅 버튼을 추가한다. | 크리에이터 계정에서만 보이고, 탭 시 새 리스너 선택 화면으로 이동한다. | `P1-T1` |
| `CDM-ENTRY-002` | 확정 | 비크리에이터와 비로그인 사용자는 신규 `+` 버튼을 보지 않는다. | `MemberRole.CREATOR`가 아니면 버튼이 렌더링되지 않는다. | `P1-T1` |
### 8.2 리스너 선택 화면
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `CDM-LIST-001` | 확정 | 새 화면은 Figma `2372:23496` 구조를 따른다. | 뒤로가기, 검색 바, 섹션 타이틀, 리스트가 검은 배경 위에 배치된다. | `P2-T1` |
| `CDM-LIST-002` | 확정 | 초기 진입 시 기존 팔로우 전체 리스트 페이지와 동일한 API를 호출한다. | `FollowCreatorRepository.getFollowedCreatorAllList(page:size:)`와 같은 endpoint·pagination 정책을 사용한다. | `P2-T2` |
| `CDM-LIST-003` | 확정 | 초기 리스트 아이템 터치 시 확인 팝업을 보여준다. | 터치만으로 방 생성 API가 바로 호출되지 않는다. | `P2-T4` |
### 8.3 검색
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `CDM-SEARCH-001` | 확정 | 검색어가 2글자 미만이면 검색 API를 호출하지 않는다. | 0~1글자 입력 중 network 요청 0회, 화면은 초기 팔로우 리스트 기준을 유지한다. | `P2-T3` |
| `CDM-SEARCH-002` | 확정 | 검색어가 2글자 이상이면 500ms debounce 후 `GET /member/search`를 호출한다. | 연속 입력 중에는 마지막 입력 후 500ms가 지나기 전 요청하지 않는다. | `P2-T3` |
| `CDM-SEARCH-003` | 확정 | 검색 결과가 없을 때 중앙 empty 문구를 표시한다. | `사용자가 없어요.\n다른 이름으로 다시 검색해 주세요.`가 다국어 키로 표시된다. | `P2-T3` |
### 8.4 확인 팝업과 방 생성
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `CDM-MODAL-001` | 확정 | 유저 터치 시 V2 내부 공통 팝업을 재사용한다. | `SodaV2ActionModal`을 사용하고 새 모달 컴포넌트를 만들지 않는다. | `P2-T4` |
| `CDM-MODAL-002` | 확정 | 팝업 문구는 `메시지 보내기`, `{{사용자이름}}에게 메시지를 보낼까요?`, `취소`, `보내기`를 다국어 처리한다. | 모든 문구가 `I18n` 키를 통해 참조된다. | `P2-T4` |
| `CDM-CREATE-001` | 확정 | 보내기 시 기존 유저-크리에이터 DM 방 생성 API를 호출한다. | `POST /api/v2/user-creator-chat/rooms/create` 성공 시 `roomId`를 받아 DM 방으로 진입한다. | `P2-T5` |
| `CDM-CREATE-002` | 확정 | 크리에이터가 리스너에게 보내는 흐름은 선택한 유저 ID를 `recipientId`로 보낸다. | request body가 `recipientId``creatorId`를 동시에 서로 다른 값으로 보내지 않는다. | `P2-T5` |
## 9. UI/UX Expectations
### 9.1 디자인 기준
- 신규 화면 Figma: `2372:23496` (`chat_src_001`, 402×874)
- 확인 팝업 Figma: `2372:23521` (`Frame 1707482932`, 340×207)
- 검색 안내 문구: Figma 기준 `팬 이름을 입력하세요`
- 초기 섹션 타이틀: Figma 기준 `팔로워`
- 리스트 아이템: 원형 프로필 이미지와 닉네임 텍스트 구조를 사용한다.
### 9.2 화면 상태
- loading: 초기 팔로우 리스트와 검색 요청 중 기존 프로젝트 loading 패턴을 따른다.
- empty: 검색 결과 0건일 때 중앙 안내 문구 표시.
- error: 기존 toast 또는 공통 오류 메시지 정책을 따른다.
- success: 방 생성 성공 시 별도 성공 toast 없이 채팅방으로 이동한다.
## 10. 다국어 문구
| 용도 | 한국어 문구 | 비고 |
|---|---|---|
| 검색 안내 문구 | 팬 이름을 입력하세요 | Figma 기준 |
| 초기 섹션 타이틀 | 팔로워 | Figma 기준 |
| 검색 empty 1행 | 사용자가 없어요. | 줄바꿈 포함 |
| 검색 empty 2행 | 다른 이름으로 다시 검색해 주세요. | 줄바꿈 포함 |
| 팝업 제목 | 메시지 보내기 | Figma 기준 |
| 팝업 본문 | {{사용자이름}}에게 메시지를 보낼까요? | 닉네임 치환 |
| 팝업 취소 | 취소 | 기존 `I18n.Common.cancel` 재사용 가능 |
| 팝업 보내기 | 보내기 | 새 키 또는 기존 동일 의미 키 재사용 |
## 11. API 계약
### 11.1 초기 팔로우 전체 리스트
| 항목 | 내용 |
|---|---|
| 기존 호출 지점 | `FollowCreatorViewModel.getFollowedCreatorAllList()` |
| Repository | `FollowCreatorRepository.getFollowedCreatorAllList(page:size:)` |
| Method | `GET` |
| Path | `/live/recommend/following/channel/all/list` |
| Query | `page`, `size`, `timezone` |
| Response decode | `ApiResponse<GetCreatorFollowingAllListResponse>` |
| Item model | `GetCreatorFollowingAllListItem(creatorId, nickname, profileImageUrl, isFollow, isNotify)` |
### 11.2 유저 검색
| 항목 | 내용 |
|---|---|
| 기존 API | `UserApi.searchUser(nickname:)` |
| Repository | `UserRepository.searchUser(nickname:)` |
| Method | `GET` |
| Path | `/member/search` |
| Query | `nickname` |
| 호출 조건 | 검색어 trim 후 2글자 이상, 500ms debounce 완료 |
### 11.3 DM 방 생성
| 항목 | 내용 |
|---|---|
| 기존 API | `UserCreatorChatApi.createRoom` |
| Repository | `UserCreatorChatRepository.createRoom(...)` |
| Method | `POST` |
| Path | `/api/v2/user-creator-chat/rooms/create` |
| 기존 iOS request | `UserCreatorCreateRoomRequest(creatorId: Int)` |
| 신규 요구 request | `recipientId: Int?`, `creatorId: Int?` 중 하나만 유효하게 전송 |
| 성공 response | `UserCreatorCreateRoomResponse(roomId: Int)` |
서버 request 기준:
```kotlin
data class CreateUserCreatorChatRoomRequest(
val recipientId: Long? = null,
val creatorId: Long? = null
)
```
- 리스너가 크리에이터 채널에서 DM 시작: 기존처럼 `creatorId` 전송.
- 크리에이터가 리스너에게 DM 시작: 선택한 유저의 ID를 `recipientId`로 전송.
- `recipientId``creatorId`가 서로 다른 값으로 동시에 전송되는 상태는 만들지 않는다.
## 12. 보안과 데이터 취급
- 인증 header는 기존 Moya target의 `Authorization: Bearer {token}` 정책을 따른다.
- 검색어, 닉네임, member ID 외 민감정보를 로그에 남기지 않는다.
- 방 생성 실패 시 서버 message 또는 `I18n.Common.commonError`를 사용한다.
## 13. 성능과 품질 요구사항
- 검색 debounce 시간은 500ms로 고정한다.
- 2글자 미만 검색어는 network 요청을 보내지 않는다.
- 검색어가 바뀌면 이전 pending debounce 작업은 취소한다.
- pagination은 기존 팔로우 전체 리스트의 `page = 1`, `size = 10`, `isLast` 정책을 따른다.
- 신규 dependency를 추가하지 않는다.
## 14. 성공 기준
### 14.1 기능 수용 기준
- [ ] 크리에이터 계정의 메인 대화 탭 우측 하단에 `+` 버튼이 표시된다.
- [ ] `+` 버튼을 누르면 Figma 기준 리스너 선택 화면으로 이동한다.
- [ ] 화면 최초 진입 시 기존 팔로우 전체 리스트 API가 호출된다.
- [ ] 검색어 2글자 미만에서는 `/member/search` 요청이 없다.
- [ ] 검색어 2글자 이상에서 마지막 입력 500ms 후 `/member/search?nickname=` 요청이 1회 발생한다.
- [ ] 검색 결과가 없으면 다국어 empty 문구가 중앙에 표시된다.
- [ ] 유저 터치 시 `SodaV2ActionModal` 확인 팝업이 표시된다.
- [ ] 보내기 터치 시 `recipientId` 기반으로 기존 DM 방 생성 API가 호출되고, 성공 시 DM 방으로 이동한다.
### 14.2 추적성 완료 기준
- [ ] 모든 `확정` 요구사항이 `plan-task.md`의 Goal에 연결된다.
- [ ] 모든 신규 문구가 다국어 키에 연결된다.
- [ ] 기존 크리에이터 채널 DM 시작 흐름이 회귀하지 않는다.
## 15. Open Questions
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 기한/시점 | 영향 Goal |
|---|---|---|---|---|---|---|
| 없음 | 확정 | 인터뷰로 초기 리스트 API 기준 확인 완료 | 기존 팔로우 전체 리스트 API 사용 | 요청자 | 2026-09-14 | `P2-T2` |
## 16. 요구사항 추적표
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|---|---|---:|---|---|---|
| `CDM-ENTRY-001~002` | 해당 없음 | 1 | `P1-T1` | 빌드 | 크리에이터/비크리에이터 버튼 노출 확인 |
| `CDM-LIST-001~003` | §11.1 | 2 | `P2-T1`, `P2-T2`, `P2-T4` | 빌드 | 초기 목록과 팝업 확인 |
| `CDM-SEARCH-001~003` | §11.2 | 2 | `P2-T3` | 빌드 | 2글자/500ms/empty 확인 |
| `CDM-MODAL-001~002`, `CDM-CREATE-001~002` | §11.3 | 2 | `P2-T4`, `P2-T5` | 빌드 | request payload와 방 진입 확인 |
## 17. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|---|---|---|---|---|---|
| 2026-09-14 | `DEC-001` | 확정 | 코드 구현 없이 PRD와 `plan-task.md`만 작성한다. | 사용자 직접 지시 | 전체 |
| 2026-09-14 | `DEC-002` | 확정 | 새 화면 초기 목록은 기존 팔로우 전체 리스트 페이지와 동일한 API를 사용한다. | 사용자 답변 `B` | `CDM-LIST-002`, §11.1, `P2-T2` |
| 2026-09-14 | `DEC-003` | 확정 | 크리에이터가 리스너에게 보내는 새 흐름은 `recipientId`를 사용하고, 기존 크리에이터 채널 흐름은 `creatorId`를 유지한다. | 서버 request contract | `CDM-CREATE-002`, §11.3, `P2-T5` |
## 18. 변경 관리
요구사항 변경 시 다음을 확인한다.
- [ ] Decision Log에 변경 이유와 날짜를 기록했다.
- [ ] 관련 요구사항 상태·본문·수용 기준을 갱신했다.
- [ ] API request/response/error 계약을 갱신했다.
- [ ] `plan-task.md`의 범위·Files·Interfaces·체크박스·완료 증거를 코드 변경 전에 갱신했다.
- [ ] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않았다.