diff --git a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift index 61495010..3281ad66 100644 --- a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift +++ b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift @@ -6,7 +6,6 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { private let communityRepository = CreatorCommunityRepository() private let v2CommunityRepository = CreatorChannelCommunityRepository() private let explorerRepository = ExplorerRepository() - private let fanTalkRepository = CreatorChannelFanTalkRepository() private var subscription = Set() private var context: CreatorChannelReplyDetailContext? private var onFanTalkRefresh: (() -> Void)? @@ -15,7 +14,6 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { private let communityReplyPageSize = 20 private var communityReplyTotalCount = 0 private var isCommunityReplyLast = false - private let fanTalkReplyPageSize = 20 @Published var replyText = "" @Published var parentItem: CreatorChannelReplyDetailDisplayItem? @@ -317,49 +315,7 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { self.applyFailureState() } } receiveValue: { [weak self] response in - self?.handleFanTalkMutationResponse(response) - } - .store(in: &subscription) - } - - - private func fetchFanTalkReplies(page: Int = 0) { - guard case .fanTalk(let creatorId, let parentFanTalk) = context else { return } - - fanTalkRepository.getFanTalks(creatorId: creatorId, page: page, size: fanTalkReplyPageSize) - .sink { [weak self] result in - guard let self else { return } - if case .failure(let error) = result { - ERROR_LOG(error.localizedDescription) - self.applyFailureState() - } - } receiveValue: { [weak self] response in - guard let self else { return } - - do { - let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) - if let data = decoded.data, decoded.success { - if let fanTalk = data.fanTalks.first(where: { $0.fanTalkId == parentFanTalk.fanTalkId }) { - self.isLoading = false - self.replies = fanTalk.latestCreatorReply.map { - CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId) - }.map { [$0] } ?? [] - } else if data.hasNext { - self.fetchFanTalkReplies(page: page + 1) - } else { - self.isLoading = false - } - } else { - self.isLoading = false - self.errorMessage = decoded.message ?? I18n.Common.commonError - self.isShowPopup = true - } - } catch { - ERROR_LOG(error.localizedDescription) - self.isLoading = false - self.errorMessage = I18n.Common.commonError - self.isShowPopup = true - } + self?.handleFanTalkDeleteResponse(response) } .store(in: &subscription) } @@ -388,11 +344,9 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { isLoading = false do { - let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) - if decoded.success { - applyLocalSuccessState() - onFanTalkRefresh?() - fetchFanTalkReplies() + let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) + if let data = decoded.data, decoded.success { + applyFanTalkReply(data) } else { errorMessage = decoded.message ?? I18n.Common.commonError isShowPopup = true @@ -404,6 +358,41 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { } } + private func handleFanTalkDeleteResponse(_ response: Moya.Response) { + isLoading = false + + do { + let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) + if decoded.success { + applyLocalSuccessState() + onFanTalkRefresh?() + } else { + errorMessage = decoded.message ?? I18n.Common.commonError + isShowPopup = true + } + } catch { + ERROR_LOG(error.localizedDescription) + errorMessage = I18n.Common.commonError + isShowPopup = true + } + } + + private func applyFanTalkReply(_ response: CreatorChannelFanTalkReplyResponse) { + let item = CreatorChannelReplyDetailDisplayItem(fanTalkReply: response, creatorId: contextCreatorId) + + if let editingItem, + let index = replies.firstIndex(where: { $0.id == editingItem.id && $0.source == editingItem.source }) { + replies[index] = item + } else { + replies = [item] + } + + replyText = "" + editingItem = nil + deletingItem = nil + onFanTalkRefresh?() + } + private func applyLocalSuccessState() { let trimmedReplyText = replyText.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/docs/20260707_글_상세_답글_쓰기/plan-task.md b/docs/20260707_글_상세_답글_쓰기/plan-task.md index 86c6336d..570ca368 100644 --- a/docs/20260707_글_상세_답글_쓰기/plan-task.md +++ b/docs/20260707_글_상세_답글_쓰기/plan-task.md @@ -240,6 +240,19 @@ - 실행 명령: `rg "onFanTalkRefresh|fetchFirstPage\\(creatorId: creatorId\\)|writeCheers|modifyCheers" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift` - 기대 결과: 팬Talk 답글 mutation 성공 후 첫 페이지 재조회 연결이 확인된다. +- [x] **Task 5.3: 팬Talk 답글 작성 응답 직접 binding 전환** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/ReplyDetail/Models/CreatorChannelReplyDetailDisplayItem.swift` + - 작업 내용: + - 팬Talk 답글 작성/수정 성공 응답을 `ApiResponse`로 decode한다. + - 성공 응답의 `data`를 현재 답글 상세 화면의 `replies`에 바로 binding한다. + - 팬Talk 전체 목록을 다시 읽어서 parent `fanTalkId`에 해당하는 답글을 찾는 재조회 경로를 mutation 성공 binding에 사용하지 않는다. + - 팬Talk 목록 갱신 callback은 상위 목록 최신화를 위해 유지한다. + - 검증 기준: + - 실행 명령: `rg "ApiResponse|applyFanTalkReply|fetchFanTalkReplies|getFanTalks" SodaLive/Sources/V2/CreatorChannel/ReplyDetail` + - 기대 결과: 팬Talk mutation 성공 응답 data를 직접 display item으로 변환하고, mutation 성공 경로에서 전체 팬Talk 재조회가 호출되지 않는다. + ### Phase 6: 문구, asset, 프로젝트 등록 - [x] **Task 6.1: I18n 문구 추가** @@ -304,3 +317,4 @@ - 2026-07-07: 답글 상세 구현, 정적 검색 검증, `SodaLive-dev` Debug 빌드 검증을 완료했다. 런타임 수동 UI 검증(Task 7.3)은 미수행 상태로 남겼다. - 2026-07-07: 리뷰 지적 사항을 반영해 팬Talk 임시 답글 row 생성을 제거하고 FanTalk 수정/삭제에 `id > 0` guard를 추가했다. 답글 action popup은 row frame 기반 위치로 변경했고, popup 문구는 `I18n.Explorer.edit`, `I18n.Common.delete`를 사용하도록 수정했다. 정적 검색, ast-grep 검색, `SodaLive-dev` Debug 빌드 검증을 완료했다. - 2026-07-07: 추가 리뷰 지적 사항을 반영해 FanTalk 답글 갱신이 parent `fanTalkId`를 찾을 때까지 `hasNext` 기반으로 페이지를 조회하도록 수정했다. 커뮤니티 답글 상세는 `totalCount` 기반 pagination과 마지막 답글 `onAppear` 추가 조회를 연결했고, 커뮤니티 댓글 item의 전체 row tap은 제거해 본문/답글 텍스트만 답글 상세로 진입하도록 제한했다. 정적 검색, ast-grep 검색, `SodaLive-dev` Debug 빌드 검증을 완료했다. +- 2026-07-09: 팬Talk 답글 작성/수정 성공 응답이 `ApiResponse`로 내려오는 변경을 반영해, 답글 상세 화면에서 전체 팬Talk 목록을 재조회하지 않고 응답 `data`를 현재 화면 답글로 즉시 binding하도록 계획과 구현을 갱신했다.