feat(creator): 커뮤니티 게시글 상세를 연결한다

This commit is contained in:
Yu Sung
2026-07-08 01:56:57 +09:00
parent f2d589eca2
commit c0369e6f58
23 changed files with 2187 additions and 138 deletions

View File

@@ -0,0 +1,379 @@
# 커뮤니티 게시글 상세 구현 계획
> **For agentic workers:** PRD를 기준으로 task 단위로 진행한다. 구현 중 요구사항 변경이 생기면 먼저 `docs/20260707_커뮤니티_게시글_상세/prd.md`를 보강하고 이 문서의 체크리스트를 갱신한다.
**Goal:** V2 커뮤니티 게시글 상세 화면을 추가하고, 상세/댓글/답글 조회 API와 기존 커뮤니티 mutation API를 연결한다.
**Architecture:** 상세/댓글/답글 조회는 `SodaLive/Sources/V2/CreatorChannel/Community/**`의 V2 API/Repository에 추가한다. 상세 화면 상태는 신규 `Community/Detail` ViewModel이 관리하고, 댓글 작성/수정/삭제/좋아요 mutation은 기존 `CreatorCommunityRepository`를 재사용한다. 댓글의 답글 상세 화면은 기존 `CreatorChannelReplyDetailView` 계열을 복사하지 않고 context/display adapter를 확장해 재사용한다.
**Tech Stack:** SwiftUI, Combine, Moya, Kingfisher/SDWebImageSwiftUI, 기존 `AppStep`/`AppState`, 기존 V2 design token(`Color`, `SodaSpacing`, `appFont`)
---
## 기준 문서
- PRD: `docs/20260707_커뮤니티_게시글_상세/prd.md`
- 문서 정책: `docs/agent-guides/documentation-policy.md`
- 빌드/검증: `docs/agent-guides/build-test-verification.md`
- 커뮤니티 탭 PRD: `docs/20260705_크리에이터_채널_커뮤니티_탭/prd.md`
- 글 상세 - 답글 쓰기 PRD: `docs/20260707_글_상세_답글_쓰기/prd.md`
- Figma 댓글 가능 상세: `290:9243`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9243&m=dev`
- Figma 댓글 불가능 상세: `290:9266`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9266&m=dev`
## 구현 원칙
- 신규 상세 화면 파일은 `SodaLive/Sources/V2/CreatorChannel/Community/Detail/**` 아래에 둔다.
- 신규 조회 모델은 `SodaLive/Sources/V2/CreatorChannel/Community/Models/**` 아래에 둔다.
- 기존 `CreatorChannelCommunityApi.getCommunity``page=0` 정책을 유지하고, 신규 V2 조회 API에도 `page - 1` 보정을 넣지 않는다.
- 댓글 작성/답글 작성/댓글 수정/댓글 삭제/좋아요 API는 기존 `CreatorCommunityRepository`를 사용한다.
- 댓글 불가능 게시글은 좋아요 UI는 유지하고, 댓글 icon/count, 댓글 목록, 하단 댓글 입력 바를 숨긴다.
- 댓글 입력창은 3줄까지 자동 확장하고 4줄 이상은 내부 스크롤로 처리한다.
- 전송 버튼 icon은 `ic_arrow_up_gray`, `ic_arrow_up_white`를 사용한다.
- 댓글 쓰기 UI에는 기존 비밀댓글 toggle을 추가하고, 기존 `isShowSecret` 노출 정책을 따른다.
- 답글 상세 UI는 기존 `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/**` 구현을 재사용한다.
## 주요 대상 파일
### 생성
- `SodaLive/Sources/V2/CreatorChannel/Community/Models/CreatorChannelCommunityPostDetailResponse.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentListView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentRow.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailInputBar.swift`
### 수정
- `SodaLive/Sources/V2/CreatorChannel/Community/Repository/CreatorChannelCommunityApi.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Repository/CreatorChannelCommunityRepository.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift`
- `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift`
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Models/CreatorChannelReplyDetailContext.swift`
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Models/CreatorChannelReplyDetailDisplayItem.swift`
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- `SodaLive/Sources/App/AppStep.swift`
- `SodaLive/Sources/ContentView.swift`
- `SodaLive/Sources/I18n/I18n.swift`
- `SodaLive.xcodeproj/project.pbxproj`
### 확인/재사용
- `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift`
- `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift`
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift`
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/ModifyCommunityPostCommentRequest.swift`
## TASK 체크리스트
### Phase 1: 조회 모델과 API
- [x] **Task 1.1: 커뮤니티 게시글 상세 응답 모델 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Models/CreatorChannelCommunityPostDetailResponse.swift`
- 작업 내용:
- `CreatorChannelCommunityPostDetailResponse``Decodable`, `Identifiable`로 정의한다.
- `CreatorChannelCommunityCommentsResponse`, `CreatorChannelCommunityCommentResponse`, `CreatorChannelCommunityReplyResponse`, `CreatorChannelCommunityRepliesResponse`를 정의한다.
- Kotlin `Long` 필드는 Swift `Int`로 선언한다.
- `CreatorChannelCommunityCommentResponse``CreatorChannelCommunityReplyResponse`에는 `writerId: Int`를 포함한다.
- 각 시간 필드는 `DateParser.relativeTimeText(fromUTC:fallback:now:)`를 사용하는 helper를 제공한다.
- 검증 기준:
- 실행 명령: `rg "struct CreatorChannelCommunityPostDetailResponse|struct CreatorChannelCommunityCommentsResponse|struct CreatorChannelCommunityCommentResponse|writerId|struct CreatorChannelCommunityRepliesResponse|relativeTimeText" SodaLive/Sources/V2/CreatorChannel/Community/Models/CreatorChannelCommunityPostDetailResponse.swift`
- 기대 결과: 상세/댓글/답글 모델과 `writerId`, 상대 시간 helper가 확인된다.
- [x] **Task 1.2: 커뮤니티 게시글 상세/댓글/답글 조회 API 추가**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Repository/CreatorChannelCommunityApi.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Repository/CreatorChannelCommunityRepository.swift`
- 작업 내용:
- `CreatorChannelCommunityApi``getCommunityPostDetail(postId:)`, `getCommunityPostComments(postId:page:size:)`, `getCommunityCommentReplies(commentId:page:size:)` case를 추가한다.
- path는 각각 `/api/v2/creator-channels/community-posts/{postId}`, `/api/v2/creator-channels/community-posts/{postId}/comments`, `/api/v2/creator-channels/community-comments/{commentId}/replies`로 구성한다.
- 댓글/답글 query는 `page`, `size`를 그대로 전달하고 `page - 1` 보정을 하지 않는다.
- Repository에 동일 이름의 publisher 함수를 추가한다.
- 검증 기준:
- 실행 명령: `rg "getCommunityPostDetail|getCommunityPostComments|getCommunityCommentReplies|community-posts|community-comments|URLEncoding.queryString" SodaLive/Sources/V2/CreatorChannel/Community/Repository`
- 기대 결과: 신규 endpoint 3개와 query 전달이 확인된다.
### Phase 2: 라우팅과 목록 진입
- [x] **Task 2.1: 게시글 상세 route 추가**
- 대상 파일:
- 수정: `SodaLive/Sources/App/AppStep.swift`
- 수정: `SodaLive/Sources/ContentView.swift`
- 작업 내용:
- `AppStep``creatorChannelCommunityPostDetail(postId:onCommunityRefresh:)` case를 추가한다.
- `ContentView` switch에서 `CreatorChannelCommunityPostDetailView(postId:onCommunityRefresh:)`를 반환한다.
- `onCommunityRefresh`는 상세 mutation 성공 후 커뮤니티 탭 첫 페이지를 갱신하기 위한 optional callback으로 둔다.
- 검증 기준:
- 실행 명령: `rg "creatorChannelCommunityPostDetail|CreatorChannelCommunityPostDetailView|onCommunityRefresh" SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift`
- 기대 결과: 새 route와 화면 연결이 확인된다.
- [x] **Task 2.2: 커뮤니티 리스트/그리드 item tap에서 상세 진입 연결**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift`
- 작업 내용:
- 리스트 item과 그리드 item에 `onTapDetail` closure를 추가한다.
- item 전체 tap은 상세 route로 연결한다.
- 좋아요, 댓글, 더보기, 구매, 오디오 재생 버튼 tap은 상세 진입과 충돌하지 않게 button action으로 유지한다.
- 상세 route에는 `post.postId``viewModel.fetchFirstPage(creatorId:)` callback을 전달한다.
- 검증 기준:
- 실행 명령: `rg "onTapDetail|creatorChannelCommunityPostDetail|fetchFirstPage\\(creatorId: creatorId\\)|contentShape|Button" SodaLive/Sources/V2/CreatorChannel/Community`
- 기대 결과: 리스트/그리드 item tap 상세 진입과 내부 버튼 action 분리가 확인된다.
### Phase 3: 상세 ViewModel과 mutation 재사용
- [x] **Task 3.1: 상세 ViewModel 기본 상태와 상세 조회 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 작업 내용:
- 상태로 `detail`, `comments`, `commentText`, `isSecret`, `isLoading`, `isLoadingNextPage`, `hasLoaded`, `errorMessage`, `isShowPopup`, `selectedComment`, `editingComment`, `deletingComment`를 둔다.
- `fetchDetail(postId:)`에서 상세 API를 호출하고 게시글과 초기 댓글 목록을 구성한다.
- `isCommentAvailable == false`이면 댓글 입력/목록 상태를 비활성 상태로 유지한다.
- `isShowSecret` computed property는 기존 정책인 `price > 0 && existOrdered && creatorId != UserDefaults.int(forKey: .userId)`를 따른다.
- `isCommentSendEnabled`는 trim된 `commentText`가 비어 있지 않고 로딩 중이 아닐 때 true로 계산한다.
- 검증 기준:
- 실행 명령: `rg "final class CreatorChannelCommunityPostDetailViewModel|fetchDetail|detail|comments|commentText|isSecret|isShowSecret|isCommentSendEnabled|getCommunityPostDetail" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 기대 결과: 상세 조회와 댓글 입력 상태가 확인된다.
- [x] **Task 3.2: 댓글 페이지네이션 구현**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 작업 내용:
- `fetchNextCommentsIfNeeded(currentComment:)`를 추가한다.
- 마지막 댓글 노출 시 `hasNext == true`이면 `page + 1`로 댓글 API를 호출한다.
- 중복 호출을 막기 위해 `isLoadingNextPage``isLoading` guard를 둔다.
- 첫 페이지 재조회 시 기존 댓글 목록을 교체하고, 다음 페이지 성공 시 append한다.
- 검증 기준:
- 실행 명령: `rg "fetchNextCommentsIfNeeded|getCommunityPostComments|isLoadingNextPage|hasNext|page \\+ 1|append\\(contentsOf:" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 기대 결과: 댓글 추가 조회와 append 로직이 확인된다.
- [x] **Task 3.3: 좋아요 mutation과 optimistic state 구현**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 작업 내용:
- `toggleLike()`에서 기존 `CreatorCommunityRepository.communityPostLike(postId:)`를 호출한다.
- tap 직후 `isLiked``likeCount`를 local state에 반영한다.
- 실패 시 toast를 표시하고 tap 직전의 `isLiked`, `likeCount` 값으로 local state를 복구한다.
- 검증 기준:
- 실행 명령: `rg "toggleLike|communityPostLike|isLiked|likeCount|errorMessage|isShowPopup" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 기대 결과: 기존 좋아요 API와 local state 반영이 확인된다.
- [x] **Task 3.4: 댓글 작성/수정/삭제 mutation 구현**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 작업 내용:
- 신규 댓글 작성은 `CreatorCommunityRepository.createCommunityPostComment(comment:postId:parentId:isSecret:)`를 사용하고 `parentId == nil`, `isSecret == 비밀댓글 toggle 값`으로 호출한다.
- 댓글 수정은 `ModifyCommunityPostCommentRequest(commentId:)`에 수정 content를 담아 `modifyComment(request:)`를 호출한다.
- 댓글 삭제는 `ModifyCommunityPostCommentRequest(commentId:)``isActive = false`를 담아 `modifyComment(request:)`를 호출한다.
- 성공 후 입력값과 수정/삭제 선택 상태를 초기화하고 상세 또는 댓글 첫 페이지를 재조회한다.
- mutation 성공 시 `onCommunityRefresh` callback을 호출해 목록 갱신을 예약한다.
- 검증 기준:
- 실행 명령: `rg "createCommunityPostComment|parentId: nil|isSecret|ModifyCommunityPostCommentRequest|modifyComment|isActive = false|onCommunityRefresh|commentText = \"\"" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 기대 결과: 기존 커뮤니티 댓글 mutation API 재사용이 확인된다.
### Phase 4: 상세 UI 컴포넌트
- [x] **Task 4.1: 상세 게시글 콘텐츠 컴포넌트 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift`
- 작업 내용:
- Figma 기준으로 작성자 프로필, 닉네임, 상대 시간, 본문, 이미지/잠금/오디오 영역, 좋아요/댓글 reaction을 표시한다.
- 유료 미구매 상태는 `price > 0 && existOrdered == false && isOwnPost == false`로 판정한다.
- 유료 미구매 상태에서는 `gray800` 계열 이미지 영역, 잠금 아이콘, 캐시 가격 capsule을 표시한다.
- 구매 완료 상태는 `구매완료` capsule tag를 표시한다.
- 접근 가능한 이미지/GIF는 기존 `WebImage` 또는 `DownsampledKFImage` 패턴을 따른다.
- `audioUrl`이 있으면 기존 `CreatorCommunityMediaPlayerManager``ContentPlayManager` 흐름을 재사용한다.
- `isCommentAvailable == false`이면 댓글 icon/count만 숨기고 좋아요는 유지한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailContentView|ic_feed_community_heart_fill|ic_feed_community_heart|ic_feed_community_reply|ic_new_community_lock|ic_bar_cash|구매완료|CreatorCommunityMediaPlayerManager|isCommentAvailable" SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift`
- 기대 결과: 게시글 상세 표시, 좋아요, 댓글 숨김, 잠금/구매완료 UI가 확인된다.
- [x] **Task 4.2: 댓글 row와 최신 답글 카드 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentRow.swift`
- 작업 내용:
- 댓글 row는 42pt 프로필, 닉네임, 상대 시간, 본문, 우측 더보기 버튼을 표시한다.
- `writerId == UserDefaults.int(forKey: .userId)`로 내 댓글 여부를 판정한다.
- `post.creatorId == UserDefaults.int(forKey: .userId)`로 현재 사용자가 해당 게시글을 쓴 크리에이터인지 판정한다.
- `latestReply != nil`이면 20pt 프로필, 닉네임, 상대 시간, 본문을 가진 `Color.gray900`, radius 14pt 답글 카드를 표시한다.
- 최신 답글이 있으면 기존 팬Talk/ReplyDetail connector와 같은 좌측 연결선을 표시한다.
- row tap은 답글 상세 진입 action을 호출한다.
- 더보기 버튼 tap은 popup/action 선택으로 연결하고 row tap과 충돌하지 않게 한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailCommentRow|latestReply|writerId|creatorId|UserDefaults.int|Color\\.gray900|RoundedRectangle|Shape|Path|onTapReplyDetail|onTapMore" SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentRow.swift`
- 기대 결과: 댓글 row, 최신 답글 카드, 연결선, action 분리가 확인된다.
- [x] **Task 4.3: 댓글 목록 컴포넌트 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentListView.swift`
- 작업 내용:
- section header는 `댓글 {commentCount}`를 표시한다.
- 댓글 목록은 `LazyVStack`으로 표시하고 마지막 댓글 `onAppear`에서 다음 페이지 조회를 요청한다.
- 댓글이 비어 있으면 header만 유지하거나 Figma 기준 빈 목록 영역을 유지한다.
- 선택된 댓글의 popup은 기존 `CreatorChannelActionPopup` 또는 `CreatorChannelReplyDetailActionPopup` 기반으로 수정/삭제 action을 표시한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailCommentListView|댓글|commentCount|LazyVStack|onAppear|fetchNext|CreatorChannelActionPopup|onTapEdit|onTapDelete" SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentListView.swift`
- 기대 결과: 댓글 목록, 페이지네이션 trigger, popup action 연결이 확인된다.
- [x] **Task 4.4: 하단 댓글 입력 바 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailInputBar.swift`
- 수정: `SodaLive/Sources/I18n/I18n.swift`
- 작업 내용:
- placeholder는 `댓글을 입력하세요`를 사용한다.
- 기존 비밀댓글 toggle UI를 추가하고 `isShowSecret`일 때만 표시한다.
- 입력창은 1줄에서 3줄까지 자동 확장한다.
- 4줄 이상은 3줄 높이로 유지하고 내부 vertical scroll을 허용한다.
- 전송 버튼은 비활성 상태에서 `ic_arrow_up_gray`/`gray900`, 활성 상태에서 `ic_arrow_up_white`/`soda400`를 사용한다.
- 수정 모드에서는 취소 버튼 또는 기존 입력 복귀 UX를 최소 범위로 제공한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailInputBar|댓글을 입력하세요|isShowSecret|isSecret|ic_arrow_up_gray|ic_arrow_up_white|Color\\.gray900|Color\\.soda400|axis: \\.vertical|lineLimit" SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailInputBar.swift SodaLive/Sources/I18n/I18n.swift`
- 기대 결과: placeholder, 비밀댓글 toggle, 3줄 확장, 전송 버튼 상태가 확인된다.
- [x] **Task 4.5: 상세 화면 조립**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift`
- 작업 내용:
- `CreatorChannelReplyDetailTitleBar`를 재사용해 back-only title bar를 표시한다.
- `ScrollView` 안에 게시글 콘텐츠와 댓글 목록을 배치한다.
- `isCommentAvailable == true`일 때만 하단 입력 바를 `safeAreaInset(edge: .bottom)`으로 고정한다.
- loading, toast, 삭제 confirm modal을 연결한다.
- `onAppear`에서 `viewModel.fetchDetail(postId:)`를 호출한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailView|CreatorChannelReplyDetailTitleBar|CreatorChannelCommunityPostDetailContentView|CreatorChannelCommunityPostDetailCommentListView|CreatorChannelCommunityPostDetailInputBar|safeAreaInset|fetchDetail|SodaV2ActionModal|sodaToast" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift`
- 기대 결과: 상세 화면 조립과 댓글 가능 여부별 하단 입력 바 조건이 확인된다.
### Phase 5: 답글 상세 재사용 확장
- [x] **Task 5.1: 신규 댓글 응답을 답글 상세 context로 전달할 수 있게 확장**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Models/CreatorChannelReplyDetailContext.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Models/CreatorChannelReplyDetailDisplayItem.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- 작업 내용:
- 기존 `.community(creatorId:postId:parentComment: GetCommunityPostCommentListItem)` 경로는 유지한다.
- 신규 상세 댓글 응답을 전달할 수 있는 context case 또는 adapter를 추가한다.
- `CreatorChannelCommunityCommentResponse``commentId`, `writerId`, `writerNickname`, `writerProfileImageUrl`, `content`, `createdAtUtc`로 display item을 만든다.
- 신규 응답에는 `isSecret`이 없으므로 원댓글 표시용 `isSecret`은 false로 처리하고, 신규 댓글 입력의 비밀 여부는 상세 댓글 작성 UI에서만 다룬다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityCommentResponse|communityPostDetail|commentId|writerId|writerProfileImageUrl|DateParser.relativeTimeText|isSecret: false" SodaLive/Sources/V2/CreatorChannel/ReplyDetail`
- 기대 결과: 신규 댓글 응답에서 답글 상세 display item을 만들 수 있다.
- [x] **Task 5.2: 답글 상세에서 신규 답글 조회 API 사용**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- 작업 내용:
- 신규 상세 댓글 context에서는 `CreatorChannelCommunityRepository.getCommunityCommentReplies(commentId:page:size:)`를 사용한다.
- 기존 V1 댓글 context는 기존 `CreatorCommunityRepository.getCommentReplyList(commentId:page:size:)` 경로를 유지한다.
- 신규 `CreatorChannelCommunityReplyResponse``CreatorChannelReplyDetailDisplayItem`으로 변환한다.
- `hasNext`, `page`, `size`를 사용해 답글 페이지네이션을 처리한다.
- 검증 기준:
- 실행 명령: `rg "getCommunityCommentReplies|CreatorChannelCommunityRepliesResponse|CreatorChannelCommunityReplyResponse|hasNext|communityReplyPage|fetchNextCommunityRepliesIfNeeded" SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- 기대 결과: 신규 답글 조회 API와 기존 V1 답글 조회 경로가 분리되어 확인된다.
- [x] **Task 5.3: 댓글 row tap에서 답글 상세 route 연결**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentRow.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- 수정: `SodaLive/Sources/App/AppStep.swift`
- 수정: `SodaLive/Sources/ContentView.swift`
- 작업 내용:
- 댓글 row tap 시 `AppState.shared.setAppStep(step: .creatorChannelReplyDetail(...))`을 호출한다.
- context에는 `creatorId`, `postId`, 선택 댓글 응답을 전달한다.
- `CreatorChannelReplyDetailView`에서 커뮤니티 답글 mutation 성공 시 호출할 `onCommunityRefresh` callback을 추가하고, 게시글 상세 진입 시 `viewModel.fetchDetail(postId:)`를 전달한다.
- 검증 기준:
- 실행 명령: `rg "creatorChannelReplyDetail|CreatorChannelReplyDetailContext|CreatorChannelCommunityCommentResponse|onTapReplyDetail|onCommunityRefresh|postId|creatorId" SodaLive/Sources/V2/CreatorChannel/Community/Detail SodaLive/Sources/V2/CreatorChannel/ReplyDetail SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift`
- 기대 결과: 상세 댓글에서 기존 답글 상세 화면으로 진입하는 route가 확인된다.
### Phase 6: 권한, 문구, 프로젝트 등록
- [x] **Task 6.1: 댓글 popup 권한과 수정/삭제 모드 연결, 신고 차단 기록**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailCommentListView.swift`
- 작업 내용:
- 내 댓글/답글이면 `수정하기`, `삭제하기`를 표시한다.
- 내가 해당 커뮤니티 게시글을 쓴 크리에이터이면 내 댓글/답글이 아니어도 `삭제하기`를 표시한다.
- `수정하기` tap 시 하단 입력창에 기존 댓글 내용을 채우고 send 시 수정 API를 호출한다.
- `삭제하기` tap 시 `SodaV2ActionModal`로 삭제 확인을 표시한다.
- [blocked] 신고는 기존 `ReportRequest`/`ReportType`에 커뮤니티 댓글 신고 식별자가 없고, 기존 `CreatorCommunityCommentItemView`도 신고 action을 노출하지 않아 연결하지 않는다. 백엔드/API 계약에 community comment report identifier가 추가된 뒤 별도 구현한다.
- 검증 기준:
- 실행 명령: `rg "writerId ==|creatorId ==|currentUserId|canEdit|canDelete|beginEditing|editingComment|deletingComment|SodaV2ActionModal|confirmDelete|CreatorChannelActionPopup" SodaLive/Sources/V2/CreatorChannel/Community/Detail`
- 기대 결과: 작성자 수정/삭제 권한, 게시글 크리에이터 삭제 권한, 수정 모드가 확인된다. 신고 action은 community comment report identifier API 계약 부재로 노출하지 않는다.
- [x] **Task 6.2: I18n 문구와 asset 참조 확인**
- 대상 파일:
- 수정: `SodaLive/Sources/I18n/I18n.swift`
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_arrow_up_gray.imageset/Contents.json`
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_arrow_up_white.imageset/Contents.json`
- 작업 내용:
- `I18n.CreatorChannelCommunity.commentPlaceholder` 또는 같은 범위의 문구로 `댓글을 입력하세요` ko/en/ja를 추가한다.
- 비밀댓글 toggle 문구는 기존 `I18n.Explorer.secretComment`를 재사용한다.
- `ic_arrow_up_gray`, `ic_arrow_up_white` asset이 존재하는지 확인하고 다른 arrow asset 이름을 참조하지 않는다.
- 검증 기준:
- 실행 명령: `rg "commentPlaceholder|댓글을 입력하세요|secretComment|ic_arrow_up_gray|ic_arrow_up_white" SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail SodaLive/Resources/Assets.xcassets/v2`
- 기대 결과: 문구와 icon 참조가 확정된 asset 이름과 일치한다.
- [x] **Task 6.3: 프로젝트 파일 등록 확인**
- 대상 파일:
- 확인/수정: `SodaLive.xcodeproj/project.pbxproj`
- 작업 내용:
- 신규 Swift 파일이 Xcode target에 포함되는지 확인한다.
- 수동 등록이 필요한 경우 기존 V2 파일 등록 패턴을 따라 최소 변경으로 추가한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelCommunityPostDetailView|CreatorChannelCommunityPostDetailViewModel|CreatorChannelCommunityPostDetailResponse|CreatorChannelCommunityPostDetailInputBar" SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: 신규 Swift 파일의 project 등록이 확인된다.
### Phase 7: 최종 검증
- [x] **Task 7.1: 정적 문서/파일 검증**
- 대상 파일:
- 확인: `docs/20260707_커뮤니티_게시글_상세/prd.md`
- 확인: `docs/20260707_커뮤니티_게시글_상세/plan-task.md`
- 확인: 구현 변경 파일 전체
- 검증 기준:
- 실행 명령: `git diff --check -- docs/20260707_커뮤니티_게시글_상세 SodaLive/Sources/V2/CreatorChannel/Community SodaLive/Sources/V2/CreatorChannel/ReplyDetail SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: 공백 오류 없이 성공한다.
- 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: `OK`가 출력된다.
- [x] **Task 7.2: 빌드 검증**
- 대상 파일:
- 확인: 전체 앱 target
- 검증 기준:
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 기대 결과: Debug 빌드 성공.
- 참고: 테스트 번들 타깃이 확인되지 않는 저장소 상태이므로, 단위 테스트가 추가되지 않은 경우 빌드 검증을 우선 성공 기준으로 둔다.
- [ ] **Task 7.3: 수동 QA 시나리오 기록**
- 대상 파일:
- 수정: `docs/20260707_커뮤니티_게시글_상세/plan-task.md`
- 검증 기준:
- 댓글 가능 게시글 상세 진입 시 상세 API가 호출되고 게시글/댓글/입력 바가 표시된다.
- 댓글 불가능 게시글 상세 진입 시 좋아요는 표시되고 댓글 icon/count, 댓글 목록, 입력 바는 숨겨진다.
- 댓글 입력값이 없으면 `ic_arrow_up_gray`/`gray900`, 입력값이 있으면 `ic_arrow_up_white`/`soda400`가 표시된다.
- 4줄 이상 입력 시 입력창 높이가 3줄로 유지되고 내부 스크롤된다.
- 비밀댓글 toggle 노출 조건에서 toggle 값이 댓글 작성 API의 `isSecret`으로 전달된다.
- 댓글 row tap 시 기존 답글 상세 화면으로 진입하고 신규 답글 조회 API가 호출된다.
- 게시글을 쓴 크리에이터 계정에서는 내가 작성하지 않은 댓글/답글에도 `삭제하기`가 표시된다.
- 작성자도 게시글 크리에이터도 아닌 계정에서는 신고 action이 표시되지 않는다. [blocked] 기존 `ReportRequest`/`ReportType`에 community comment report identifier가 없어 백엔드/API 계약 확정 전까지 수동 QA 대상에서 제외한다.
- 하트 tap 시 기존 좋아요 API가 호출되고 icon/count가 갱신된다.
- 실행 후 이 문서의 `검증 기록` 섹션에 실행 일자, 명령, 결과를 누적한다.
## 검증 기록
- 2026-07-07: `docs/20260707_커뮤니티_게시글_상세/prd.md`, `docs/agent-guides/build-test-verification.md`, 기존 `docs/20260707_글_상세_답글_쓰기/plan-task.md`, `docs/20260705_크리에이터_채널_커뮤니티_탭/plan-task.md`를 확인해 구현 범위와 문서 형식을 정했다.
- 2026-07-07: 현재 코드 기준으로 `AppStep.creatorChannelReplyDetail`, `CreatorChannelReplyDetailView`, V2 커뮤니티 탭 파일, V2 커뮤니티 API/Repository, 답글 상세 display 모델이 이미 존재함을 확인하고, 게시글 상세는 신규 `Community/Detail` 하위 구현으로 계획했다.
- 2026-07-07: 기존 `CreatorCommunityCommentItemView`에서 게시글 크리에이터 또는 댓글 작성자이면 더보기 버튼을 표시하고, 수정은 작성자에게만, 삭제는 작성자 또는 게시글 크리에이터에게 허용하는 것을 확인했다. 계획 문서에 동일 권한 규칙을 반영했다.
- 2026-07-07: 사용자 확인 사항에 따라 신고 규칙도 기존 커뮤니티 댓글과 동일하게 검토 대상으로 두었으나, 구현 확인 결과 기존 `ReportRequest`/`ReportType`에 community comment report identifier가 없고 기존 `CreatorCommunityCommentItemView`도 신고 action을 노출하지 않아 계획 문서에 [blocked]로 정정했다.
- 2026-07-07: `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행 결과 `OK`를 확인했다.
- 2026-07-07: `git diff --check -- docs/20260707_커뮤니티_게시글_상세 SodaLive/Sources/V2/CreatorChannel/Community SodaLive/Sources/V2/CreatorChannel/ReplyDetail SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` 실행 결과 공백 오류 없이 통과했다.
- 2026-07-07: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행 결과 `BUILD SUCCEEDED`를 확인했다.
- 2026-07-07: `rg "canReport|onTapReport|showReportError|CreatorChannelActionPopupWithReport|private struct CreatorChannelActionPopup|신고" SodaLive/Sources/V2/CreatorChannel/Community/Detail` 실행 결과 fake report 제거 후 출력이 없음을 확인했다.
- 2026-07-07: 커뮤니티 댓글 신고 흐름은 기존 `ReportRequest`/`ReportType`에 커뮤니티 댓글 신고 식별자가 없어 연결하지 못했다. 구현은 기존 `CreatorCommunityCommentItemView`의 수정/삭제 popup 동작과 동일하게 맞췄고, fake report action은 노출하지 않는다.
- 2026-07-07: 정적 검증과 Debug 빌드 검증은 완료했다. Task 7.3의 device/simulator/API-backed 환경 수동 runtime QA는 아직 수행하지 않았다.

