383 lines
33 KiB
Markdown
383 lines
33 KiB
Markdown
# 커뮤니티 게시글 상세 구현 계획
|
|
|
|
> **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을 표시한다.
|
|
- 반응 정보는 댓글 icon/count를 먼저, 좋아요 icon/count를 뒤에 표시한다.
|
|
- 유료 미구매 상태는 `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`에서 다음 페이지 조회를 요청한다.
|
|
- 댓글 row 사이에는 Figma 기준 `gray800` 1pt separator를 표시한다.
|
|
- 댓글이 비어 있으면 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를 표시한다.
|
|
- 댓글 가능 상세에서는 게시글 영역과 댓글 목록 사이에 Figma 기준 `gray800` 1pt separator를 표시한다.
|
|
- `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는 아직 수행하지 않았다.
|