docs(creator-channel): 커뮤니티 탭 Phase 1 기록을 갱신한다
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
- `size`: fallback 보정 후 실제 적용된 page size
|
||||
- `hasNext`: 다음 page 존재 여부
|
||||
- community post item:
|
||||
- `postId`, `creatorId`, `creatorNickname`, `createdAtUtc`, `content`, `imageUrl`, `audioUrl`, `price`, `isCommentAvailable`, `likeCount`, `commentCount`, `isPinned`
|
||||
- `postId`, `creatorId`, `creatorNickname`, `creatorProfileUrl`, `createdAtUtc`, `content`, `imageUrl`, `audioUrl`, `price`, `isCommentAvailable`, `existOrdered`, `likeCount`, `commentCount`, `isPinned`
|
||||
- 공개 게시글 기준: `CreatorCommunity.isActive == true`, `CreatorCommunity.member.id == creatorId`, `CreatorCommunity.member.isActive == true`.
|
||||
- 성인 콘텐츠는 조회자의 성인 콘텐츠 노출 정책이 false이면 목록과 count에서 제외한다.
|
||||
- 성인 콘텐츠 필터는 구매 여부보다 우선한다. 조회자가 19금 게시글을 구매했거나 작성자여도 성인 콘텐츠 노출 정책이 false이면 제외한다.
|
||||
@@ -35,14 +35,19 @@
|
||||
- 일반 게시글 사이의 정렬은 `createdAt desc`, `id desc`다.
|
||||
- 고정 게시글과 일반 게시글은 하나의 목록으로 페이징한다.
|
||||
- `communityPostCount`는 고정 게시글과 일반 게시글을 모두 포함한 전체 개수다.
|
||||
- `createdAtUtc`는 게시글 생성 시각을 UTC 기준 ISO-8601 문자열로 내려준다.
|
||||
- `imageUrl`은 `CreatorCommunity.imagePath`를 `String?.toCdnUrl(cloudFrontHost)`로 변환한다. 경로가 없거나 blank이면 `null`이다.
|
||||
- `createdAtUtc`는 게시글 생성 시각을 UTC 기준 ISO-8601 문자열로 내려준다. 구현 전 재사용 가능한 `toUtcIso` 확장함수를 검색하고, public 확장함수가 있으면 신규 생성 없이 import해서 사용한다.
|
||||
- 문서 작성 시점 확인 결과 `toUtcIso`는 일부 DTO의 private/internal 확장함수로만 존재하고, 공용 확장 파일인 `kr.co.vividnext.sodalive.extensions.LocalDateTimeExtensions.kt`에는 없다. 구현 시점에도 public 확장함수가 없으면 이 공용 확장 파일에 `fun LocalDateTime.toUtcIso(): String`을 추가하고 커뮤니티 DTO에서 import한다.
|
||||
- `creatorProfileUrl`은 `CreatorCommunity.member.profileImage`를 `String?.toCdnUrl(cloudFrontHost)`로 변환하고, 없으면 기존 홈 API의 기본 프로필 이미지 URL을 내려준다.
|
||||
- `existOrdered`는 조회자가 게시글 작성자이면 `true`, 조회자가 유효 구매 내역을 가지고 있으면 `true`, 그 외에는 `false`로 내려준다.
|
||||
- `imageUrl`은 `CreatorCommunity.imagePath`가 있고 이미지 접근 권한이 있을 때만 `String?.toCdnUrl(cloudFrontHost)`로 변환한다. 경로가 없거나 blank이면 `null`이다.
|
||||
- legacy 커뮤니티 목록은 유료 미구매 게시글의 이미지를 노출했지만, 커뮤니티 탭 API는 유료 미구매 게시글의 `imageUrl`도 `audioUrl`과 동일하게 `null`로 내려준다.
|
||||
- 이미지는 signed URL을 생성하지 않고 기존 CDN URL 조합 정책만 사용한다.
|
||||
- `audioUrl`은 `CreatorCommunity.audioPath`가 있고 접근 권한이 있을 때만 `AudioContentCloudFront.generateSignedURL(resourcePath, 1000 * 60 * 30)` 결과를 내려준다.
|
||||
- 오디오 접근 권한:
|
||||
- 이미지/오디오 접근 권한:
|
||||
- 무료 게시글이면 접근 가능
|
||||
- 유료 게시글이고 조회자가 게시글 작성자이면 접근 가능
|
||||
- 유료 게시글이고 조회자가 `CanUsage.PAID_COMMUNITY_POST`, `isRefund == false` 구매 내역을 가지면 접근 가능
|
||||
- 그 외에는 `audioUrl == null`
|
||||
- 그 외에는 `imageUrl == null`, `audioUrl == null`
|
||||
- 유료 게시글 본문은 기존 홈 API/legacy 커뮤니티 정책과 같은 마스킹을 적용한다.
|
||||
- 접근 가능하면 원문
|
||||
- 접근 불가이고 길이가 15 code point 초과이면 앞 15 code point + `...`
|
||||
@@ -87,6 +92,7 @@
|
||||
### 기존 파일 확인/재사용
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelPage.kt`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/common/domain/CdnUrlExtensions.kt`
|
||||
- Modify/Verify: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunity.kt`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt`
|
||||
@@ -108,10 +114,9 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPost
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityTab
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
data class CreatorChannelCommunityTabResponse(
|
||||
val communityPostCount: Int,
|
||||
@@ -138,6 +143,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
val postId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileUrl: String,
|
||||
val createdAtUtc: String,
|
||||
val content: String,
|
||||
val imageUrl: String?,
|
||||
@@ -145,6 +151,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
val price: Int,
|
||||
@JsonProperty("isCommentAvailable")
|
||||
val isCommentAvailable: Boolean,
|
||||
val existOrdered: Boolean,
|
||||
val likeCount: Int,
|
||||
val commentCount: Int,
|
||||
@JsonProperty("isPinned")
|
||||
@@ -156,12 +163,14 @@ data class CreatorChannelCommunityPostResponse(
|
||||
postId = post.postId,
|
||||
creatorId = post.creatorId,
|
||||
creatorNickname = post.creatorNickname,
|
||||
creatorProfileUrl = post.creatorProfileUrl,
|
||||
createdAtUtc = post.createdAt.toUtcIso(),
|
||||
content = post.content,
|
||||
imageUrl = post.imageUrl,
|
||||
audioUrl = post.audioUrl,
|
||||
price = post.price,
|
||||
isCommentAvailable = post.isCommentAvailable,
|
||||
existOrdered = post.existOrdered,
|
||||
likeCount = post.likeCount,
|
||||
commentCount = post.commentCount,
|
||||
isPinned = post.isPinned
|
||||
@@ -169,10 +178,6 @@ data class CreatorChannelCommunityPostResponse(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LocalDateTime.toUtcIso(): String {
|
||||
return atOffset(ZoneOffset.UTC).toInstant().toString()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -272,7 +277,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
|
||||
### Phase 1: 커뮤니티 도메인 계약과 순수 정책 추가
|
||||
|
||||
- [ ] **Task 1.1: `CreatorChannelCommunityQueryPolicy`와 domain/port 계약 테스트 작성**
|
||||
- [x] **Task 1.1: `CreatorChannelCommunityQueryPolicy`와 domain/port 계약 테스트 작성**
|
||||
- Files:
|
||||
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicyTest.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt`
|
||||
@@ -294,6 +299,10 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest"`
|
||||
- 기대 결과: `BUILD SUCCESSFUL`
|
||||
- REFACTOR: 정책 클래스에는 DB, Spring MVC, API DTO 의존성을 넣지 않는다.
|
||||
- 검증 기록:
|
||||
- RED: `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest"` 실행 결과 `CreatorChannelCommunityQueryPolicy`, domain, port 미구현 심볼로 `compileTestKotlin` 실패를 확인했다.
|
||||
- GREEN: 같은 명령 재실행 결과 `BUILD SUCCESSFUL`을 확인했다.
|
||||
- 범위: Phase 1의 순수 정책, domain model, port 계약, 계약 테스트만 추가했고 DB/Spring MVC/API DTO 의존성은 넣지 않았다.
|
||||
|
||||
### Phase 2: QueryDSL repository 분리와 조회 정책 구현
|
||||
|
||||
@@ -316,6 +325,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- 차단 관계에 걸린 댓글 작성자의 댓글은 `commentCount`에서 제외된다.
|
||||
- 유효 구매 내역은 `CanUsage.PAID_COMMUNITY_POST`, `UseCan.member.id == viewerId`, `UseCan.communityPost.id == postId`, `UseCan.isRefund == false`다.
|
||||
- 같은 게시글에 구매 내역이 중복되어도 list item은 중복되지 않는다.
|
||||
- 조회자가 게시글 작성자이면 구매 내역이 없어도 `existOrdered == true`다.
|
||||
- RED 실행:
|
||||
- `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest"`
|
||||
- 기대 결과: repository 미구현으로 컴파일 실패 또는 테스트 실패
|
||||
@@ -344,6 +354,9 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- `MemberContentPreferenceService`와 `isAdultVisibleByPolicy` 결과를 port의 `canViewAdultContent`로 전달한다.
|
||||
- 이미지 path는 `toCdnUrl(cloudFrontHost)`로 변환하고 blank path는 `null`이다.
|
||||
- 작성자 프로필 path가 없으면 기존 홈 API의 기본 프로필 URL 정책을 적용한다.
|
||||
- 조회자가 게시글 작성자이면 구매 내역이 없어도 `existOrdered == true`로 조립한다.
|
||||
- 무료 이미지, 구매한 유료 이미지, 작성자 본인 유료 이미지는 CDN URL을 사용하고 signed URL을 생성하지 않는다.
|
||||
- 미구매 유료 이미지는 `imageUrl == null`이다.
|
||||
- 무료 오디오, 구매한 유료 오디오, 작성자 본인 유료 오디오는 `AudioContentCloudFront.generateSignedURL(audioPath, 1000 * 60 * 30)` 결과를 사용한다.
|
||||
- 미구매 유료 오디오는 signed URL을 생성하지 않고 `audioUrl == null`이다.
|
||||
- 유료 미구매 본문은 policy의 마스킹 결과를 사용한다.
|
||||
@@ -355,7 +368,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- GREEN 실행:
|
||||
- `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest"`
|
||||
- 기대 결과: `BUILD SUCCESSFUL`
|
||||
- REFACTOR: signed URL 생성은 접근 가능한 오디오 path가 있을 때만 호출하고, service는 API DTO를 반환하지 않는다.
|
||||
- REFACTOR: signed URL 생성은 접근 가능한 오디오 path가 있을 때만 호출하고, 이미지는 signed URL 생성 대상에서 제외한다. service는 API DTO를 반환하지 않는다.
|
||||
|
||||
### Phase 4: 홈 API 커뮤니티 조회 로직을 새 도메인으로 연결
|
||||
|
||||
@@ -400,11 +413,14 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- [ ] **Task 5.1: response DTO와 facade 테스트 작성**
|
||||
- Files:
|
||||
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt`
|
||||
- Modify/Verify: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt`
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt`
|
||||
- RED: 아래 케이스를 테스트로 먼저 작성한다.
|
||||
- facade는 `CreatorChannelCommunityQueryService.getCommunityTab` 결과를 `CreatorChannelCommunityTabResponse`로 변환한다.
|
||||
- `createdAtUtc`는 UTC ISO-8601 문자열이다.
|
||||
- `createdAtUtc` 변환은 재사용 가능한 `toUtcIso` 확장함수가 있으면 기존 확장함수를 import해서 사용하고, 없으면 `LocalDateTimeExtensions.kt`에 공용 확장함수를 추가해 사용한다.
|
||||
- `creatorProfileUrl`, `existOrdered`가 응답에 포함된다.
|
||||
- `imageUrl == null`, `audioUrl == null`이 그대로 응답된다.
|
||||
- `@JsonProperty`로 `isCommentAvailable`, `isPinned`, `hasNext` 필드명이 유지된다.
|
||||
- RED 실행:
|
||||
@@ -424,7 +440,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- 비회원 요청은 `401 Unauthorized`다.
|
||||
- 인증 회원 요청은 `GET /api/v2/creator-channels/{creatorId}/community`를 호출하고 `creatorId`, `page`, `size`, `viewer`를 facade에 전달한다.
|
||||
- `page=-1`, `size=100` 같은 값은 controller에서 거부하지 않고 facade로 전달한다.
|
||||
- 성공 응답은 `success=true`, `data.communityPostCount`, `data.communityPosts[0].postId`, `data.communityPosts[0].isCommentAvailable`, `data.communityPosts[0].isPinned`, `data.page`, `data.size`, `data.hasNext`를 포함한다.
|
||||
- 성공 응답은 `success=true`, `data.communityPostCount`, `data.communityPosts[0].postId`, `data.communityPosts[0].creatorProfileUrl`, `data.communityPosts[0].existOrdered`, `data.communityPosts[0].isCommentAvailable`, `data.communityPosts[0].isPinned`, `data.page`, `data.size`, `data.hasNext`를 포함한다.
|
||||
- RED 실행:
|
||||
- `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest"`
|
||||
- 기대 결과: controller 미구현으로 컴파일 실패 또는 테스트 실패
|
||||
@@ -445,6 +461,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- `page=-1`, `size=10` 요청은 `page=0`, `size=20`으로 fallback된다.
|
||||
- 성인 콘텐츠 노출 정책이 false인 조회자는 19금 게시글을 count와 list에서 받지 않는다.
|
||||
- 성인 콘텐츠 노출 정책이 false인 조회자가 19금 게시글을 구매했더라도 count와 list에서 받지 않는다.
|
||||
- 구매한 유료 게시글의 `imageUrl`은 CDN URL이고 signed URL이 아니며, 미구매 유료 게시글의 `imageUrl`은 `null`이다.
|
||||
- 구매한 유료 게시글의 `audioUrl`은 signed URL 형태이고, 미구매 유료 게시글의 `audioUrl`은 `null`이다.
|
||||
- 이미지가 없는 게시글의 `imageUrl`은 `null`이다.
|
||||
- RED 실행:
|
||||
@@ -493,7 +510,7 @@ data class CreatorChannelCommunityPostRecord(
|
||||
|
||||
1. Phase 1에서 순수 정책과 domain/port 계약을 먼저 고정한다.
|
||||
2. Phase 2에서 QueryDSL repository를 새 커뮤니티 도메인으로 분리한다.
|
||||
3. Phase 3에서 service가 인증/성인/차단/URL/signed URL/마스킹 정책을 조립하게 한다.
|
||||
3. Phase 3에서 service가 인증/성인/차단/CDN URL/오디오 signed URL/마스킹 정책을 조립하게 한다.
|
||||
4. Phase 4에서 홈 API의 커뮤니티 조회를 새 도메인으로 연결하고 홈 repository의 커뮤니티 쿼리를 제거한다.
|
||||
5. Phase 5에서 공개 API DTO/facade/controller를 추가한다.
|
||||
6. Phase 6에서 커뮤니티 탭 E2E와 홈 API 회귀를 확인한다.
|
||||
@@ -507,3 +524,4 @@ data class CreatorChannelCommunityPostRecord(
|
||||
- 2026-06-21: 문서 작성 검증 - placeholder와 모호한 문구 검색 결과 0건.
|
||||
- 2026-06-21: 문서 변경 whitespace 검증 - `git diff --check` 실행 결과 출력 없음, exit code 0.
|
||||
- 2026-06-21: 문서 유지보수 규칙 검증 - sandbox 내부 `./gradlew tasks --all`은 `~/.gradle` lock 파일 접근 제한으로 실패했고, 승인 실행으로 재시도한 `./gradlew tasks --all`은 `BUILD SUCCESSFUL` 확인.
|
||||
- 2026-06-21: Phase 1 Task 1.1 검증 - RED focused test는 미구현 심볼로 `compileTestKotlin` 실패, GREEN focused test는 `BUILD SUCCESSFUL` 확인.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 커뮤니티 게시글 조회 로직은 홈 화면과 커뮤니티 탭에서 모두 쓰일 수 있으므로, 하나의 커뮤니티 조회 도메인으로 분리되어야 한다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 커뮤니티 게시글은 전체 개수와 목록에서 모두 제외되어야 한다.
|
||||
- legacy `/creator-community` 목록 조회는 구매 내역 조건과 성인 필터 조건이 섞일 수 있으므로, `isAdult=false` 조회에서 구매한 19금 게시글이 개수나 목록에 포함되지 않도록 새 v2 조회 정책에서 명확히 보장해야 한다.
|
||||
- legacy 커뮤니티 목록은 유료 게시글을 구매하지 않은 조회자에게도 게시글 이미지를 노출했지만, 커뮤니티 탭 API는 유료 미구매 게시글의 이미지도 오디오와 동일하게 `null`로 내려줘야 한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -18,12 +19,12 @@
|
||||
- 크리에이터 채널 커뮤니티 탭 조회 API를 제공한다.
|
||||
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/community`로 한다.
|
||||
- 클라이언트에서 호출하는 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.community` 하위 조립 계층에 둔다.
|
||||
- 커뮤니티 게시글 목록, 전체 개수, 구매 여부, 좋아요 수, 댓글 수, 성인 콘텐츠 노출, 유료 오디오 접근 정책은 API 패키지 밖 커뮤니티 도메인 패키지에 둔다.
|
||||
- 커뮤니티 게시글 목록, 전체 개수, 구매 여부, 좋아요 수, 댓글 수, 성인 콘텐츠 노출, 유료 이미지/오디오 접근 정책은 API 패키지 밖 커뮤니티 도메인 패키지에 둔다.
|
||||
- 크리에이터 채널 홈 API와 커뮤니티 탭 API는 동일한 커뮤니티 조회 도메인을 사용한다.
|
||||
- 응답에는 조회 가능한 커뮤니티 게시글 전체 개수, 게시글 목록, page, size, hasNext를 포함한다.
|
||||
- 게시글 목록 item에는 게시글 id, 크리에이터 id, 크리에이터 닉네임, 작성 시간 UTC, 게시글 본문, 이미지 URL, 오디오 URL, 가격, 댓글 쓰기 가능 여부, 좋아요 개수, 댓글 개수, pin 여부를 포함한다.
|
||||
- 유료 게시글의 오디오 콘텐츠는 조회자가 구매했거나 게시글 작성자인 경우에만 signed URL로 내려준다.
|
||||
- 유료 게시글을 구매하지 않은 조회자에게는 오디오 콘텐츠 URL을 `null`로 내려준다.
|
||||
- 게시글 목록 item에는 게시글 id, 크리에이터 id, 크리에이터 닉네임, 크리에이터 프로필 이미지 URL, 작성 시간 UTC, 게시글 본문, 이미지 URL, 오디오 URL, 가격, 댓글 쓰기 가능 여부, 구매 여부, 좋아요 개수, 댓글 개수, pin 여부를 포함한다.
|
||||
- 유료 게시글의 이미지와 오디오 콘텐츠는 조회자가 구매했거나 게시글 작성자인 경우에만 내려준다.
|
||||
- 유료 게시글을 구매하지 않은 조회자에게는 이미지 URL과 오디오 콘텐츠 URL을 `null`로 내려준다.
|
||||
- 이미지가 없는 게시글은 `imageUrl`을 `null`로 내려준다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 전체 개수와 목록에서 제외한다.
|
||||
- 페이징 요청값은 기존 오디오/시리즈 탭 API와 같은 보정 규칙을 따른다.
|
||||
@@ -56,8 +57,8 @@
|
||||
- 성인 콘텐츠 노출이 꺼진 사용자는 19금 게시글이 개수와 목록에 포함되지 않기를 원한다.
|
||||
- 사용자는 이미지가 없는 게시글도 정상적으로 목록에서 확인하고 싶다.
|
||||
- 사용자는 구매한 유료 게시글의 오디오 콘텐츠를 재생할 수 있어야 한다.
|
||||
- 구매하지 않은 사용자는 유료 게시글의 오디오 콘텐츠 URL을 받지 않아야 한다.
|
||||
- 앱 클라이언트는 댓글 작성 가능 여부, 좋아요 개수, 댓글 개수, pin 여부를 게시글 item에서 바로 확인하고 싶다.
|
||||
- 구매하지 않은 사용자는 유료 게시글의 이미지 URL과 오디오 콘텐츠 URL을 받지 않아야 한다.
|
||||
- 앱 클라이언트는 크리에이터 프로필 이미지, 댓글 작성 가능 여부, 구매 여부, 좋아요 개수, 댓글 개수, pin 여부를 게시글 item에서 바로 확인하고 싶다.
|
||||
- 서버 개발자는 홈 API와 커뮤니티 탭 API가 동일한 커뮤니티 조회 도메인을 사용한다는 것을 패키지 의존 방향으로 확인하고 싶다.
|
||||
|
||||
---
|
||||
@@ -121,6 +122,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
val postId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileUrl: String,
|
||||
val createdAtUtc: String,
|
||||
val content: String,
|
||||
val imageUrl: String?,
|
||||
@@ -128,6 +130,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
val price: Int,
|
||||
@JsonProperty("isCommentAvailable")
|
||||
val isCommentAvailable: Boolean,
|
||||
val existOrdered: Boolean,
|
||||
val likeCount: Int,
|
||||
val commentCount: Int,
|
||||
@JsonProperty("isPinned")
|
||||
@@ -138,6 +141,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
#### Edge Cases
|
||||
- 조회 가능한 커뮤니티 게시글이 없으면 `communityPostCount`는 `0`, `communityPosts`는 빈 배열, `hasNext`는 `false`로 내려준다.
|
||||
- 이미지가 없는 게시글은 `imageUrl`을 `null`로 내려준다.
|
||||
- 유료 게시글을 구매하지 않았고 게시글 작성자도 아닌 조회자에게는 이미지가 있는 게시글이어도 `imageUrl`을 `null`로 내려준다.
|
||||
- 오디오가 없는 게시글은 `audioUrl`을 `null`로 내려준다.
|
||||
- `isCommentAvailable == false`인 게시글의 `commentCount`는 기존 커뮤니티 목록 정책과 동일하게 `0`으로 내려준다.
|
||||
- Boolean 응답 필드는 Jackson 직렬화 시 `commentAvailable`, `pinned`로 바뀌지 않고 `isCommentAvailable`, `isPinned`로 내려가야 한다.
|
||||
@@ -157,7 +161,9 @@ data class CreatorChannelCommunityPostResponse(
|
||||
- 목록은 `page`, `size` 기준으로 페이징 조회한다.
|
||||
- 다음 page 존재 여부는 `size + 1`개를 조회하거나 동등한 방식으로 판단하되, 응답 목록에는 최대 `size`개만 내려준다.
|
||||
- `createdAtUtc`는 게시글 생성 시간을 UTC 기준 ISO-8601 문자열로 내려준다.
|
||||
- `imageUrl`은 커뮤니티 게시글 이미지 path가 있으면 기존 CDN URL 조합 정책으로 내려준다.
|
||||
- `creatorProfileUrl`은 크리에이터 프로필 이미지 path가 있으면 기존 CDN URL 조합 정책으로 내려주고, 없으면 기본 프로필 이미지 URL을 내려준다.
|
||||
- `existOrdered`는 조회자가 게시글 작성자이면 `true`, 조회자가 유효 구매 내역을 가지고 있으면 `true`, 그 외에는 `false`로 내려준다.
|
||||
- `imageUrl`은 커뮤니티 게시글 이미지 path가 있고 조회자가 해당 게시글의 유료 미디어에 접근할 수 있을 때만 기존 CDN URL 조합 정책으로 내려준다.
|
||||
- `likeCount`는 활성 좋아요 수를 기준으로 계산한다.
|
||||
- `commentCount`는 조회자가 볼 수 있는 활성 최상위 댓글 수를 기준으로 계산한다.
|
||||
- 댓글 수 계산에는 기존 커뮤니티 댓글의 차단 관계와 비밀 댓글 노출 정책을 적용한다.
|
||||
@@ -168,24 +174,32 @@ data class CreatorChannelCommunityPostResponse(
|
||||
- 게시글 작성자가 조회자인 경우에도 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 제외한다.
|
||||
- 좋아요나 댓글이 없는 게시글은 `likeCount`, `commentCount`를 `0`으로 내려준다.
|
||||
|
||||
### Feature D. 유료 오디오 콘텐츠 접근 정책
|
||||
### Feature D. 유료 이미지와 오디오 콘텐츠 접근 정책
|
||||
|
||||
#### Requirements
|
||||
- 커뮤니티 게시글에 이미지 path가 없으면 `imageUrl`은 `null`이다.
|
||||
- 커뮤니티 게시글에 오디오 path가 없으면 `audioUrl`은 `null`이다.
|
||||
- 무료 게시글에 이미지 path가 있으면 CDN URL을 내려준다.
|
||||
- 무료 게시글에 오디오 path가 있으면 signed URL을 내려준다.
|
||||
- 유료 게시글에 이미지 path가 있고 조회자가 해당 게시글을 구매했으면 CDN URL을 내려준다.
|
||||
- 유료 게시글에 오디오 path가 있고 조회자가 해당 게시글을 구매했으면 signed URL을 내려준다.
|
||||
- 유료 게시글에 이미지 path가 있고 조회자가 게시글 작성자이면 CDN URL을 내려준다.
|
||||
- 유료 게시글에 오디오 path가 있고 조회자가 게시글 작성자이면 signed URL을 내려준다.
|
||||
- 유료 게시글에 이미지 path가 있지만 조회자가 구매하지 않았고 게시글 작성자도 아니면 `imageUrl`은 `null`이다.
|
||||
- 유료 게시글에 오디오 path가 있지만 조회자가 구매하지 않았고 게시글 작성자도 아니면 `audioUrl`은 `null`이다.
|
||||
- signed URL 생성은 기존 `AudioContentCloudFront.generateSignedURL` 방식을 재사용한다.
|
||||
- signed URL 만료 시간은 legacy 커뮤니티 목록 정책과 동일하게 30분을 기본으로 한다.
|
||||
- 이 이미지 제한 정책은 legacy `/creator-community` 목록의 기존 이미지 노출 동작과 다르며, 커뮤니티 탭 API에서는 오디오 접근 정책과 동일하게 적용한다.
|
||||
- 이미지 URL은 signed URL로 만들지 않고 기존 CDN URL 조합 정책만 사용한다.
|
||||
- 오디오 signed URL 생성은 기존 `AudioContentCloudFront.generateSignedURL` 방식을 재사용한다.
|
||||
- 오디오 signed URL 만료 시간은 legacy 커뮤니티 목록 정책과 동일하게 30분을 기본으로 한다.
|
||||
- 유료 게시글 본문은 기존 크리에이터 채널 홈 API의 유료 커뮤니티 본문 마스킹 정책을 따른다.
|
||||
- 유료 게시글 오디오 접근 여부는 `CanUsage.PAID_COMMUNITY_POST`의 유효 구매 내역을 기준으로 판단한다.
|
||||
- 유료 게시글 이미지/오디오 접근 여부는 `CanUsage.PAID_COMMUNITY_POST`의 유효 구매 내역을 기준으로 판단한다.
|
||||
- 환불된 구매 내역은 접근 가능 구매로 보지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 조회자가 구매했더라도 성인 콘텐츠 노출 정책이 false인 19금 게시글은 목록에 포함되지 않으므로 signed URL도 내려주지 않는다.
|
||||
- 조회자가 구매했더라도 성인 콘텐츠 노출 정책이 false인 19금 게시글은 목록에 포함되지 않으므로 이미지 URL과 오디오 signed URL도 내려주지 않는다.
|
||||
- 구매 내역이 중복으로 있어도 응답 item은 게시글 1개로 중복 없이 내려준다.
|
||||
- signed URL 생성 대상 path가 blank이면 `audioUrl`은 `null`로 내려준다.
|
||||
- 이미지 path가 blank이면 `imageUrl`은 `null`로 내려준다.
|
||||
- 오디오 signed URL 생성 대상 path가 blank이면 `audioUrl`은 `null`로 내려준다.
|
||||
|
||||
### Feature E. 커뮤니티 조회 도메인 분리
|
||||
|
||||
@@ -220,6 +234,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
- 페이징 응답은 기존 오디오/시리즈 탭 API와 같은 `page`, `size`, `hasNext` 패턴을 따른다.
|
||||
- 이미지 URL은 기존 `String?.toCdnUrl(cloudFrontHost)` 방식과 같은 CDN URL 조합 정책을 따른다.
|
||||
- 오디오 URL은 콘텐츠 CloudFront signed URL 생성 정책을 따른다.
|
||||
- `createdAtUtc` 변환은 기존에 재사용 가능한 `toUtcIso` 확장함수가 있으면 신규 private 확장함수를 만들지 않고 기존 확장함수를 사용한다.
|
||||
- 날짜 응답은 UTC 기준 ISO-8601 문자열로 내려준다.
|
||||
|
||||
---
|
||||
@@ -229,7 +244,7 @@ data class CreatorChannelCommunityPostResponse(
|
||||
- 커뮤니티 탭 API 응답 시간
|
||||
- 커뮤니티 탭 추가 로딩 요청 건수
|
||||
- 성인 콘텐츠 노출 정책이 false인 조회에서 19금 게시글이 개수와 목록에 포함되지 않는 테스트 통과 여부
|
||||
- 유료 오디오 콘텐츠 signed URL/null 처리 테스트 통과 여부
|
||||
- 유료 게시글 이미지 CDN URL/null 처리와 오디오 signed URL/null 처리 테스트 통과 여부
|
||||
- 홈 API 커뮤니티 요약 조회 회귀 테스트 통과 여부
|
||||
- `v2.creator.channel.community` 도메인 패키지의 `v2.api.*` import 검색 결과 0건 여부
|
||||
|
||||
|
||||
Reference in New Issue
Block a user