fix(creator): 팬Talk 답글 응답을 반영한다
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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하도록 계획과 구현을 갱신했다.
|
||||||
|
|||||||
Reference in New Issue
Block a user