From aea4e5558c83751ee6849ac53779b3010b54e453 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 14 Sep 2026 22:03:37 +0900 Subject: [PATCH] =?UTF-8?q?docs(chat):=20DM=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=9D=84=20=EB=AC=B8=EC=84=9C=ED=99=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 411 ++++++++++++++++++ .../20260914_크리에이터_DM_리스너_선택/prd.md | 282 ++++++++++++ 2 files changed, 693 insertions(+) create mode 100644 docs/20260914_크리에이터_DM_리스너_선택/plan-task.md create mode 100644 docs/20260914_크리에이터_DM_리스너_선택/prd.md diff --git a/docs/20260914_크리에이터_DM_리스너_선택/plan-task.md b/docs/20260914_크리에이터_DM_리스너_선택/plan-task.md new file mode 100644 index 00000000..b05f62bb --- /dev/null +++ b/docs/20260914_크리에이터_DM_리스너_선택/plan-task.md @@ -0,0 +1,411 @@ +# 크리에이터 DM 리스너 선택 Goal 실행형 구현 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 구현 검증 중 | +| 작성일 | `2026-09-14` | +| 요구사항 기준 | `docs/20260914_크리에이터_DM_리스너_선택/prd.md` | +| API 기준 | `docs/20260914_크리에이터_DM_리스너_선택/prd.md` §11 | +| 현재 Phase | Phase 3 검증 | +| 현재 활성 Goal | `P3-GATE` | + +## 목표 + +크리에이터가 메인 대화 탭에서 팔로우 리스트 또는 닉네임 검색으로 리스너를 선택하고 기존 유저-크리에이터 DM 방 생성 API로 DM 방을 시작한다. + +## 현재 상태 + +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | +|---:|---|---:|---|---| +| 1 | 완료 | `2/2` | 없음 | 없음 | +| 2 | 완료 | `5/5` | 없음 | 수동 네트워크 QA 필요 | +| 3 | 진행 중 | `0/1` | `P3-GATE` | 수동 QA 미수행 | + +## 범위 + +### 포함 + +- 메인 대화 탭 우측 하단 크리에이터 전용 `+` 플로팅 버튼. +- 신규 리스너 선택 화면과 라우팅. +- 기존 팔로우 전체 리스트 API 재사용. +- `GET /member/search?nickname=` 2글자 이상, 500ms debounce 검색. +- 검색 empty 문구 다국어 처리. +- `SodaV2ActionModal` 기반 확인 팝업. +- 기존 DM 방 생성 API의 request model 확장과 `recipientId` 기반 호출. +- 성공 시 `AppStep.userCreatorChatRoom(roomId:)` 진입. + +### 제외 + +- 채팅방 메시지 UI와 WebSocket 전송 로직 변경. +- 새 공통 모달 컴포넌트 작성. +- 신규 dependency 추가. +- 팔로우 전체 리스트 API 서버 계약 변경. + +## 기술적 제약 + +- 기술 스택: SwiftUI, Combine, Moya, 기존 `AppState`/`AppStep` 라우팅. +- UI 배치: Figma `2372:23496`, 팝업 `2372:23521` 기준. +- 공통 컴포넌트: `SodaV2ActionModal`, 기존 색상·폰트·spacing token 재사용. +- API 재사용: `FollowCreatorRepository`, `UserRepository.searchUser`, `UserCreatorChatRepository` 기준. +- request model: `UserCreatorCreateRoomRequest`는 `recipientId`와 `creatorId`를 optional로 표현할 수 있어야 한다. +- 검증: 테스트 번들 타깃이 확인되지 않으므로 focused 자동 테스트 대신 빌드와 수동 QA 기준을 명시한다. + +## Task TDD 작성 규칙 + +현재 저장소는 `docs/agent-guides/build-test-verification.md` 기준 테스트 번들 타깃이 확인되지 않는다. 구현 Task는 가능한 경우 작은 ViewModel 단위 테스트를 우선 검토하되, 테스트 타깃이 없으면 `TDD 예외 사유`와 `대체 검증 방법`을 Task에 기록한다. + +## Phase 1 — 라우팅과 진입점 + +**Phase 결과:** 크리에이터가 메인 대화 탭에서 신규 리스너 선택 화면에 진입할 수 있다. + +**선행조건:** PRD 확정. + +**Phase 완료 조건:** `P1-T1`, `P1-T2` 완료와 빌드 통과. + +### 구현 항목 + +#### Task 1.1 메인 대화 탭 플로팅 버튼 + +**Goal 실행 `P1-T1`:** 크리에이터 계정의 메인 대화 탭 우측 하단에 신규 DM 시작 `+` 버튼을 표시한다. + +- **시작 조건:** `CDM-ENTRY-001`, `CDM-ENTRY-002` 확정. +- **완료 증거:** 크리에이터 role에서만 버튼 표시, 비크리에이터에서는 미표시. +- **범위 밖:** 새 화면 내부 리스트와 API 호출. +- **TDD 예외 사유:** UI 조건 렌더링 작업이며 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** `UserDefaults.role`이 `MemberRole.CREATOR.rawValue`일 때와 아닐 때의 수동 QA, Debug 빌드. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/MainView.swift` +- Reuse: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift` 또는 기존 홈 플로팅 버튼 컴포넌트 + +**Interfaces:** + +- Consumes: `MemberRole.CREATOR.rawValue`, `MainTab.chat` +- Produces: `MainView` 채팅 탭 overlay에서 신규 DM 시작 액션 + +- [x] 버튼 재사용 후보를 확인하고 새 공통 abstraction 없이 가장 가까운 기존 플로팅 버튼을 사용한다. +- [x] 기존 `MainChatView` 수정 없이 `MainView` overlay에서 버튼을 표시한다. +- [x] `MainView`에서 현재 탭이 `.chat`이고 role이 creator일 때만 버튼을 표시한다. +- [x] 버튼 bottom padding은 main tab bar와 mini player가 가리지 않도록 기존 `creatorActionMenuBottomPadding` 계산 방식을 따른다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +#### Task 1.2 AppStep 라우팅 추가 + +**Goal 실행 `P1-T2`:** 신규 리스너 선택 화면으로 이동할 수 있는 AppStep 라우팅을 추가한다. + +- **시작 조건:** `P1-T1` 완료. +- **완료 증거:** `+` 버튼 탭 시 신규 화면이 push되고 뒤로가기로 복귀 가능. +- **범위 밖:** 화면 내부 API 연동. +- **TDD 예외 사유:** 라우팅 연결 작업이며 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** 수동 내비게이션 QA와 Debug 빌드. + +**Files:** + +- Modify: `SodaLive/Sources/App/AppStep.swift` +- Modify: `SodaLive/Sources/ContentView.swift` +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` + +**Interfaces:** + +- Produces: `AppStep.userCreatorChatRecipientSearch` +- Consumes: `AppState.shared.setAppStep(step:)` + +- [x] `AppStep`에 신규 case를 추가한다. +- [x] `AppStepLayerView`에서 신규 case를 새 화면으로 매핑한다. +- [x] `MainView`의 채팅 탭 `+` 버튼 액션에서 신규 AppStep을 호출한다. +- [x] 새 화면은 최소 shell만 만들고 내부 API는 Phase 2에서 연결한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +## Phase 2 — 리스트, 검색, 확인, 방 생성 + +**Phase 결과:** 크리에이터가 리스너를 선택하고 DM 방 생성 후 채팅방에 진입한다. + +**선행조건:** Phase 1 완료. + +**Phase 완료 조건:** `P2-T1`~`P2-T5` 완료와 Phase 2 Gate 통과. + +### 구현 항목 + +#### Task 2.1 화면 UI shell과 다국어 키 + +**Goal 실행 `P2-T1`:** Figma 기준 신규 화면 shell과 필요한 다국어 키를 추가한다. + +- **시작 조건:** `P1-T2` 완료, Figma `2372:23496`, `2372:23521` 확인. +- **완료 증거:** 검색 바, 섹션 타이틀, 리스트 검색 안내 문구, empty 영역, 팝업 문구 키가 존재한다. +- **범위 밖:** 실제 API 호출과 방 생성. +- **TDD 예외 사유:** UI shell과 resource 추가 작업이며 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** SwiftGen 또는 프로젝트 string 생성 절차가 있으면 실행, 없으면 빌드로 `I18n` 참조 성공 확인. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` +- Modify: localization resource files used by `I18n` + +**Interfaces:** + +- Produces: `I18n.UserCreatorChatRecipientSearch.searchPlaceholder` +- Produces: `I18n.UserCreatorChatRecipientSearch.sectionTitle` +- Produces: `I18n.UserCreatorChatRecipientSearch.emptyMessage` +- Produces: `I18n.UserCreatorChatRecipientSearch.dialogTitle` +- Produces: `I18n.UserCreatorChatRecipientSearch.dialogMessage(_ nickname: String)` +- Produces: `I18n.UserCreatorChatRecipientSearch.sendButton` + +- [x] 검색 안내 문구 `팬 이름을 입력하세요`를 다국어 키로 추가한다. +- [x] 섹션 타이틀 `팔로워`를 다국어 키로 추가한다. +- [x] empty 문구 `사용자가 없어요.\n다른 이름으로 다시 검색해 주세요.`를 다국어 키로 추가한다. +- [x] 팝업 제목과 본문, 보내기 문구를 다국어 키로 추가한다. +- [x] 화면 shell이 Figma 기준 검은 배경, 뒤로가기, 검색 바, 리스트 영역을 표시하도록 작성한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +#### Task 2.2 초기 팔로우 전체 리스트 연동 + +**Goal 실행 `P2-T2`:** 화면 진입 시 기존 팔로우 전체 리스트 API로 초기 목록을 표시한다. + +- **시작 조건:** `P2-T1` 완료. +- **완료 증거:** 화면 최초 진입 시 `/live/recommend/following/channel/all/list`가 기존 pagination 정책으로 호출된다. +- **범위 밖:** 검색 API와 방 생성 API. +- **TDD 예외 사유:** network ViewModel 작업이나 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** Charles/Xcode network log 또는 Moya stub 가능 시 요청 path·query 확인, Debug 빌드. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` +- Reuse: `SodaLive/Sources/Follow/FollowCreatorRepository.swift` +- Reuse: `SodaLive/Sources/Follow/GetCreatorFollowingAllListResponse.swift` + +**Interfaces:** + +- Consumes: `FollowCreatorRepository.getFollowedCreatorAllList(page:size:)` +- Produces: `UserCreatorChatRecipientItem(id:nickname:profileImageUrl:)` + +- [x] ViewModel에 `isLoading`, `errorMessage`, `isShowPopup`, `items`, `page`, `isLast` 상태를 추가한다. +- [x] 최초 진입 시 `getFollowedCreatorAllList(page: 1, size: 10)`을 호출한다. +- [x] API response의 `creatorId`, `nickname`, `profileImageUrl`을 화면 공통 item으로 매핑한다. +- [x] 리스트 마지막 아이템 노출 시 다음 page를 호출한다. +- [x] 오류는 기존 toast 방식으로 표시한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +#### Task 2.3 유저 검색과 debounce + +**Goal 실행 `P2-T3`:** 2글자 이상 검색어에 대해 500ms debounce 후 유저 검색 API를 호출하고 empty 상태를 표시한다. + +- **시작 조건:** `P2-T2` 완료. +- **완료 증거:** 0~1글자는 검색 요청 0회, 2글자 이상은 마지막 입력 500ms 후 요청 1회. +- **범위 밖:** 유저 선택 후 방 생성. +- **TDD 예외 사유:** 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** 수동 입력 QA와 network 요청 횟수 확인. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` +- Reuse: `SodaLive/Sources/User/UserRepository.swift` +- Reuse: `SodaLive/Sources/User/UserApi.swift` + +**Interfaces:** + +- Consumes: `UserRepository.searchUser(nickname:)` +- Produces: search mode item list and empty state flag + +- [x] 검색어 상태를 ViewModel로 전달한다. +- [x] trim 결과가 2글자 미만이면 pending debounce를 취소하고 초기 팔로우 리스트 모드로 돌아간다. +- [x] trim 결과가 2글자 이상이면 500ms debounce 후 `UserRepository.searchUser(nickname:)`를 호출한다. +- [x] 검색어가 바뀌면 이전 debounce 작업과 이전 검색 결과 적용을 취소하거나 무시한다. +- [x] 검색 결과 0건이면 중앙 empty 문구를 표시한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +#### Task 2.4 유저 선택 확인 팝업 + +**Goal 실행 `P2-T4`:** 리스트 유저 터치 시 V2 공통 팝업으로 DM 보내기 확인을 받는다. + +- **시작 조건:** `P2-T2` 또는 `P2-T3` 완료. +- **완료 증거:** 유저 터치 시 API 호출 없이 팝업이 뜨고, 취소 또는 dimmed tap으로 닫힌다. +- **범위 밖:** 보내기 이후 방 생성 API 구현. +- **TDD 예외 사유:** UI interaction 작업이며 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** 수동 QA와 Debug 빌드. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` +- Reuse: `SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift` + +**Interfaces:** + +- Consumes: `UserCreatorChatRecipientItem` +- Produces: selected recipient state + +- [x] 리스트 아이템 tap handler에서 selected recipient를 저장한다. +- [x] `SodaV2ActionModal` title은 `메시지 보내기` 키를 사용한다. +- [x] message는 선택 유저 닉네임을 넣어 `{{사용자이름}}에게 메시지를 보낼까요?` 형식으로 표시한다. +- [x] 취소와 dimmed tap은 selected recipient를 초기화한다. +- [x] 보내기 버튼은 `P2-T5`의 방 생성 액션을 호출할 수 있도록 연결 지점만 만든다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +#### Task 2.5 recipientId 기반 DM 방 생성 + +**Goal 실행 `P2-T5`:** 확인 팝업의 보내기에서 선택한 리스너 ID로 기존 DM 방 생성 API를 호출하고 채팅방에 진입한다. + +- **시작 조건:** `P2-T4` 완료. +- **완료 증거:** request body가 `recipientId`를 포함하고, 성공 response의 `roomId`로 `userCreatorChatRoom`에 진입한다. +- **범위 밖:** 채팅방 내부 메시지 전송 UI 변경. +- **TDD 예외 사유:** API integration 작업이나 현재 테스트 타깃이 확인되지 않는다. +- **대체 검증 방법:** network request body 확인, 성공 시 화면 이동 수동 QA, 기존 크리에이터 채널 DM 시작 회귀 확인. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatModels.swift` +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Repository/UserCreatorChatRepository.swift` +- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift` +- Verify: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + +**Interfaces:** + +- Produces: `UserCreatorCreateRoomRequest(recipientId: Int? = nil, creatorId: Int? = nil)` +- Produces: `UserCreatorChatRepository.createRoom(recipientId: Int)` +- Preserves: `UserCreatorChatRepository.createRoom(creatorId: Int)` existing caller behavior + +- [x] `UserCreatorCreateRoomRequest`를 `recipientId`와 `creatorId` optional request로 변경한다. +- [x] 기존 `createRoom(creatorId:)`는 `creatorId`만 보내도록 유지한다. +- [x] 신규 `createRoom(recipientId:)`를 추가하고 `recipientId`만 보낸다. +- [x] 보내기 탭 중 중복 제출을 막기 위해 loading 상태 동안 버튼 액션을 무시한다. +- [x] 성공 response의 `roomId`로 `AppState.shared.setAppStep(step: .userCreatorChatRoom(roomId: roomId))`를 호출한다. +- [x] 실패 시 서버 message 또는 `I18n.Common.commonError`를 toast로 표시한다. +- [x] 기존 크리에이터 채널의 `AppStep.userCreatorChatCreator(creatorId:)` 흐름이 `creatorId` request를 유지하는지 확인한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 성공을 확인한다. + +## Phase 3 — 검증 Gate + +**Phase 결과:** 신규 DM 시작 흐름과 기존 DM 시작 흐름이 모두 동작한다. + +**선행조건:** Phase 2 전체 완료. + +### 검증 방법 + +#### Phase 3 Gate + +**Goal 실행 `P3-GATE`:** 요구사항 전체와 회귀 위험을 최종 판정한다. + +- **시작 조건:** `P2-T1`~`P2-T5` 완료. +- **완료 증거:** 아래 명령과 수동 QA 결과를 Progress에 기록. +- **범위 밖:** 실패와 무관한 UI 리팩터링. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +**Expected:** exit code 0. + +수동 검증: + +- [ ] 크리에이터 계정: 메인 대화 탭 우측 하단 `+` 버튼 표시. +- [ ] 비크리에이터 계정: 메인 대화 탭 우측 하단 `+` 버튼 미표시. +- [ ] 신규 화면 최초 진입: `/live/recommend/following/channel/all/list?page=0&size=10&timezone=...` 호출. +- [ ] 검색 1글자: `/member/search` 요청 0회. +- [ ] 검색 2글자 이상 연속 입력: 마지막 입력 500ms 후 `/member/search?nickname=` 요청 1회. +- [ ] 검색 결과 없음: 중앙 empty 문구 표시. +- [ ] 유저 선택: `SodaV2ActionModal` 표시, 취소와 dimmed tap 닫힘. +- [ ] 보내기: `/api/v2/user-creator-chat/rooms/create` request body에 `recipientId`만 포함. +- [ ] 방 생성 성공: `UserCreatorChatRoomView`로 이동. +- [ ] 기존 크리에이터 채널 DM 시작: request body에 `creatorId`만 포함하고 기존처럼 동작. + +## 실행 순서와 의존성 + +| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 | +|---:|---|---|---|---| +| 1 | `P1-T1` | PRD 확정 | 아니요 | role/진입 정책 재확인 | +| 2 | `P1-T2` | `P1-T1` | 아니요 | AppStep 라우팅 방식 확인 | +| 3 | `P2-T1` | `P1-T2` | 아니요 | 다국어 생성 절차 확인 | +| 4 | `P2-T2` | `P2-T1` | 아니요 | 기존 팔로우 API 계약 확인 | +| 5 | `P2-T3` | `P2-T2` | 아니요 | 검색 response model 확인 | +| 6 | `P2-T4` | `P2-T2` 또는 `P2-T3` | 아니요 | 팝업 문구 재확인 | +| 7 | `P2-T5` | `P2-T4` | 아니요 | 서버 request contract 확인 | +| 8 | `P3-GATE` | Phase 2 전체 | 아니요 | 실패 소유 Task에 회귀 수정 Task 추가 | + +```text +P1-T1 → P1-T2 → P2-T1 → P2-T2 → P2-T3 → P2-T4 → P2-T5 → P3-GATE +``` + +## 변경 금지 항목 + +- `Pods/**`, `generated/**`, `build/**`를 직접 수정하지 않는다. +- 새 dependency를 추가하지 않는다. +- 기존 DM 방 UI와 WebSocket 전송 로직을 변경하지 않는다. +- `recipientId`와 `creatorId`를 서로 다른 값으로 동시에 보내는 request를 만들지 않는다. +- 실패하는 검증을 통과시키기 위해 테스트를 삭제·완화하거나 타입 오류를 우회하지 않는다. + +## 의사결정 및 중단 규칙 + +- PRD와 구현 계획이 충돌하면 PRD Decision Log를 먼저 갱신한다. +- 검색 API response model이 기존 `SearchResponseItem`으로 충분하지 않으면 코드 변경 전에 이 문서에 Task를 추가한다. +- 서버가 `recipientId` request를 거부하면 `P2-T5`를 중단하고 request contract 확인 Task를 추가한다. +- UI가 Figma와 충돌하면 Figma 구조를 우선하되 기존 V2 component token을 벗어나지 않는다. + +## Progress + +기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다. + +### 문서 작성 — 2026-09-14 + +- 상태: 완료 +- 무엇을: PRD와 Goal 실행형 구현 계획 초안 작성. +- 왜: 사용자 요청이 “모호한 것은 인터뷰하고 문서만 생성, 코드 구현 금지”였기 때문. +- 어떻게: + - `docs/sample/sample-prd.md` 확인 — 샘플 구조 반영. + - `docs/sample/sample-plan-task.md` 확인 — Phase/Task/Goal 구조 반영. + - Figma `2372:23496`, `2372:23521` 확인 — 화면과 팝업 기준 반영. + - 기존 코드 조사 — `MainChatView`, `FollowCreatorViewModel`, `UserApi.searchUser`, `UserCreatorChatRepository`, `SodaV2ActionModal` 기준 반영. +- 남은 항목: 사용자 문서 검토. +- 다음 행동: PRD와 plan-task 승인 또는 수정 요청. + +### Phase 1~2 구현 — 2026-09-14 + +- 상태: 빌드 검증 완료, 수동 QA 필요 +- 무엇을: + - 메인 대화 탭에 크리에이터 전용 신규 DM 시작 `+` 버튼을 추가했다. + - `AppStep.userCreatorChatRecipientSearch` 라우팅과 리스너 선택 화면을 추가했다. + - 팔로우 전체 리스트, 2글자 이상 500ms debounce 검색, empty 상태, 확인 팝업, `recipientId` 기반 방 생성을 연결했다. + - 기존 `creatorId` 기반 `createRoom(creatorId:)` 흐름은 유지했다. +- 왜: PRD의 크리에이터 선제 DM 시작 요구사항을 기존 API와 V2 컴포넌트 재사용으로 구현하기 위해서다. +- 어떻게: + - 신규 Swift 파일은 프로젝트 수동 등록 위험이 있어 만들지 않고, 기존 컴파일 대상인 `UserCreatorChatRoomView.swift`에 선택 화면 타입을 추가했다. + - `UserCreatorCreateRoomRequest`는 `encodeIfPresent`로 `recipientId`와 `creatorId` 중 nil 필드를 request body에서 제외하도록 했다. + - 검색 응답은 기존 `UserRepository.searchUser` 사용처와 동일하게 `ApiResponse<[GetRoomDetailUser]>`로 디코딩했다. + - 검색어를 2글자 미만으로 지울 때 진행 중인 검색 요청을 취소하고 초기 팔로우 리스트로 복귀하도록 했다. + - 검색/팔로우 목록/방 생성 로딩 상태를 분리해 지연된 검색 실패가 방 생성 중복 방지 상태를 해제하지 않도록 했다. + - 최신 검색 요청만 성공·실패 상태를 반영하고, 새 검색 시작 시 이전 검색 결과를 비워 현재 검색어와 목록이 어긋나지 않도록 했다. +- 검증: + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` — 성공. + - 첫 빌드는 Swift 접근제어 오류를 발견했고 `UserCreatorChatRecipientItem` 접근 수준을 보정했다. + - 두 번째 빌드는 코드사인/리소스 복사 단계에서 120초 제한으로 중단됐다. + - 세 번째 빌드는 600초 제한으로 재실행해 `** BUILD SUCCEEDED **` 확인. + - 검색 취소 로직 보정 후 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 재실행해 `** BUILD SUCCEEDED **` 확인. + - Oracle 코드 리뷰에서 검색 취소·지연 실패·이전 결과 잔존 이슈를 지적받았고, 상태 처리 보정 후 동일 빌드를 재실행해 `** BUILD SUCCEEDED **` 확인. +- 수동 QA 미수행: + - 이 환경에서 로그인된 크리에이터/비크리에이터 계정과 실기기 또는 시뮬레이터 네트워크 관찰을 아직 실행하지 못했다. + - 아래 Phase 3 Gate의 수동 검증 항목은 앱 실행 환경에서 별도 확인해야 한다. + +## Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | +|---|---|---|---|---|---| +| 2026-09-14 | `DEC-001` | 확정 | 코드 구현 없이 문서만 작성한다. | 사용자 직접 지시 | 전체 | +| 2026-09-14 | `DEC-002` | 확정 | 초기 목록은 기존 팔로우 전체 리스트 API를 사용한다. | 사용자 답변 `B` | `P2-T2` | +| 2026-09-14 | `DEC-003` | 확정 | 새 크리에이터→리스너 DM 시작 request는 `recipientId`를 사용한다. | 서버 request contract | `P2-T5` | + +## 발견된 문제 + +| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | +|---|---|---|---|---|---| +| `ISSUE-001` | Medium | 처리 완료 | 현재 iOS `UserCreatorCreateRoomRequest`는 `creatorId` 단일 필드만 가진다. | `P2-T5` | request model을 optional `recipientId`/`creatorId` 구조로 확장했다. | +| `ISSUE-002` | Low | 처리 완료 | 신규 Swift 파일은 `project.pbxproj` 수동 등록이 필요해 누락 시 빌드 실패 위험이 있다. | `P1-T2`, `P2-T1`~`P2-T5` | 기존 컴파일 대상인 `UserCreatorChatRoomView.swift`에 선택 화면 타입을 추가했다. | + +## 최종 보고 형식 + +구현 완료 보고에는 아래 항목을 기록한다. + +- 구현 결과: 완료한 Phase와 사용자가 수행할 수 있게 된 DM 시작 흐름. +- 변경: 주요 파일과 사용자에게 보이는 동작. +- 결정: 적용된 Decision Log ID와 결정 내용. +- 검증: 실행한 명령, exit code, 수동 QA 성공·실패 결과. +- 남은 항목: 외부 의존, 후속 범위, 또는 없음. +- 문서: 갱신한 PRD, plan-task, review 문서 경로. diff --git a/docs/20260914_크리에이터_DM_리스너_선택/prd.md b/docs/20260914_크리에이터_DM_리스너_선택/prd.md new file mode 100644 index 00000000..ea86e833 --- /dev/null +++ b/docs/20260914_크리에이터_DM_리스너_선택/prd.md @@ -0,0 +1,282 @@ +# 크리에이터 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` | +| 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·검증 기록을 삭제하거나 덮어쓰지 않았다.