From d99104d945743663803b506417b4d2ad3ac4b0a2 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 10 Aug 2026 12:09:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator-channel):=20=ED=99=88=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EC=9D=91=EB=8B=B5=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 29 +++++++++++++------ .../home/dto/CreatorChannelHomeResponse.kt | 6 ++++ .../home/CreatorChannelHomeEndToEndTest.kt | 4 +++ .../web/CreatorChannelHomeControllerTest.kt | 13 ++++++++- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/plan-task.md b/docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/plan-task.md index a00684c4..17b56e20 100644 --- a/docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/plan-task.md +++ b/docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/plan-task.md @@ -12,19 +12,19 @@ | 문서 항목 | 내용 | |---|---| -| 상태 | 구현 대기 | +| 상태 | 구현 중 | | 작성일 | 2026-08-10 | | 요구사항 기준 | `docs/20260810_크리에이터_채널_홈_커뮤니티_응답_확장/prd.md` | | API 기준 | PRD `7. API 계약` | | 현재 Phase | Phase 1: 홈 커뮤니티 상태 응답 확장 | | 현재 활성 Goal | 없음 | -| 다음 Goal | `P1-T1` | +| 다음 Goal | `P1-GATE` | ## 현재 상태 | Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| -| 1 | 대기 | `0/1` | `P1-T1` | 없음 | +| 1 | 진행 중 | `1/1` | `P1-GATE` | `P1-GATE` 검증 대기 | - 동시에 하나의 미완료 goal만 운용한다. - 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다. @@ -87,7 +87,7 @@ - Produces: `CreatorChannelCommunityPostResponse.isPinned: Boolean`, `CreatorChannelCommunityPostResponse.isCommentAvailable: Boolean` - 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 communities = listOf( @@ -116,7 +116,7 @@ E2E assertion은 실제 저장값과 조회 조건을 검증한다. .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 ./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 증거로 인정하지 않는다. -- [ ] **GREEN:** `CreatorChannelCommunityPostResponse`에 기존 커뮤니티 탭 DTO와 같은 `@JsonProperty` 관례로 필드를 추가하고 `from`에서 도메인 값을 직접 매핑한다. +- [x] **GREEN:** `CreatorChannelCommunityPostResponse`에 기존 커뮤니티 탭 DTO와 같은 `@JsonProperty` 관례로 필드를 추가하고 `from`에서 도메인 값을 직접 매핑한다. ```kotlin data class CreatorChannelCommunityPostResponse( @@ -171,7 +171,7 @@ data class CreatorChannelCommunityPostResponse( } ``` -- [ ] **GREEN 확인:** controller test와 홈 E2E를 함께 실행해 공유 DTO 매핑, JSON 이름, 실제 조회값이 모두 통과하는지 확인한다. +- [x] **GREEN 확인:** controller test와 홈 E2E를 함께 실행해 공유 DTO 매핑, JSON 이름, 실제 조회값이 모두 통과하는지 확인한다. ```bash ./gradlew test \ @@ -182,7 +182,7 @@ data class CreatorChannelCommunityPostResponse( **Expected:** 두 테스트 클래스가 `BUILD SUCCESSFUL`로 종료된다. -- [ ] **REFACTOR:** 새 abstraction을 만들지 않고 이번 Task가 추가한 fixture·필드·매핑만 정리한다. 다음 명령을 실행하고 결과를 `P1-T1` Progress에 기록한다. +- [x] **REFACTOR:** 새 abstraction을 만들지 않고 이번 Task가 추가한 fixture·필드·매핑만 정리한다. 다음 명령을 실행하고 결과를 `P1-T1` Progress에 기록한다. ```bash ./gradlew ktlintCheck --no-daemon @@ -191,7 +191,7 @@ git diff --check **Expected:** 두 명령 모두 exit code `0`. -- [ ] **Commit:** 커밋 전후 규칙 검증과 함께 구현·테스트·Progress 변경만 커밋한다. +- [x] **Commit:** 커밋 전후 규칙 검증과 함께 구현·테스트·Progress 변경만 커밋한다. ```bash work/scripts/check-commit-message-rules.sh --message "feat(creator-channel): 홈 커뮤니티 응답 상태를 추가한다" @@ -260,6 +260,17 @@ P1-T1 → P1-GATE 실행 전이다. 구현 시 기존 기록을 삭제하지 않고 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 | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt index 8859108a..6e14c92a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt @@ -181,8 +181,12 @@ data class CreatorChannelCommunityPostResponse( val price: Int, val dateUtc: String, val existOrdered: Boolean, + @JsonProperty("isCommentAvailable") + val isCommentAvailable: Boolean, val likeCount: Int, val commentCount: Int, + @JsonProperty("isPinned") + val isPinned: Boolean, @JsonProperty("isLiked") val isLiked: Boolean ) { @@ -199,8 +203,10 @@ data class CreatorChannelCommunityPostResponse( price = post.price, dateUtc = post.createdAt.toUtcIso(), existOrdered = post.existOrdered, + isCommentAvailable = post.isCommentAvailable, likeCount = post.likeCount, commentCount = post.commentCount, + isPinned = post.isPinned, isLiked = post.isLiked ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt index 9389351d..9970dff6 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt @@ -66,12 +66,16 @@ class CreatorChannelHomeEndToEndTest @Autowired constructor( .andExpect(jsonPath("$.data.latestAudioContent.audioContentId").value(fixture.latestAudioId)) .andExpect(jsonPath("$.data.channelDonations[0].can").value(500)) .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[1].targetId").value(fixture.audioScheduleId)) .andExpect(jsonPath("$.data.audioContents[0].audioContentId").value(fixture.listAudioId)) .andExpect(jsonPath("$.data.audioContents[1].audioContentId").value(fixture.firstAudioId)) .andExpect(jsonPath("$.data.series[0].seriesId").value(fixture.seriesId)) .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.introduce").value("e2e introduce")) .andExpect(jsonPath("$.data.activity.audioContentCount").value(3)) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt index 0a2785cb..a95f29f3 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt @@ -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].imageUrl").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.communities[0].isPinned").value(false)) + .andExpect(jsonPath("$.data.communities[0].isCommentAvailable").value(false)) .andExpect(jsonPath("$.data.communities[0].isLiked").value(true)) .andExpect(jsonPath("$.data.series[0].isNew").value(true)) .andExpect(jsonPath("$.data.series[0].isOriginal").value(true)) @@ -283,7 +287,14 @@ class CreatorChannelHomeControllerTest @Autowired constructor( isOriginal = true ) ), - communities = listOf(post.copy(postId = 302L, content = "community")), + communities = listOf( + post.copy( + postId = 302L, + content = "community", + isCommentAvailable = false, + isPinned = false + ) + ), fanTalk = CreatorChannelFanTalkSummary( totalCount = 1, latestFanTalk = CreatorChannelFanTalk(