feat(creator-channel): 홈 커뮤니티 응답 상태를 추가한다
This commit is contained in:
@@ -12,19 +12,19 @@
|
|||||||
|
|
||||||
| 문서 항목 | 내용 |
|
| 문서 항목 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 상태 | 구현 대기 |
|
| 상태 | 구현 중 |
|
||||||
| 작성일 | 2026-08-10 |
|
| 작성일 | 2026-08-10 |
|
||||||
| 요구사항 기준 | `docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/prd.md` |
|
| 요구사항 기준 | `docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/prd.md` |
|
||||||
| API 기준 | PRD `7. API 계약` |
|
| API 기준 | PRD `7. API 계약` |
|
||||||
| 현재 Phase | Phase 1: 홈 커뮤니티 상태 응답 확장 |
|
| 현재 Phase | Phase 1: 홈 커뮤니티 상태 응답 확장 |
|
||||||
| 현재 활성 Goal | 없음 |
|
| 현재 활성 Goal | 없음 |
|
||||||
| 다음 Goal | `P1-T1` |
|
| 다음 Goal | `P1-GATE` |
|
||||||
|
|
||||||
## 현재 상태
|
## 현재 상태
|
||||||
|
|
||||||
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|---:|---|---:|---|---|
|
|---:|---|---:|---|---|
|
||||||
| 1 | 대기 | `0/1` | `P1-T1` | 없음 |
|
| 1 | 진행 중 | `1/1` | `P1-GATE` | `P1-GATE` 검증 대기 |
|
||||||
|
|
||||||
- 동시에 하나의 미완료 goal만 운용한다.
|
- 동시에 하나의 미완료 goal만 운용한다.
|
||||||
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다.
|
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다.
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
- Produces: `CreatorChannelCommunityPostResponse.isPinned: Boolean`, `CreatorChannelCommunityPostResponse.isCommentAvailable: Boolean`
|
- Produces: `CreatorChannelCommunityPostResponse.isPinned: Boolean`, `CreatorChannelCommunityPostResponse.isCommentAvailable: Boolean`
|
||||||
- JSON: `data.notices[*].isPinned`, `data.notices[*].isCommentAvailable`, `data.communities[*].isPinned`, `data.communities[*].isCommentAvailable`
|
- JSON: `data.notices[*].isPinned`, `data.notices[*].isCommentAvailable`, `data.communities[*].isPinned`, `data.communities[*].isCommentAvailable`
|
||||||
|
|
||||||
- [ ] **RED:** `CreatorChannelHomeControllerTest.shouldReturnCreatorChannelHomeForAuthenticatedMember`의 일반 커뮤니티 fixture를 아래처럼 공지와 반대 상태로 만들고, 네 JSON 경로 assertion을 추가한다. 같은 assertion을 `CreatorChannelHomeEndToEndTest.shouldAssembleCreatorChannelHomeSectionsThroughSingleHttpRequest`에도 추가하되 E2E fixture의 댓글 가능 값은 기존 `true`를 사용한다.
|
- [x] **RED:** `CreatorChannelHomeControllerTest.shouldReturnCreatorChannelHomeForAuthenticatedMember`의 일반 커뮤니티 fixture를 아래처럼 공지와 반대 상태로 만들고, 네 JSON 경로 assertion을 추가한다. 같은 assertion을 `CreatorChannelHomeEndToEndTest.shouldAssembleCreatorChannelHomeSectionsThroughSingleHttpRequest`에도 추가하되 E2E fixture의 댓글 가능 값은 기존 `true`를 사용한다.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
communities = listOf(
|
communities = listOf(
|
||||||
@@ -116,7 +116,7 @@ E2E assertion은 실제 저장값과 조회 조건을 검증한다.
|
|||||||
.andExpect(jsonPath("$.data.communities[0].isCommentAvailable").value(true))
|
.andExpect(jsonPath("$.data.communities[0].isCommentAvailable").value(true))
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **RED 확인:** 다음 focused test를 실행해 `$.data.notices[0].isPinned` 등 신규 JSON 경로에 값이 없어 실패하는지 확인한다.
|
- [x] **RED 확인:** 다음 focused test를 실행해 `$.data.notices[0].isPinned` 등 신규 JSON 경로에 값이 없어 실패하는지 확인한다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest' --no-daemon
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest' --no-daemon
|
||||||
@@ -124,7 +124,7 @@ E2E assertion은 실제 저장값과 조회 조건을 검증한다.
|
|||||||
|
|
||||||
**Expected:** 테스트가 신규 필드 미직렬화로 실패한다. compile 오류나 fixture 오류는 RED 증거로 인정하지 않는다.
|
**Expected:** 테스트가 신규 필드 미직렬화로 실패한다. compile 오류나 fixture 오류는 RED 증거로 인정하지 않는다.
|
||||||
|
|
||||||
- [ ] **GREEN:** `CreatorChannelCommunityPostResponse`에 기존 커뮤니티 탭 DTO와 같은 `@JsonProperty` 관례로 필드를 추가하고 `from`에서 도메인 값을 직접 매핑한다.
|
- [x] **GREEN:** `CreatorChannelCommunityPostResponse`에 기존 커뮤니티 탭 DTO와 같은 `@JsonProperty` 관례로 필드를 추가하고 `from`에서 도메인 값을 직접 매핑한다.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
data class CreatorChannelCommunityPostResponse(
|
data class CreatorChannelCommunityPostResponse(
|
||||||
@@ -171,7 +171,7 @@ data class CreatorChannelCommunityPostResponse(
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **GREEN 확인:** controller test와 홈 E2E를 함께 실행해 공유 DTO 매핑, JSON 이름, 실제 조회값이 모두 통과하는지 확인한다.
|
- [x] **GREEN 확인:** controller test와 홈 E2E를 함께 실행해 공유 DTO 매핑, JSON 이름, 실제 조회값이 모두 통과하는지 확인한다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew test \
|
./gradlew test \
|
||||||
@@ -182,7 +182,7 @@ data class CreatorChannelCommunityPostResponse(
|
|||||||
|
|
||||||
**Expected:** 두 테스트 클래스가 `BUILD SUCCESSFUL`로 종료된다.
|
**Expected:** 두 테스트 클래스가 `BUILD SUCCESSFUL`로 종료된다.
|
||||||
|
|
||||||
- [ ] **REFACTOR:** 새 abstraction을 만들지 않고 이번 Task가 추가한 fixture·필드·매핑만 정리한다. 다음 명령을 실행하고 결과를 `P1-T1` Progress에 기록한다.
|
- [x] **REFACTOR:** 새 abstraction을 만들지 않고 이번 Task가 추가한 fixture·필드·매핑만 정리한다. 다음 명령을 실행하고 결과를 `P1-T1` Progress에 기록한다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew ktlintCheck --no-daemon
|
./gradlew ktlintCheck --no-daemon
|
||||||
@@ -191,7 +191,7 @@ git diff --check
|
|||||||
|
|
||||||
**Expected:** 두 명령 모두 exit code `0`.
|
**Expected:** 두 명령 모두 exit code `0`.
|
||||||
|
|
||||||
- [ ] **Commit:** 커밋 전후 규칙 검증과 함께 구현·테스트·Progress 변경만 커밋한다.
|
- [x] **Commit:** 커밋 전후 규칙 검증과 함께 구현·테스트·Progress 변경만 커밋한다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
work/scripts/check-commit-message-rules.sh --message "feat(creator-channel): 홈 커뮤니티 응답 상태를 추가한다"
|
work/scripts/check-commit-message-rules.sh --message "feat(creator-channel): 홈 커뮤니티 응답 상태를 추가한다"
|
||||||
@@ -260,6 +260,17 @@ P1-T1 → P1-GATE
|
|||||||
|
|
||||||
실행 전이다. 구현 시 기존 기록을 삭제하지 않고 Goal별 실행 결과를 아래에 누적한다.
|
실행 전이다. 구현 시 기존 기록을 삭제하지 않고 Goal별 실행 결과를 아래에 누적한다.
|
||||||
|
|
||||||
|
### P1-T1
|
||||||
|
|
||||||
|
- RED 작성: controller fixture의 일반 게시글을 `postId=302L`, `content="community"`, `isCommentAvailable=false`, `isPinned=false`로 구성하고 controller 및 E2E에 공지/일반 게시글 상태 assertion을 추가했다.
|
||||||
|
- RED 확인: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest' --no-daemon`은 exit code `1`로 실패했다. `CreatorChannelHomeControllerTest.kt:138`의 `$.data.notices[0].isPinned` assertion에서 `PathNotFoundException`이 발생해 신규 JSON 필드 미직렬화가 원인임을 확인했으며 compile 및 fixture 오류는 없었다.
|
||||||
|
- GREEN 구현: `CreatorChannelCommunityPostResponse`에 non-null Boolean `isCommentAvailable`, `isPinned`을 명시적 `@JsonProperty`와 함께 추가하고 `post.isCommentAvailable`, `post.isPinned`을 직접 매핑했다.
|
||||||
|
- GREEN 확인: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest' --no-daemon`은 exit code `0`, `BUILD SUCCESSFUL in 54s`로 종료됐다. controller focused test와 직접 영향받는 홈 E2E가 함께 통과해 공지 `true/true`, 일반 게시글 `false/false` controller fixture 및 `false/true` 실제 E2E 저장값의 매핑과 JSON 이름을 확인했다.
|
||||||
|
- REFACTOR 확인: 새 abstraction 없이 계획된 fixture, 응답 필드, 직접 매핑만 유지했다. `./gradlew ktlintCheck --no-daemon`은 exit code `0`, `BUILD SUCCESSFUL in 25s`였고 `git diff --check`도 exit code `0`이었다.
|
||||||
|
- 전체 회귀 생략: 이번 변경은 공유 홈 DTO의 additive 직렬화 필드 두 개와 직접 영향받는 테스트에 한정되고 controller focused test와 실제 저장·조회 E2E가 모두 통과했으므로 계획의 조건에 따라 전체 `./gradlew test`는 실행하지 않았다.
|
||||||
|
- 변경 파일: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt`, `docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/plan-task.md`.
|
||||||
|
- 커밋 검증: 커밋 전 `work/scripts/check-commit-message-rules.sh --message "feat(creator-channel): 홈 커뮤니티 응답 상태를 추가한다"`가 모든 규칙을 `[PASS]`로 확인했다. 최종 diff는 계획된 네 파일만 포함하고 비밀값, 관련 없는 변경, agent footer가 없음을 자체 검토했다.
|
||||||
|
|
||||||
## Decision Log
|
## Decision Log
|
||||||
|
|
||||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|||||||
@@ -181,8 +181,12 @@ data class CreatorChannelCommunityPostResponse(
|
|||||||
val price: Int,
|
val price: Int,
|
||||||
val dateUtc: String,
|
val dateUtc: String,
|
||||||
val existOrdered: Boolean,
|
val existOrdered: Boolean,
|
||||||
|
@JsonProperty("isCommentAvailable")
|
||||||
|
val isCommentAvailable: Boolean,
|
||||||
val likeCount: Int,
|
val likeCount: Int,
|
||||||
val commentCount: Int,
|
val commentCount: Int,
|
||||||
|
@JsonProperty("isPinned")
|
||||||
|
val isPinned: Boolean,
|
||||||
@JsonProperty("isLiked")
|
@JsonProperty("isLiked")
|
||||||
val isLiked: Boolean
|
val isLiked: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -199,8 +203,10 @@ data class CreatorChannelCommunityPostResponse(
|
|||||||
price = post.price,
|
price = post.price,
|
||||||
dateUtc = post.createdAt.toUtcIso(),
|
dateUtc = post.createdAt.toUtcIso(),
|
||||||
existOrdered = post.existOrdered,
|
existOrdered = post.existOrdered,
|
||||||
|
isCommentAvailable = post.isCommentAvailable,
|
||||||
likeCount = post.likeCount,
|
likeCount = post.likeCount,
|
||||||
commentCount = post.commentCount,
|
commentCount = post.commentCount,
|
||||||
|
isPinned = post.isPinned,
|
||||||
isLiked = post.isLiked
|
isLiked = post.isLiked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,12 +66,16 @@ class CreatorChannelHomeEndToEndTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.latestAudioContent.audioContentId").value(fixture.latestAudioId))
|
.andExpect(jsonPath("$.data.latestAudioContent.audioContentId").value(fixture.latestAudioId))
|
||||||
.andExpect(jsonPath("$.data.channelDonations[0].can").value(500))
|
.andExpect(jsonPath("$.data.channelDonations[0].can").value(500))
|
||||||
.andExpect(jsonPath("$.data.notices[0].postId").value(fixture.noticeId))
|
.andExpect(jsonPath("$.data.notices[0].postId").value(fixture.noticeId))
|
||||||
|
.andExpect(jsonPath("$.data.notices[0].isPinned").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.notices[0].isCommentAvailable").value(true))
|
||||||
.andExpect(jsonPath("$.data.schedules[0].targetId").value(fixture.liveScheduleId))
|
.andExpect(jsonPath("$.data.schedules[0].targetId").value(fixture.liveScheduleId))
|
||||||
.andExpect(jsonPath("$.data.schedules[1].targetId").value(fixture.audioScheduleId))
|
.andExpect(jsonPath("$.data.schedules[1].targetId").value(fixture.audioScheduleId))
|
||||||
.andExpect(jsonPath("$.data.audioContents[0].audioContentId").value(fixture.listAudioId))
|
.andExpect(jsonPath("$.data.audioContents[0].audioContentId").value(fixture.listAudioId))
|
||||||
.andExpect(jsonPath("$.data.audioContents[1].audioContentId").value(fixture.firstAudioId))
|
.andExpect(jsonPath("$.data.audioContents[1].audioContentId").value(fixture.firstAudioId))
|
||||||
.andExpect(jsonPath("$.data.series[0].seriesId").value(fixture.seriesId))
|
.andExpect(jsonPath("$.data.series[0].seriesId").value(fixture.seriesId))
|
||||||
.andExpect(jsonPath("$.data.communities[0].postId").value(fixture.communityId))
|
.andExpect(jsonPath("$.data.communities[0].postId").value(fixture.communityId))
|
||||||
|
.andExpect(jsonPath("$.data.communities[0].isPinned").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.communities[0].isCommentAvailable").value(true))
|
||||||
.andExpect(jsonPath("$.data.fanTalk.latestFanTalk.fanTalkId").value(fixture.fanTalkId))
|
.andExpect(jsonPath("$.data.fanTalk.latestFanTalk.fanTalkId").value(fixture.fanTalkId))
|
||||||
.andExpect(jsonPath("$.data.introduce").value("e2e introduce"))
|
.andExpect(jsonPath("$.data.introduce").value("e2e introduce"))
|
||||||
.andExpect(jsonPath("$.data.activity.audioContentCount").value(3))
|
.andExpect(jsonPath("$.data.activity.audioContentCount").value(3))
|
||||||
|
|||||||
@@ -135,7 +135,11 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.notices[0].dateUtc").value("2026-06-12T04:00:00Z"))
|
.andExpect(jsonPath("$.data.notices[0].dateUtc").value("2026-06-12T04:00:00Z"))
|
||||||
.andExpect(jsonPath("$.data.notices[0].imageUrl").doesNotExist())
|
.andExpect(jsonPath("$.data.notices[0].imageUrl").doesNotExist())
|
||||||
.andExpect(jsonPath("$.data.notices[0].audioUrl").doesNotExist())
|
.andExpect(jsonPath("$.data.notices[0].audioUrl").doesNotExist())
|
||||||
|
.andExpect(jsonPath("$.data.notices[0].isPinned").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.notices[0].isCommentAvailable").value(true))
|
||||||
.andExpect(jsonPath("$.data.notices[0].isLiked").value(true))
|
.andExpect(jsonPath("$.data.notices[0].isLiked").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.communities[0].isPinned").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.communities[0].isCommentAvailable").value(false))
|
||||||
.andExpect(jsonPath("$.data.communities[0].isLiked").value(true))
|
.andExpect(jsonPath("$.data.communities[0].isLiked").value(true))
|
||||||
.andExpect(jsonPath("$.data.series[0].isNew").value(true))
|
.andExpect(jsonPath("$.data.series[0].isNew").value(true))
|
||||||
.andExpect(jsonPath("$.data.series[0].isOriginal").value(true))
|
.andExpect(jsonPath("$.data.series[0].isOriginal").value(true))
|
||||||
@@ -283,7 +287,14 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
|
|||||||
isOriginal = true
|
isOriginal = true
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
communities = listOf(post.copy(postId = 302L, content = "community")),
|
communities = listOf(
|
||||||
|
post.copy(
|
||||||
|
postId = 302L,
|
||||||
|
content = "community",
|
||||||
|
isCommentAvailable = false,
|
||||||
|
isPinned = false
|
||||||
|
)
|
||||||
|
),
|
||||||
fanTalk = CreatorChannelFanTalkSummary(
|
fanTalk = CreatorChannelFanTalkSummary(
|
||||||
totalCount = 1,
|
totalCount = 1,
|
||||||
latestFanTalk = CreatorChannelFanTalk(
|
latestFanTalk = CreatorChannelFanTalk(
|
||||||
|
|||||||
Reference in New Issue
Block a user