fix(creator): 팬Talk 답글 응답을 반영한다

This commit is contained in:
Yu Sung
2026-07-09 07:00:16 +09:00
parent 2d636a9bb8
commit 0d9a4880ef
2 changed files with 53 additions and 50 deletions

View File

@@ -6,7 +6,6 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject {
private let communityRepository = CreatorCommunityRepository() private let communityRepository = CreatorCommunityRepository()
private let v2CommunityRepository = CreatorChannelCommunityRepository() private let v2CommunityRepository = CreatorChannelCommunityRepository()
private let explorerRepository = ExplorerRepository() private let explorerRepository = ExplorerRepository()
private let fanTalkRepository = CreatorChannelFanTalkRepository()
private var subscription = Set<AnyCancellable>() private var subscription = Set<AnyCancellable>()
private var context: CreatorChannelReplyDetailContext? private var context: CreatorChannelReplyDetailContext?
private var onFanTalkRefresh: (() -> Void)? private var onFanTalkRefresh: (() -> Void)?
@@ -15,7 +14,6 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject {
private let communityReplyPageSize = 20 private let communityReplyPageSize = 20
private var communityReplyTotalCount = 0 private var communityReplyTotalCount = 0
private var isCommunityReplyLast = false private var isCommunityReplyLast = false
private let fanTalkReplyPageSize = 20
@Published var replyText = "" @Published var replyText = ""
@Published var parentItem: CreatorChannelReplyDetailDisplayItem? @Published var parentItem: CreatorChannelReplyDetailDisplayItem?
@@ -317,49 +315,7 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject {
self.applyFailureState() self.applyFailureState()
} }
} receiveValue: { [weak self] response in } receiveValue: { [weak self] response in
self?.handleFanTalkMutationResponse(response) self?.handleFanTalkDeleteResponse(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<CreatorChannelFanTalkTabResponse>.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
}
} }
.store(in: &subscription) .store(in: &subscription)
} }
@@ -388,11 +344,9 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject {
isLoading = false isLoading = false
do { do {
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) let decoded = try JSONDecoder().decode(ApiResponse<CreatorChannelFanTalkReplyResponse>.self, from: response.data)
if decoded.success { if let data = decoded.data, decoded.success {
applyLocalSuccessState() applyFanTalkReply(data)
onFanTalkRefresh?()
fetchFanTalkReplies()
} else { } else {
errorMessage = decoded.message ?? I18n.Common.commonError errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true 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() { private func applyLocalSuccessState() {
let trimmedReplyText = replyText.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedReplyText = replyText.trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -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` - 실행 명령: `rg "onFanTalkRefresh|fetchFirstPage\\(creatorId: creatorId\\)|writeCheers|modifyCheers" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift`
- 기대 결과: 팬Talk 답글 mutation 성공 후 첫 페이지 재조회 연결이 확인된다. - 기대 결과: 팬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<CreatorChannelFanTalkReplyResponse>`로 decode한다.
- 성공 응답의 `data`를 현재 답글 상세 화면의 `replies`에 바로 binding한다.
- 팬Talk 전체 목록을 다시 읽어서 parent `fanTalkId`에 해당하는 답글을 찾는 재조회 경로를 mutation 성공 binding에 사용하지 않는다.
- 팬Talk 목록 갱신 callback은 상위 목록 최신화를 위해 유지한다.
- 검증 기준:
- 실행 명령: `rg "ApiResponse<CreatorChannelFanTalkReplyResponse>|applyFanTalkReply|fetchFanTalkReplies|getFanTalks" SodaLive/Sources/V2/CreatorChannel/ReplyDetail`
- 기대 결과: 팬Talk mutation 성공 응답 data를 직접 display item으로 변환하고, mutation 성공 경로에서 전체 팬Talk 재조회가 호출되지 않는다.
### Phase 6: 문구, asset, 프로젝트 등록 ### Phase 6: 문구, asset, 프로젝트 등록
- [x] **Task 6.1: I18n 문구 추가** - [x] **Task 6.1: I18n 문구 추가**
@@ -304,3 +317,4 @@
- 2026-07-07: 답글 상세 구현, 정적 검색 검증, `SodaLive-dev` Debug 빌드 검증을 완료했다. 런타임 수동 UI 검증(Task 7.3)은 미수행 상태로 남겼다. - 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: 리뷰 지적 사항을 반영해 팬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-07: 추가 리뷰 지적 사항을 반영해 FanTalk 답글 갱신이 parent `fanTalkId`를 찾을 때까지 `hasNext` 기반으로 페이지를 조회하도록 수정했다. 커뮤니티 답글 상세는 `totalCount` 기반 pagination과 마지막 답글 `onAppear` 추가 조회를 연결했고, 커뮤니티 댓글 item의 전체 row tap은 제거해 본문/답글 텍스트만 답글 상세로 진입하도록 제한했다. 정적 검색, ast-grep 검색, `SodaLive-dev` Debug 빌드 검증을 완료했다.
- 2026-07-09: 팬Talk 답글 작성/수정 성공 응답이 `ApiResponse<CreatorChannelFanTalkReplyResponse>`로 내려오는 변경을 반영해, 답글 상세 화면에서 전체 팬Talk 목록을 재조회하지 않고 응답 `data`를 현재 화면 답글로 즉시 binding하도록 계획과 구현을 갱신했다.