View File

@@ -0,0 +1,289 @@
# PRD: 커뮤니티 게시글 상세
## 1. Overview
크리에이터 채널 커뮤니티 게시글을 터치했을 때 진입하는 V2 커뮤니티 게시글 상세 화면을 제공한다. 화면은 게시글 본문, 이미지/오디오/유료 잠금 상태, 좋아요, 댓글 목록, 최신 답글, 하단 고정 댓글 입력 바를 표시한다.
상세 API는 `GET /api/v2/creator-channels/community-posts/{postId}`를 사용한다. 댓글 추가 조회는 `GET /api/v2/creator-channels/community-posts/{postId}/comments`, 답글 추가 조회는 `GET /api/v2/creator-channels/community-comments/{commentId}/replies`를 사용한다. `page` 기본값은 `0`, `size` 기본값은 `20`이다.
Figma 참조:
- 댓글 가능 상세: `290:9243`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9243&m=dev`
- 댓글 불가능 상세: `290:9266`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9266&m=dev`
## 2. Problem
- 현재 V2 커뮤니티 탭은 게시글 목록을 제공하지만, 게시글 상세 API와 Figma 기준 상세 화면이 연결되어 있지 않다.
- 기존 커뮤니티 댓글 화면은 V1 sheet 기반이고, 이번 상세 화면의 하단 고정 입력 바와 댓글/최신 답글 노출 구조와 다르다.
- 팬Talk/댓글 답글 상세 UI가 이미 V2에 구현되어 있으므로 같은 UI를 복사할지 재사용할지 결정이 필요하다.
- 신규 상세/댓글/답글 조회 API는 `page=0` 기준인 반면 기존 V1 커뮤니티 API 일부는 내부에서 `page - 1`을 사용하므로 API 계층을 분리해야 한다.
## 3. Goals
- 커뮤니티 목록 item tap 시 게시글 상세 화면으로 진입한다.
- 상세 진입 시 `GET /api/v2/creator-channels/community-posts/{postId}`를 호출한다.
- 상세 응답의 `comments`를 초기 댓글 목록으로 표시한다.
- 댓글 목록 추가 조회는 `GET /api/v2/creator-channels/community-posts/{postId}/comments?page={page}&size={size}`를 사용한다.
- 댓글의 답글 추가 조회는 `GET /api/v2/creator-channels/community-comments/{commentId}/replies?page={page}&size={size}`를 사용한다.
- 댓글 작성, 답글 작성, 댓글 수정, 댓글 삭제, 좋아요 API는 기존 커뮤니티 구현에서 사용하던 API를 그대로 사용한다.
- 댓글 popup menu는 기존 커뮤니티 댓글과 동일한 수정/삭제 흐름을 사용한다.
- 커뮤니티 댓글 신고는 [blocked] 상태로 구현 범위에서 제외한다. 현재 코드베이스에는 커뮤니티 댓글 신고용 `ReportRequest`/`ReportType` 식별자와 API가 없고, 기존 `CreatorCommunityCommentItemView`도 댓글 신고 UI를 노출하지 않는다.
- 댓글의 답글 상세 UI는 팬Talk 답글 상세와 동일하므로 `CreatorChannelReplyDetailView` 계열을 복사하지 않고 재사용/확장한다.
- `isCommentAvailable == false`이면 reaction bar의 댓글 icon/count, 댓글 목록, 하단 댓글 입력 바를 숨긴다.
- 하단 댓글 입력 바는 화면 하단에 고정하고 키보드 표시 시 키보드 위로 밀려 올라와야 한다.
- 댓글 입력창은 최대 3줄까지 자동 확장하고, 4줄 이상은 3줄 높이를 유지한 채 내부 vertical scroll로 처리한다.
- 좋아요 상태에 따라 `ic_feed_community_heart_fill` 또는 `ic_feed_community_heart`를 표시하고 tap 시 기존 좋아요 API를 호출한다.
## 4. Non-Goals
- 커뮤니티 게시글 작성/수정 화면을 변경하지 않는다.
- 댓글/답글 작성, 수정, 삭제, 좋아요 mutation API를 신규 V2 endpoint로 교체하지 않는다.
- 기존 V1 커뮤니티 댓글 화면 전체를 삭제하거나 일괄 리팩터링하지 않는다.
- 팬Talk 답글 상세 UI를 별도 파일로 동일 복사하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
## 5. Target Users
- 크리에이터의 커뮤니티 게시글을 상세로 읽고 좋아요를 누르려는 사용자
- 커뮤니티 게시글에 댓글을 작성하려는 사용자
- 커뮤니티 댓글에 답글을 확인하거나 작성하려는 사용자
## 6. User Stories
- 사용자는 커뮤니티 목록에서 게시글을 터치해 상세 본문, 이미지, 좋아요 수, 댓글 수를 확인할 수 있다.
- 사용자는 댓글 입력창에 글자를 입력하면 전송 버튼이 활성화되는 것을 확인하고 댓글을 작성할 수 있다.
- 사용자는 댓글이 불가능한 게시글에서는 댓글 목록과 댓글 입력창이 보이지 않는 것을 확인할 수 있다.
- 사용자는 댓글에 최신 답글이 있으면 댓글 아래에서 답글 요약을 확인하고, 댓글을 터치해 답글 상세 화면으로 이동할 수 있다.
- 사용자는 게시글 하트를 터치해 좋아요 상태를 변경할 수 있다.
## 7. Core Requirements
### 7.1 API
신규 V2 조회 API는 `SodaLive/Sources/V2/CreatorChannel/Community/**` 하위 Repository/API에 추가한다. 기존 `CreatorCommunityApi`는 mutation 재사용 대상으로 유지하고, 신규 V2 조회 API에는 `page - 1` 보정을 적용하지 않는다.
상세:
- Method: `GET`
- Path: `/api/v2/creator-channels/community-posts/{postId}`
- Path parameter: `postId`
댓글:
- Method: `GET`
- Path: `/api/v2/creator-channels/community-posts/{postId}/comments`
- Path parameter: `postId`
- Query parameters: `page` 기본값 `0`, `size` 기본값 `20`
답글:
- Method: `GET`
- Path: `/api/v2/creator-channels/community-comments/{commentId}/replies`
- Path parameter: `commentId`
- Query parameters: `page` 기본값 `0`, `size` 기본값 `20`
응답 래퍼는 기존 V2 패턴대로 `ApiResponse<...>`로 디코딩한다. Kotlin `Long`은 기존 V2 관례대로 Swift `Int`로 선언한다.
```kotlin
data class CreatorChannelCommunityPostDetailResponse(
val postId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileUrl: String,
val createdAtUtc: String,
val content: String,
val imageUrl: String?,
val audioUrl: String?,
val price: Int,
@JsonProperty("isCommentAvailable")
val isCommentAvailable: Boolean,
val existOrdered: Boolean,
val likeCount: Int,
val commentCount: Int,
@JsonProperty("isPinned")
val isPinned: Boolean,
@JsonProperty("isLiked")
val isLiked: Boolean,
val comments: CreatorChannelCommunityCommentsResponse
)
data class CreatorChannelCommunityCommentsResponse(
val commentCount: Int,
val comments: List<CreatorChannelCommunityCommentResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
data class CreatorChannelCommunityCommentResponse(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAtUtc: String,
val latestReply: CreatorChannelCommunityReplyResponse?
)
data class CreatorChannelCommunityReplyResponse(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAtUtc: String
)
data class CreatorChannelCommunityRepliesResponse(
val replyCount: Int,
val replies: List<CreatorChannelCommunityReplyResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
```
### 7.2 Entry and navigation
- `CreatorChannelCommunityTabView`의 리스트형/썸네일형 item tap에서 상세 화면으로 진입한다.
- 상세 route는 `AppStep`에 추가하고 `postId`와 필요한 refresh callback을 전달한다.
- 상세 화면에서 back tap 시 이전 화면으로 돌아간다.
- 상세에서 좋아요/댓글 작성/댓글 삭제 등으로 목록 갱신이 필요한 경우, 상세 종료 또는 mutation 성공 시 커뮤니티 탭 첫 페이지 갱신 callback을 호출한다.
### 7.3 Screen layout
- 배경은 `Color.black`을 사용한다.
- 상단 title bar는 Figma처럼 title text 없이 back chevron만 표시한다.
- 게시글 영역은 작성자 프로필, 닉네임, 상대 시간, 본문, 이미지/오디오/잠금 영역, 좋아요/댓글 count를 표시한다.
- `createdAtUtc`는 기존 `DateParser.relativeTimeText(fromUTC:fallback:now:)` 계열 helper로 상대 시간으로 표시한다.
- `price > 0 && existOrdered == false && isOwnPost == false`이면 유료 미구매 잠금 상태로 표시한다.
- 유료 미구매 잠금 상태는 Figma처럼 `gray800` 계열 이미지 영역, 잠금 아이콘, 캐시 가격 capsule을 표시한다.
- 구매 완료 상태는 Figma `290:9266`처럼 `구매완료` capsule tag를 표시한다.
- 이미지가 있고 접근 가능한 게시글이면 이미지 또는 GIF를 표시한다.
- `audioUrl`이 있으면 기존 커뮤니티 오디오 재생 흐름을 재사용해 이미지 중앙에 재생 버튼을 표시한다.
- `isPinned == true`이면 기존 커뮤니티 탭의 pinned 표시 규칙을 따른다.
### 7.4 Reaction
- `isLiked == true`이면 `ic_feed_community_heart_fill`을 표시한다.
- `isLiked == false`이면 `ic_feed_community_heart`를 표시한다.
- 하트 tap 시 기존 `CreatorCommunityRepository.communityPostLike(postId:)`를 호출한다.
- 좋아요 tap 직후 UI는 기존 `CreatorChannelCommunityListItem`처럼 local optimistic state로 반영하고, API 실패 시 toast를 표시한 뒤 tap 직전의 `isLiked`, `likeCount` 값으로 local state를 복구한다.
- `isCommentAvailable == true`이면 댓글 icon과 `commentCount`를 표시한다.
- `isCommentAvailable == false`이면 댓글 icon/count, 댓글 목록, 하단 입력 바를 모두 숨긴다.
### 7.5 Comments
- 상세 API 응답의 `comments.comments`를 초기 댓글 목록으로 표시한다.
- 댓글 section header는 Figma처럼 `댓글 {commentCount}` 형식으로 표시한다.
- 댓글 row는 작성자 프로필, 닉네임, 상대 시간, 본문, 우측 더보기 버튼을 표시한다.
- `writerId`로 내 댓글/답글 여부를 판정하고, 게시글의 `creatorId`로 현재 사용자가 해당 커뮤니티 게시글을 쓴 크리에이터인지 판정한다.
- 댓글/답글 popup menu는 기존 커뮤니티 댓글과 동일하게 내 댓글/답글이면 `수정하기`, `삭제하기`를 표시하고, 해당 게시글을 쓴 크리에이터이면 내가 쓴 댓글/답글이 아니어도 `삭제하기`를 표시한다.
- 커뮤니티 댓글 신고는 [blocked] 상태로 구현하지 않는다. 현재 코드베이스에는 커뮤니티 댓글 신고용 `ReportRequest`/`ReportType` 식별자와 API가 없고, 기존 `CreatorCommunityCommentItemView`도 댓글 신고 UI를 노출하지 않는다.
- `latestReply != nil`이면 댓글 아래에 최신 답글 카드와 좌측 연결선을 표시한다.
- 댓글 목록 하단 도달 시 `hasNext == true`이면 다음 page를 append한다.
- 첫 페이지 재조회 시 기존 댓글 목록을 교체한다.
- 댓글을 터치하면 답글 상세 화면으로 진입한다.
### 7.6 Reply detail reuse decision
- 댓글의 답글 상세 UI는 팬Talk 상세와 동일한 UI를 가진다.
- 별도 동일 복사는 하지 않는다.
- 기존 `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift`, `CreatorChannelReplyDetailFeedView.swift`, `CreatorChannelReplyDetailInputBar.swift`를 재사용한다.
- 신규 응답 모델이 기존 `GetCommunityPostCommentListItem`과 필드가 다르므로, 구현 시 `CreatorChannelReplyDetailDisplayItem` 생성용 adapter 또는 context 확장을 추가한다.
- 답글 목록 추가 조회는 신규 `GET /api/v2/creator-channels/community-comments/{commentId}/replies`를 우선 사용한다.
- 답글 작성/수정/삭제 mutation은 사용자 요구대로 기존 커뮤니티 댓글 API를 재사용한다.
### 7.7 Comment input bar
- `isCommentAvailable == true`일 때만 하단 고정 댓글 입력 바를 표시한다.
- placeholder는 사용자 요구사항 기준 `댓글을 입력하세요`를 표시한다.
- Figma에는 `댓글을 입력하세요.`처럼 마침표가 보이나, 구현 기준은 사용자 요구 문구를 우선한다.
- 댓글 쓰기 UI에는 기존 커뮤니티 댓글과 동일한 비밀댓글 toggle을 추가한다.
- 비밀댓글 toggle은 기존 커뮤니티 댓글 sheet의 `isShowSecret` 노출 정책을 따른다.
- 비밀댓글 toggle이 노출되지 않는 경우 `isSecret == false`로 댓글 작성 API를 호출한다.
- 입력값이 없거나 공백/줄바꿈만 있으면 전송 버튼을 비활성화한다.
- 입력값이 있으면 전송 버튼을 활성화한다.
- 입력창은 1줄에서 시작하고 3줄까지 자동 확장한다.
- 4줄 이상 입력 시 3줄 높이를 유지하고 입력창 내부에서 vertical scroll 처리한다.
- 하단 입력 바는 키보드가 표시되면 키보드 위로 올라와야 한다.
- 전송 중에는 중복 tap으로 같은 댓글이 여러 번 등록되지 않도록 한다.
- 댓글 작성 성공 후 입력값을 비우고 댓글 첫 페이지 또는 상세 데이터를 재조회한다.
전송 버튼 상태:
- 비활성화: icon `ic_arrow_up_gray`, 원형 배경, `gray900`
- 활성화: icon `ic_arrow_up_white`, 원형 배경, `soda400`
### 7.8 Comment popup menu and mutation reuse
- 댓글/답글 popup menu는 기존 커뮤니티 댓글과 동일한 메뉴 정책을 따른다.
- 기존 메뉴 정책은 `CreatorCommunityCommentItemView`와 동일하게 수정은 작성자에게만 허용하고, 삭제는 작성자 또는 게시글 크리에이터에게 허용한다.
- 커뮤니티 댓글 신고는 [blocked] 상태로 구현하지 않는다. `ReportRequest`/`ReportType`에 커뮤니티 댓글 신고 식별자가 추가되기 전까지 작성자도 게시글 크리에이터도 아닌 사용자에게도 `신고`를 제공하지 않는다.
- 댓글 작성은 기존 `CreatorCommunityRepository.createCommunityPostComment(comment:postId:parentId:isSecret:)`를 사용하고, 원댓글 작성 시 `parentId == nil`, `isSecret == 비밀댓글 toggle 값`으로 호출한다.
- 답글 작성은 같은 API에 `parentId == commentId`를 전달한다.
- 댓글/답글 수정과 삭제는 기존 `CreatorCommunityRepository.modifyComment(request:)`를 사용한다.
- 좋아요는 기존 `CreatorCommunityRepository.communityPostLike(postId:)`를 사용한다.
- 댓글 신고가 필요한 경우 `ReportRequest`/`ReportType`에 커뮤니티 댓글 신고 식별자가 추가된 뒤 별도 범위에서 연결한다.
### 7.9 Pagination and state
- 상세 첫 진입은 상세 API 1회로 게시글과 초기 댓글을 함께 구성한다.
- 댓글 추가 조회는 `comments.page + 1` 기준으로 호출한다.
- 답글 상세 추가 조회는 `replies.page + 1` 기준으로 호출한다.
- 조회 중 중복 호출을 막는다.
- `hasNext == false`이면 추가 조회하지 않는다.
- pull-to-refresh가 필요하면 상세 API를 다시 호출해 게시글과 댓글 첫 페이지를 동기화한다.
## 8. Reusable V2 Candidates
- `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift`
- 게시글 header, content, 유료 잠금 overlay, 오디오 재생, 좋아요/댓글 reaction 규칙을 상세 게시글 영역 구현 후보로 검토한다.
- 목록 카드 배경/radius는 상세 화면과 다르므로 전체 View 복사보다 내부 규칙을 추출하거나 참고하는 방향이 적합하다.
- `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift`
- 유료 잠금 가격 overlay와 pinned 표시 규칙 참고 후보이다.
- `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`
- 홈/추천 카드의 좋아요 optimistic state, 잠금 overlay, 오디오 재생 흐름 참고 후보이다.
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift`
- 댓글의 답글 상세 화면 재사용 대상이다.
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Components/CreatorChannelReplyDetailFeedView.swift`
- 원댓글/답글 카드/좌측 연결선/더보기 popup 위치 계산을 재사용한다.
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Components/CreatorChannelReplyDetailInputBar.swift`
- 답글 입력 바의 send 상태와 하단 고정 구조를 참고하되, 댓글 상세 입력 바는 3줄 자동 확장 요구가 있어 별도 확장 또는 공용화가 필요하다.
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
- 댓글 row + latest reply 카드 + 좌측 연결선 구조가 Figma 댓글 row와 유사하다.
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
- `CreatorChannelActionPopup`이 수정/삭제 popup 공용 기반으로 이미 사용 중이다.
- `SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift`
- 댓글/답글 삭제 확인 dialog 후보이다.
- `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Components/CreatorChannelReplyDetailTitleBar.swift`
- back-only title bar 재사용 후보이다.
## 9. Success Criteria
- 커뮤니티 게시글 item tap 시 상세 화면으로 진입한다.
- 상세 진입 시 `GET /api/v2/creator-channels/community-posts/{postId}`가 호출된다.
- 상세 API 응답으로 게시글 본문, 이미지/오디오/잠금 상태, 좋아요 수, 댓글 수, 초기 댓글 목록이 표시된다.
- 댓글 추가 로딩 시 `GET /api/v2/creator-channels/community-posts/{postId}/comments?page={page}&size=20`가 호출된다.
- 댓글의 답글 상세 추가 로딩 시 `GET /api/v2/creator-channels/community-comments/{commentId}/replies?page={page}&size=20`가 호출된다.
- `isCommentAvailable == false`이면 reaction bar의 댓글 icon/count, 댓글 section, 하단 댓글 입력 바가 표시되지 않는다.
- 댓글 쓰기 UI에는 기존 비밀댓글 toggle이 표시 조건에 맞게 제공된다.
- 입력값이 없으면 댓글 전송 버튼은 `gray900` 배경과 `ic_arrow_up_gray` icon으로 비활성화된다.
- 입력값이 있으면 댓글 전송 버튼은 `soda400` 배경과 `ic_arrow_up_white` icon으로 활성화된다.
- 댓글 입력창은 3줄까지 높이가 늘어나고, 4줄 이상은 3줄 높이에서 내부 스크롤된다.
- 키보드가 올라오면 하단 댓글 입력 바가 키보드 위로 올라온다.
- 댓글 작성 성공 후 입력값이 비워지고 댓글 목록이 갱신된다.
- 하트 tap 시 기존 좋아요 API가 호출되고 `isLiked` 상태에 맞는 heart icon이 표시된다.
- 댓글 popup menu는 기존 커뮤니티 댓글과 동일한 수정/삭제 흐름을 사용한다.
- 게시글을 쓴 크리에이터는 내가 작성하지 않은 댓글/답글에도 `삭제하기`를 사용할 수 있다.
- 커뮤니티 댓글 신고는 [blocked] 상태이며, 현재 코드베이스에 커뮤니티 댓글 신고용 `ReportRequest`/`ReportType` 식별자와 API가 없고 기존 `CreatorCommunityCommentItemView`도 신고를 노출하지 않으므로 이번 구현의 성공 기준에서 제외한다.
- 댓글의 답글 상세 UI는 팬Talk 상세와 동일한 기존 `CreatorChannelReplyDetailView` 계열을 재사용한다.
## 10. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/**` 하위에서 우선 해결한다.
- 커뮤니티 게시글 상세 신규 View, ViewModel, 모델, 조회 Repository/API는 `SodaLive/Sources/V2/CreatorChannel/Community/**` 하위에 둔다.
- 댓글/답글 mutation은 기존 `SodaLive/Sources/Explorer/Profile/CreatorCommunity/**` repository를 호출할 수 있다.
- 신규 공용 컴포넌트가 필요하면 `SodaLive/Sources/V2/Component/**` 아래에 둔다.
- 특정 상세 화면에서만 쓰는 컴포넌트는 `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/**` 아래에 둔다.
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
## 11. Assumptions
- 상세 API의 `postId`는 기존 좋아요/댓글 작성 mutation API가 요구하는 post id와 동일하다.
- 신규 답글 API의 `commentId`는 기존 `modifyComment``createCommunityPostComment(parentId:)`에서 사용하는 comment id와 동일하다.
- `existOrdered == true`이면 유료 게시글의 구매 완료 또는 접근 가능 상태로 본다.
- 본인 게시글 판정은 기존 관례대로 `creatorId == UserDefaults.int(forKey: .userId)`를 우선 사용한다.
- 댓글 원문과 최신 답글은 서버 응답 문자열을 그대로 표시한다.
## 12. Open Questions
- 해당 없음
## 13. Verification Notes
- 2026-07-07: Figma `get_design_context``get_screenshot`으로 `290:9243`, `290:9266`을 확인했다.
- 2026-07-07: `docs/agent-guides/documentation-policy.md``docs/prd/sample-prd.md`를 확인해 신규 PRD 경로와 필수 섹션을 맞췄다.
- 2026-07-07: 기존 문서 `docs/20260705_크리에이터_채널_커뮤니티_탭/prd.md`, `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`, `docs/20260707_글_상세_답글_쓰기/prd.md`를 확인했다.
- 2026-07-07: 기존 V2 재사용 후보로 `CreatorChannelCommunityListItem`, `CreatorChannelCommunityGridItem`, `CommunityPostCard`, `CreatorChannelReplyDetailView`, `CreatorChannelReplyDetailFeedView`, `CreatorChannelReplyDetailInputBar`, `CreatorChannelFanTalkListItem`, `CreatorChannelActionPopup`, `SodaV2ActionModal`, `CreatorChannelReplyDetailTitleBar`를 확인했다.
- 2026-07-07: 기존 mutation 재사용 후보로 `CreatorCommunityRepository.communityPostLike`, `createCommunityPostComment`, `modifyComment`, `getCommunityPostDetail`, `getCommunityPostCommentList`, `getCommentReplyList`를 확인했다. 신규 V2 조회 API는 path/page 기준이 달라 V2 API에 별도 추가가 필요하다.
- 2026-07-07: 사용자 확인 사항을 반영해 댓글/답글 응답에 `writerId`를 추가하고, 댓글 쓰기 UI의 기존 비밀댓글 toggle 유지, 전송 버튼 icon의 `ic_arrow_up_gray`/`ic_arrow_up_white` 매핑, 댓글 불가능 게시글의 댓글 icon/count 숨김 정책을 확정했다.
- 2026-07-07: 기존 커뮤니티 댓글과 동일하게 수정은 작성자에게만, 삭제는 작성자 또는 게시글 크리에이터에게 제공하는 권한 규칙을 반영했다. 커뮤니티 댓글 신고는 기존 `ReportRequest`/`ReportType`에 커뮤니티 댓글 신고 식별자가 없고 기존 `CreatorCommunityCommentItemView`도 신고를 노출하지 않아 [blocked]로 정정했다.