docs(user-creator-chat): DM 방 생성 요구사항을 기록한다

This commit is contained in:
2026-09-14 14:50:37 +09:00
parent ca1f9b9d00
commit 70f717aa6d
2 changed files with 547 additions and 0 deletions
@@ -0,0 +1,159 @@
# PRD: 크리에이터 시작 DM 방 생성
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 기준 확정 |
| 작성일 | 2026-09-14 |
| 최종 수정일 | 2026-09-14 |
| 대상 제품 | 유저-크리에이터 DM |
| 작성자·결정권자 | 사용자 인터뷰 기준 |
| 관련 구현 계획 | `docs/20260914_크리에이터_리스너_DM방_생성/plan-task.md` |
| 관련 review | 없음 |
## 1. Overview
현재 `POST /api/v2/user-creator-chat/rooms/create`는 일반 유저가 크리에이터에게 DM을 시작할 때 방을 생성하거나 기존 방을 반환한다. 이번 요구사항은 같은 DM 도메인에서 크리에이터도 상대 회원에게 먼저 DM 방을 만들 수 있게 하는 것이다.
첫 메시지 저장, 실시간 전달, 푸시는 이번 API에서 처리하지 않는다. 방 생성 후 메시지는 기존 WebSocket 텍스트 발송 또는 기존 메시지 발송 흐름을 사용한다.
## 2. Problem Statement
- 현재 생성 요청 DTO는 `creatorId`만 받기 때문에 API 의미가 “일반 유저가 크리에이터에게 DM 시작”에 고정되어 있다.
- 크리에이터가 먼저 상대 회원에게 연락하려면 같은 유저-크리에이터 DM 방을 만들 수 있는 서버 계약이 필요하다.
- 기존 클라이언트는 이미 `creatorId`를 보내고 있으므로, 새 필드만 강제하면 기존 리스너 시작 DM 생성 흐름이 깨진다.
문제 해결 여부는 신규 클라이언트가 `recipientId`로 방을 만들 수 있고, 기존 클라이언트가 `creatorId`로 같은 API를 계속 사용할 수 있는지로 판단한다.
## 3. Goals
- `POST /api/v2/user-creator-chat/rooms/create`가 발신자 역할과 무관하게 상대 회원 ID로 DM 방을 생성하거나 기존 방을 반환한다.
- 신규 표준 request 필드는 `recipientId`로 한다.
- 기존 클라이언트 호환을 위해 `creatorId` request 필드는 alias로 유지한다.
- 기존 리스너 → 크리에이터 방 생성 흐름은 계속 동작한다.
- 크리에이터 → 상대 회원 방 생성 흐름도 같은 참여자/중복 방 정책을 따른다.
## 4. Non-Goals
- `/create`에서 첫 텍스트 메시지를 저장하거나 발송하지 않는다.
- `/create`에서 WebSocket 메시지를 대신 보내지 않는다.
- FCM 푸시 발송 정책은 변경하지 않는다.
- DB 스키마를 변경하지 않는다.
- 새 endpoint를 만들지 않는다.
- 기존 `roomId` 응답 구조를 변경하지 않는다.
- `creatorId` alias 제거 시점이나 클라이언트 마이그레이션 일정은 이번 범위에서 정하지 않는다.
## 5. Target Users and Permissions
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|---|---|---|---|
| 리스너/일반 유저 | 크리에이터에게 먼저 DM 시작 | 상대 회원 지정 후 방 생성 | 모바일 클라이언트 |
| 크리에이터 | 상대 회원에게 먼저 DM 시작 | 상대 회원 지정 후 방 생성 | 모바일 클라이언트 |
권한과 거부 조건:
- 인증 주체: 로그인 `Member`
- 허용 역할: 별도 `MemberRole` 제한 없음
- 수신자 조건: 활성 회원, AI 캐릭터 아님, 자기 자신 아님, 차단 정책 통과
- 미인증: 기존처럼 `common.error.bad_credentials`
- 수신자 없음 또는 AI 캐릭터: 기존 정책에 맞춰 `message.error.recipient_not_found`
- 비활성 수신자: 기존처럼 `message.error.recipient_inactive`
- 자기 자신: 기존처럼 `common.error.invalid_request`
- 상대가 발신자를 차단한 경우: 기존처럼 `message.error.blocked_by_recipient`
## 6. 핵심 사용자 흐름
1. 인증 회원이 DM을 시작할 상대 회원을 선택한다.
2. 클라이언트는 `POST /api/v2/user-creator-chat/rooms/create`에 `recipientId`를 보낸다.
3. 서버는 기존 alias인 `creatorId`만 온 요청도 허용한다.
4. 서버는 발신자와 수신자 사이의 활성 DM 방이 있으면 기존 `roomId`를 반환한다.
5. 활성 DM 방이 없으면 방과 두 참여자를 생성하고 새 `roomId`를 반환한다.
6. 클라이언트는 반환된 `roomId`로 기존 방 입장 및 메시지 발송 흐름을 진행한다.
## 7. 기능 요구사항
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `DMROOM-001` | 확정 | `/create`는 `recipientId`를 표준 상대 회원 ID로 받아 방을 생성/조회한다. | `recipientId`만 보낸 요청이 기존 `roomId` 응답을 받는다. | `P1-T1` |
| `DMROOM-002` | 확정 | 기존 클라이언트 호환을 위해 `creatorId`만 보낸 요청도 계속 허용한다. | 기존 `creatorId` request가 깨지지 않고 동일한 서비스 흐름을 탄다. | `P1-T1` |
| `DMROOM-003` | 확정 | `recipientId`와 `creatorId`가 모두 없으면 잘못된 요청으로 거부한다. | `common.error.invalid_request`로 실패한다. | `P1-T1` |
| `DMROOM-004` | 확정 | 두 필드가 모두 있고 값이 다르면 모호한 요청으로 거부한다. | `common.error.invalid_request`로 실패하고 방을 만들지 않는다. | `P1-T1` |
| `DMROOM-005` | 확정 | 수신자 권한/상태 검증은 기존 `validateRecipient` 정책을 재사용한다. | 비활성, AI 캐릭터, 자기 자신, 차단 관계의 기존 오류 key가 유지된다. | `P1-T2` |
| `DMROOM-006` | 확정 | 기존 활성 방이 있으면 새 방을 만들지 않고 기존 `roomId`를 반환한다. | 같은 두 회원으로 두 번 호출해도 활성 방은 1개다. | `P1-T2` |
| `DMROOM-007` | 확정 | `/create`는 첫 메시지 저장/전달/푸시를 수행하지 않는다. | 방 생성 후 `UserCreatorChatMessage`가 생성되지 않는다. | `P1-T2` |
## 8. API 계약
### 8.1 Endpoint
- Method: `POST`
- Path: `/api/v2/user-creator-chat/rooms/create`
- Auth: 로그인 회원 필수
- Success envelope: 기존 `ApiResponse.ok(...)`
### 8.2 Request
```json
{
"recipientId": 200,
"creatorId": 200
}
```
- `recipientId`: 신규 표준 필드. 발신자 반대편 회원 ID다.
- `creatorId`: 기존 클라이언트 호환용 alias. 신규 클라이언트는 `recipientId`를 사용한다.
- 둘 중 하나만 보내는 요청을 허용한다.
- 둘 다 보내는 경우 값이 같으면 허용한다.
- 둘 다 보내는 경우 값이 다르면 `common.error.invalid_request`로 거부한다.
- 둘 다 없으면 `common.error.invalid_request`로 거부한다.
### 8.3 Response
```json
{
"roomId": 123
}
```
- 응답 DTO `CreateUserCreatorChatRoomResponse`의 필드는 변경하지 않는다.
## 9. 보안과 데이터 취급
- 수신자 ID는 요청 본문 외 로그에 별도 기록하지 않는다.
- 차단 정책은 기존 `BlockMemberRepository.isBlocked(blockedMemberId = sender.id, memberId = recipient.id)` 기준을 유지한다.
- AI 캐릭터용 `Member`와의 DM 방은 계속 생성하지 않는다.
- 공개 API 스키마에서 기존 필드 제거는 금지한다.
## 10. 성공 기준
- [x] 신규 `recipientId` 요청으로 크리에이터가 상대 회원과 DM 방을 만들 수 있다.
- [x] 기존 `creatorId` 요청으로 리스너가 크리에이터와 DM 방을 만들 수 있다.
- [x] 같은 두 회원의 중복 요청은 같은 활성 `roomId`를 반환한다.
- [x] `/create` 호출만으로 메시지, WebSocket 전달, FCM 푸시가 발생하지 않는다.
- [x] 오류 key는 기존 정책과 호환된다.
## 11. Open Questions
없음. 인터뷰로 아래 결정을 확정했다.
## 12. 요구사항 추적표
| 요구사항 범위 | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|---|---:|---|---|---|
| `DMROOM-001~004` | 1 | `P1-T1` | `UserCreatorChatControllerMappingTest`, `UserCreatorChatServiceTest` | request alias 계약 대조 |
| `DMROOM-005~007` | 1 | `P1-T2` | `UserCreatorChatServiceTest`, `UserCreatorChatServiceIntegrationTest` | 기존 메시지 발송 흐름 분리 확인 |
## 13. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|---|---|---|---|---|---|
| 2026-09-14 | `DEC-001` | 확정 | `/create`는 방 생성/조회만 처리하고 첫 메시지는 발송하지 않는다. | 사용자 선택 A | `DMROOM-007`, `P1-T2` |
| 2026-09-14 | `DEC-002` | 확정 | 수신자는 `MemberRole`로 제한하지 않고 활성/AI 아님/자기 자신 아님/차단 정책으로 제한한다. | 사용자 선택 B, 기존 `validateRecipient` 정책 | `DMROOM-005`, `P1-T2` |
| 2026-09-14 | `DEC-003` | 확정 | 신규 표준 필드는 `recipientId`로 하고 기존 `creatorId`는 alias로 유지한다. | 기능 의미는 A가 맞지만 기존 클라이언트 동작 보존 필요 | `DMROOM-001~004`, `P1-T1` |
## 14. 변경 관리
- 코드 구현 전 `plan-task.md`의 체크박스와 Goal 단위를 따른다.
- 구현 중 API 계약이 바뀌면 이 PRD의 Decision Log를 먼저 갱신한다.
- 기존 `creatorId` alias를 제거하려면 별도 PRD와 클라이언트 마이그레이션 계획을 작성한다.