163 lines
16 KiB
Markdown
163 lines
16 KiB
Markdown
# 채널 후원 메시지 항목 숨김 PRD
|
|
|
|
## 문서 정보
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 문서 상태 | 서버 구현·기능 범위 검증·최종 리뷰 완료. 운영 DDL/프론트엔드 연동 미수행 |
|
|
| 작성일 | 2026-11-05 |
|
|
| 최종 수정일 | 2026-11-05 |
|
|
| 대상 제품 | 크리에이터 채널 후원 메시지 숨김 |
|
|
| 작성자·결정권자 | 요구사항은 사용자 인터뷰로 확정, 기술안은 문서 작성자 제안 |
|
|
| 관련 API Contract | 이 문서 §8, `DEC-006`으로 구현 승인 |
|
|
| 관련 구현 계획 | [plan-task.md](plan-task.md) |
|
|
| 관련 review | 없음 |
|
|
|
|
## 1. 개요
|
|
|
|
크리에이터는 자신의 채널에 표시되는 단일 후원 항목을 숨길 수 있다. 숨김은 원본 후원 기록을 보존하는 표시 조정이며, 채널 홈의 `channelDonations`와 후원 탭의 `donations`에서만 항목을 제외한다. 최초에는 문서만 작성했으며, 이후 사용자의 "문서대로 진행" 승인(`DEC-006`)에 따라 구현했다. 현재 실행 상태와 검증 결과는 [plan-task.md](plan-task.md)를 기준으로 한다.
|
|
|
|
## 2. 문제와 목표
|
|
|
|
채널에 표시된 후원 메시지가 부적절하거나 더 이상 게시하기 적절하지 않을 수 있지만, 이를 제거하면서 후원 및 정산 이력을 변경해서는 안 된다. 크리에이터가 해당 후원 항목을 숨기면 모든 조회자에게 두 V2 채널 표시 위치에서 보이지 않고 원본 기록과 재무 효과는 그대로 남아야 한다.
|
|
|
|
## 3. 목표와 범위 밖
|
|
|
|
### 목표
|
|
|
|
- 채널의 소유 CREATOR만 해당 채널의 후원 항목 하나를 숨길 수 있다.
|
|
- 숨긴 항목은 채널 소유자와 후원자를 포함한 모든 조회자에게 두 V2 표시 목록에서 제외된다.
|
|
- `donationCount`는 숨기지 않은 후원 항목만 센다.
|
|
- 데이터베이스 원본 값, 기여도 랭킹, 정산 및 사용한 can은 그대로 보존된다.
|
|
|
|
### 범위 밖
|
|
|
|
- 복원, 물리 삭제, 후원 취소, 환불, can 사용 취소 또는 정산 역분개.
|
|
- legacy 프로필/후원 목록 API의 숨김 처리. 해당 경로는 같은 원본 기록을 계속 노출할 수 있다. 이 기능은 전역 삭제나 전체 서비스 비노출을 약속하지 않는다.
|
|
- 관리자 추가 권한, 후원자 권한, 다른 크리에이터 권한, 일반 회원 권한.
|
|
- 숨김 사유, 수행자/시각 이력, UI 이력, 일괄 API, 실시간 push.
|
|
- 프론트엔드 구현 및 화면 디자인. 별도 프론트엔드 작업은 소유자 전용 ID로 숨김 API를 호출하고 두 표시 경로를 다시 조회해야 한다.
|
|
|
|
## 4. 사용자와 권한
|
|
|
|
| 주체 | 숨김 가능 여부 | 두 V2 목록의 숨긴 항목 |
|
|
|---|---:|---|
|
|
| 해당 채널의 소유 CREATOR | 가능 | 제외 |
|
|
| 후원자 본인 | 불가 | 제외 |
|
|
| 다른 CREATOR | 불가 | 제외 |
|
|
| 일반 회원 | 불가 | 제외 |
|
|
| 관리자 | 이 요청으로 추가 권한 없음 | 제외 |
|
|
|
|
서버는 인증 회원, `CREATOR` role, 요청 경로의 `creatorId`와 인증 회원의 채널 소유권, 후원 항목과 동일 채널 소속을 검증한다. 존재하지 않는 항목 또는 다른 채널의 ID를 안전하게 거부하고 어떠한 기록도 변경하지 않는다. 현재 `SodaException` 및 공통 오류 handler를 따른다. 프로젝트 관례상 도메인 실패는 HTTP 200 응답의 `success=false` envelope일 수 있으며, 별도의 REST 403/404 계약을 만들지 않는다.
|
|
|
|
## 5. 핵심 흐름 (기술 제안 포함)
|
|
|
|
1. 소유 CREATOR가 채널 홈 또는 후원 탭에서 항목을 선택한다.
|
|
2. 프론트엔드는 소유자에게만 응답되는 안정적인 `donationId`로 숨김 요청을 보낸다.
|
|
3. 서버가 인증, role, 채널 소유권 및 후원 소속을 확인한 뒤 해당 항목의 숨김 상태만 기록한다.
|
|
4. 같은 소유자가 이미 숨긴 항목을 다시 요청해도 성공하는 멱등 동작을 제공한다.
|
|
5. 프론트엔드는 홈과 후원 탭을 다시 조회한다. 숨긴 항목은 양쪽에서 보이지 않으며 count도 감소한다.
|
|
|
|
## 6. 제품 요구사항
|
|
|
|
| ID | 상태 | 요구사항 | 수용 기준 |
|
|
|---|---|---|---|
|
|
| `CDH-001` | 확정 | 소유 CREATOR는 채널 후원 한 항목을 숨긴다. | ID로 단일 항목을 지정하며 소유권과 채널 소속이 모두 일치할 때만 성공한다. |
|
|
| `CDH-002` | 확정 | 숨김 상태는 soft-hide다. | DB 행, ID, can, 메시지, donor/creator 연결, 생성 시각, `isSecret`이 보존된다. |
|
|
| `CDH-003` | 확정 | 숨긴 항목은 모든 사용자에게 숨긴다. | V2 홈 `channelDonations` 및 V2 후원 탭 `donations`에서 creator와 donor 본인 포함 전원에게 제외된다. |
|
|
| `CDH-004` | 확정 | 후원 탭의 `donationCount`는 보이는 항목 수다. | 숨김 항목은 count에서 빠지며 pagination 및 `hasNext`도 같은 숨김 제외 조건을 쓴다. |
|
|
| `CDH-005` | 확정 | 랭킹과 재무 효과는 보존한다. | 기존 랭킹, 정산, 원본 can 사용 및 후원 기록의 값/행이 달라지지 않는다. |
|
|
| `CDH-006` | 확정 | 권한은 소유 CREATOR에 한정한다. | donor, 타 CREATOR, USER, ADMIN 요청은 거부되며 기록 변화가 없다. |
|
|
| `CDH-007` | 확정 | 숨김은 지정된 두 V2 표시 경로만 바꾼다. | legacy 프로필/후원 목록은 변경하지 않으며 같은 행을 계속 반환할 수 있음을 명시한다. |
|
|
| `CDH-008` | 확정 | 현재 공개/비밀 후원 표시 정책을 유지한다. | 숨기지 않은 공개 및 비밀 항목은 현재 `isSecret` 마스킹 규칙으로 계속 표시된다. |
|
|
| `CDH-009` | 작성자 기술 초안 | 숨김 재요청은 안전하게 성공하도록 제안한다. | 같은 소유자의 중복 숨김 요청은 성공하며 금융 효과를 재적용하지 않는다. |
|
|
|
|
## 7. 기술 상태와 근거
|
|
|
|
다음 기술 선택은 최초 **AUTHOR TECHNICAL DRAFT**로 작성했고 `DEC-006`으로 구현 승인됐다. 아래 제안 표현은 작성 당시의 결정 근거이며 현재 구현 계약으로 사용한다.
|
|
|
|
- 변경 전 엔티티 `ChannelDonationMessage`는 `BaseEntity`의 ID/시간을 상속하며 숨김 필드가 없었다. 구현은 boolean `isHidden=false`를 추가하고 MySQL의 `is_hidden TINYINT(1) NOT NULL DEFAULT 0 COMMENT '채널 후원 표시 숨김 여부'`를 사용한다. [운영 반영용 DDL](channel_donation_message_is_hidden_ddl.sql)을 작성했으며 운영 DB에는 실행하지 않았다.
|
|
- 기존 donation 탭 QueryDSL은 채널과 KST 월 조건으로 count와 page 조회를 나눠 수행하고, 홈 QueryDSL은 같은 기간의 최신 8건을 조회한다. 숨김 조건은 각 조회 경로의 `offset/limit` 적용 전에 둔다. 홈은 기존 limit 8을 유지해 숨긴 항목 다음의 표시 가능 항목으로 최대 8개를 채운다.
|
|
- donation 탭의 count와 list가 동일한 숨김 제외 predicate를 사용한다. legacy query, 랭킹/정산/금융 query에는 전역 filter를 추가하지 않는다. Hibernate 전역 `@Where`/`@SQLDelete`는 사용하지 않는다.
|
|
- 현재 홈/탭 후원 응답은 후원 항목의 ID와 donor member ID를 노출하지 않으며 기존 테스트도 ID 부재를 검증한다. 숨김 동작을 위해 안정적인 `donationId`를 소유 CREATOR 응답에만 nullable/생략 방식으로 추가하는 방안을 제안한다. 일반 조회자, donor 및 다른 사용자의 JSON에는 ID가 없어야 하며 donor `memberId`는 누구에게도 추가 노출하지 않는다. 기존 owner ID 부재 assertion은 owner-only ID 노출 검증으로 갱신하고 non-owner 부재 검증은 유지한다.
|
|
- 제안 endpoint는 bodyless `DELETE /api/v2/creator-channels/{creatorId}/donations/{donationId}`이며 `ApiResponse.ok` envelope에 data 없이 성공을 반환한다. 기존 인증 주입, `SodaException`, message key/i18n 및 handler를 재사용한다. generic REST 403/404를 새로 도입하지 않는다.
|
|
- 공개 후원 및 비밀 후원의 기존 `isSecret` 표시/masking은 visible row에만 그대로 적용한다. 숨긴 항목은 공개/비밀 구분 없이 전체 제외한다.
|
|
|
|
## 8. 제안 API 경계 (AUTHOR TECHNICAL DRAFT)
|
|
|
|
| Method / path | 입력 | 성공 | 실패 및 제한 |
|
|
|---|---|---|---|
|
|
| `DELETE /api/v2/creator-channels/{creatorId}/donations/{donationId}` | 인증 사용자, 경로 ID, body 없음 | 기존 `ApiResponse.ok` envelope, data 없음 | 현재 `SodaException`/handler 및 현행 HTTP-200 `success=false` 도메인 오류 패턴. 비소유/불일치/미존재는 상태 변경 없이 거부 |
|
|
|
|
제안된 application 경계는 재사용 패키지 `kr.co.vividnext.sodalive.v2.creator.channel.donation`의 `HideCreatorChannelDonationService.hide(creatorId: Long, donationId: Long, actor: Member)`다. 출력 port는 `HideCreatorChannelDonationPort.hideOwnedDonation(creatorId: Long, donationId: Long): Boolean`로 채널에 속하는 항목만 원자적으로 숨긴다. 소유자 role/identity 검증은 service 경계에서 수행한다. adapter 패키지 `kr.co.vividnext.sodalive.v2.api.creator.channel.donation`은 controller/facade와 공개 응답의 소유자 전용 `donationId`를 조립한다. 구체 파일과 인터페이스는 plan의 기술 초안에 적는다.
|
|
|
|
숨김 행 재요청은 이미 숨김 상태면 성공으로 끝낸다. 첫 요청과 재요청 모두 can 사용, 환불, 랭킹, 정산을 호출하거나 수정하지 않는다. 소유권·같은 채널 소속·항목 존재 여부가 일치하지 않으면 성공 처리하지 않는다.
|
|
|
|
## 9. 비기능, 보안과 데이터
|
|
|
|
- 사용자 제공 ID는 신뢰하지 않는다. 인증 회원, `CREATOR` role, 채널 소유자 ID 및 donation의 channel ID를 서버에서 대조한다.
|
|
- 다른 채널/존재하지 않는 donation에 대한 거부가 데이터 존재 여부를 불필요하게 노출하지 않도록 기존 도메인 오류 패턴을 검토한다.
|
|
- `isHidden` 외에 `hiddenAt`, `hiddenBy`, reason, history를 추가하지 않는다.
|
|
- 변경 범위 밖의 구형 API가 원본을 노출할 수 있다는 제한을 제품/운영 설명에서 숨기지 않는다.
|
|
- 테스트는 repository/HTTP 통합 근거를 포함하고, 기존 테스트 관례인 JUnit 5, `@SpringBootTest`, 실제 H2/MockMvc 경로를 따른다.
|
|
|
|
## 10. 성공 기준과 추적
|
|
|
|
- [x] `CDH-001`~`CDH-009`의 미래 구현 테스트와 계획 Goal이 연결되어 있다. 구현 및 테스트 통과를 뜻하지 않는다.
|
|
- [x] 역할별 권한, 다른 채널 및 존재하지 않는 ID, 숨김 중복 요청을 HTTP 통합으로 확인했다.
|
|
- [x] 모든 viewer role의 홈/탭 결과에서 숨긴 공개/비밀 행이 제외된다.
|
|
- [x] count, page, `hasNext`, 홈 최신 8개 보충 결과가 숨김 predicate와 일치한다.
|
|
- [x] 원본 행과 값, ranking, settlement, can 사용 내역의 불변을 H2/실제 HTTP로 확인했다. MySQL 자동 수정 시각 보존은 생성 SQL 자기 대입 회귀로 확인했으며 운영 MySQL 실행은 미검증이다.
|
|
- [x] legacy 출력이 변경되지 않고 그 제한을 문서화했다.
|
|
- [x] donor member ID는 추가 노출되지 않으며 donation ID는 소유 CREATOR에만 응답된다.
|
|
|
|
| 요구사항 | 계획 Goal | 주요 완료 증거 |
|
|
|---|---|---|
|
|
| `CDH-001`, `CDH-006` | `P1-T1`, `P1-T2`, `P1-GATE` | 소유 CREATOR 성공, 비소유/위조 ID/미존재 거부 |
|
|
| `CDH-002`, `CDH-005`, `CDH-007` | `P1-T1`, `P3-GATE` | DB 재조회, 실제 랭킹/정산 및 legacy 조회 전후 비교 |
|
|
| `CDH-003`, `CDH-004`, `CDH-008` | `P2-T1`, `P2-T2`, `P2-GATE` | 역할별 숨김 제외, count/page/refill, visible 비밀 후원 마스킹 |
|
|
| `CDH-009` (기술 초안) | `P1-T1`, `P1-T2` | 동일 소유자의 중복 요청 성공 및 추가 변경 없음 |
|
|
| 소유자 전용 ID (기술 초안) | `P2-T1`, `P2-T2` | 양 응답의 동일 persisted ID, 비소유자 JSON의 ID 부재 |
|
|
|
|
## 11. 인터뷰 기록과 미결 사항
|
|
|
|
사용자 인터뷰 결정: 단일 항목 soft-hide, 소유 CREATOR만 권한 보유, 모든 viewer에게 두 V2 목록에서 제외, 복원 없음, donationCount 제외, 랭킹/정산/can 사용 보존, legacy API 불변 및 전역 삭제가 아님.
|
|
|
|
점수: Goal 1.00, Scope 1.00, Constraints 0.90, Success 0.85, Context 0.85. 기존 코드베이스 가중치 `.30/.20/.15/.20/.15` 적용 모호성은 **0.0675 (약 0.07)**다. 제품 질문은 모두 해결되었다. 기술안은 사용자 결정으로 승격하지 않는다.
|
|
|
|
| 분류 | 상태 | 항목 |
|
|
|---|---|---|
|
|
| 제품 질문 | 없음 | 구현 범위·권한·보존 정책·조회 범위는 인터뷰로 확정 |
|
|
| 기술 제안 | `DEC-006` 승인 | §7~§9의 필드, predicate, ID 노출 및 endpoint 계약을 구현 기준으로 확정 |
|
|
| 외부 의존 | 프론트엔드 연동 필요 | 실제 화면의 숨김 조작과 양쪽 데이터 갱신은 별도 프론트엔드 작업. 이 계획의 backend 완료와 화면 기능 완료를 구분 |
|
|
|
|
## 12. Decision Log
|
|
|
|
| 날짜 | ID | 상태 | 결정 | 근거 |
|
|
|---|---|---|---|---|
|
|
| 2026-11-05 | `DEC-001` | 사용자 확정 | 단일 채널 후원 항목을 soft-hide하고 원본 기록/값을 보존한다. | 사용자 인터뷰 |
|
|
| 2026-11-05 | `DEC-002` | 사용자 확정 | 소유 CREATOR만 숨김할 수 있고, 숨긴 항목은 두 V2 목록에서 모든 viewer에게 제외한다. | 사용자 인터뷰 |
|
|
| 2026-11-05 | `DEC-003` | 사용자 확정 | donationCount에서 제외하되 랭킹, 정산, can 사용은 그대로 둔다. | 사용자 인터뷰 |
|
|
| 2026-11-05 | `DEC-004` | 사용자 확정 | legacy 프로필/후원 목록, 복원 및 기타 global hide는 범위 밖이다. | 사용자 인터뷰 |
|
|
| 2026-11-05 | `DEC-005` | 작성자 기술 초안 | `isHidden` 필드, 로컬 query filter, owner-only ID, DELETE route, 멱등 재요청을 제안한다. | 기존 코드 경계와 최소 구현 검토. 사용자 승인 아님 |
|
|
|
|
## 13. 기준 문서와 변경 관리
|
|
|
|
[`docs/20261104_크리에이터채널_비밀후원_익명조회/prd.md`](../20261104_크리에이터채널_비밀후원_익명조회/prd.md)는 현재 visible row의 비밀 여부 마스킹 기준이다. 본 문서는 기존 `isSecret` 마스킹 정책을 변경하지 않고, 숨김 상태의 두 V2 query 제외 및 `donationCount`에 한해 후속 요구를 더한다. 과거 PRD의 목록 DTO ID 비노출 요구는 **owner-only `donationId` 기술안이 승인될 경우에만** 소유 CREATOR 응답 범위에서 좁게 대체한다. 현재 공개 계약을 이미 변경한 것은 아니다. donor member ID 부재 및 모든 non-owner의 ID 부재 요구는 유지한다. 과거 문서는 수정하지 않는다.
|
|
|
|
구현 착수 전 별도의 명시적 요청이 필요하다. 구현 선택을 확정하면 이 PRD의 기술 제안을 승인된 계약과 구분해 Decision Log에 반영하고 계획을 동기화한다.
|
|
|
|
### 구현 승인: 2026-11-05
|
|
|
|
사용자의 "문서대로 진행" 요청으로 이 문서의 기술 제안과 계획을 구현 기준으로 승인했다. 위 초안/미요청 표기는 작성 당시 이력이며 현재는 `isHidden`, 소유 CREATOR 전용 `donationId`, bodyless DELETE 및 멱등 재요청을 구현한다. 프론트엔드·운영 DDL 실행·배포는 포함하지 않는다.
|
|
|
|
| 날짜 | ID | 상태 | 결정 | 근거 |
|
|
|---|---|---|---|---|
|
|
| 2026-11-05 | `DEC-006` | 사용자 승인 | `DEC-005` 기술 초안을 본 구현의 계약으로 사용한다. | "문서대로 진행" |
|
|
|
|
### 서버 구현 완료: 2026-11-05
|
|
|
|
- 관련 기능 19개 클래스 208건이 실패/오류/skip 없이 통과했다. 실제 JWT socket HTTP, 원본 DB·랭킹·정산·legacy 보존과 SQL 시각 보존을 검증했고 최종 리뷰가 승인됐다.
|
|
- `ktlintCheck`, `bootJar` 성공. 전체 suite는 미실행이며 사용자가 `./gradlew test`로 검증한다. 상세 명령·실패 이력·최종 증거는 [plan-task.md](plan-task.md)에 있다.
|
|
- [DDL](channel_donation_message_is_hidden_ddl.sql)은 작성만 했으며 배포 전에 반영해야 한다. 프론트엔드는 소유자 전용 `donationId`로 DELETE를 호출하고 두 조회 결과를 갱신해야 한다. 화면 클라이언트·운영 배포 완료를 의미하지 않는다.
|