From bc9f4ed37089b0e0f0dfcf78428df1c943424d27 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 9 Jul 2026 02:54:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(creator):=20=ED=8C=ACTalk=20=EB=8B=B5?= =?UTF-8?q?=EA=B8=80=20=EC=88=98=EC=A0=95=EC=9D=84=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/I18n/I18n.swift | 12 +++++++++ .../CreatorChannelFanTalkListItem.swift | 6 ++--- .../CreatorChannelFanTalkTabResponse.swift | 15 +++++++++++ .../CreatorChannelReplyDetailView.swift | 16 ++++++++++++ .../CreatorChannelReplyDetailViewModel.swift | 26 ++++++++++++++++--- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 0e717907..75100b82 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3475,6 +3475,18 @@ If you block this user, the following features will be restricted. static var replyPlaceholder: String { pick(ko: "답글을 입력하세요", en: "Enter a reply", ja: "返信を入力してください") } + + static var replaceReplyTitle: String { + pick(ko: "답글을 수정할까요?", en: "Replace your reply?", ja: "返信を修正しますか?") + } + + static var replaceReplyMessage: String { + pick(ko: "이미 작성한 답글이 있어요. 확인을 누르면 기존 답글이 새 내용으로 수정됩니다.", en: "You already have a reply. If you continue, the existing reply will be updated with the new content.", ja: "すでに返信があります。確認すると既存の返信が新しい内容に修正されます。") + } + + static var replaceReplyConfirm: String { + pick(ko: "수정하기", en: "Update", ja: "修正する") + } } enum CreatorChannelFanTalk { diff --git a/SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift b/SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift index 6067043b..f4374ad0 100644 --- a/SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift +++ b/SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift @@ -20,7 +20,7 @@ struct CreatorChannelFanTalkListItem: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 50) .background(alignment: .topLeading) { - if fanTalk.creatorReplies.isEmpty == false { + if fanTalk.latestCreatorReply != nil { GeometryReader { proxy in CreatorChannelFanTalkReplyConnector() .stroke(Color.gray800, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) @@ -29,7 +29,7 @@ struct CreatorChannelFanTalkListItem: View { } } - if fanTalk.creatorReplies.isEmpty == false { + if fanTalk.latestCreatorReply != nil { replies .padding(.leading, 50) } @@ -95,7 +95,7 @@ struct CreatorChannelFanTalkListItem: View { private var replies: some View { VStack(alignment: .leading, spacing: SodaSpacing.s8) { - ForEach(fanTalk.creatorReplies) { reply in + if let reply = fanTalk.latestCreatorReply { replyCard(reply) } } diff --git a/SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift b/SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift index cf7531b9..2f711148 100644 --- a/SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift +++ b/SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift @@ -18,6 +18,21 @@ struct CreatorChannelFanTalkItemResponse: Decodable, Identifiable { let creatorReplies: [CreatorChannelFanTalkReplyResponse] var id: Int { fanTalkId } + + var latestCreatorReply: CreatorChannelFanTalkReplyResponse? { + creatorReplies.max { lhs, rhs in + switch (DateParser.parse(lhs.createdAtUtc), DateParser.parse(rhs.createdAtUtc)) { + case let (lhsDate?, rhsDate?): + return lhsDate < rhsDate + case (nil, _?): + return true + case (_?, nil): + return false + case (nil, nil): + return lhs.createdAtUtc < rhs.createdAtUtc + } + } + } } struct CreatorChannelFanTalkReplyResponse: Decodable, Identifiable { diff --git a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift index 29f72d2e..5dfa8d35 100644 --- a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailView.swift @@ -60,6 +60,22 @@ struct CreatorChannelReplyDetailView: View { onDimmedTap: dismissDeleteDialog ) } + + if viewModel.isShowReplaceReplyDialog { + SodaV2ActionModal( + title: I18n.CreatorChannelReplyDetail.replaceReplyTitle, + message: I18n.CreatorChannelReplyDetail.replaceReplyMessage, + button1: SodaV2ActionModalButton( + label: I18n.CreatorChannelReplyDetail.replaceReplyConfirm, + action: viewModel.confirmReplaceReply + ), + button2: SodaV2ActionModalButton( + label: I18n.Common.cancel, + action: viewModel.dismissReplaceReplyDialog + ), + onDimmedTap: viewModel.dismissReplaceReplyDialog + ) + } } .navigationBarBackButtonHidden(true) .onAppear { diff --git a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift index 88800768..61495010 100644 --- a/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift +++ b/SodaLive/Sources/V2/CreatorChannel/ReplyDetail/CreatorChannelReplyDetailViewModel.swift @@ -28,6 +28,7 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { @Published var editingItem: CreatorChannelReplyDetailDisplayItem? @Published var deletingItem: CreatorChannelReplyDetailDisplayItem? @Published var isShowDeleteDialog = false + @Published var isShowReplaceReplyDialog = false var isSendEnabled: Bool { replyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false @@ -53,9 +54,9 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { fetchCommunityReplies(reset: true) case .fanTalk(let creatorId, let parentFanTalk): parentItem = CreatorChannelReplyDetailDisplayItem(fanTalk: parentFanTalk) - replies = parentFanTalk.creatorReplies.map { + replies = parentFanTalk.latestCreatorReply.map { CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId) - } + }.map { [$0] } ?? [] } } @@ -118,11 +119,23 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { if let editingItem { modify(item: editingItem) + } else if existingFanTalkCreatorReply != nil { + isShowReplaceReplyDialog = true } else { createReply() } } + func dismissReplaceReplyDialog() { + isShowReplaceReplyDialog = false + } + + func confirmReplaceReply() { + guard let existingFanTalkCreatorReply, isLoading == false else { return } + isShowReplaceReplyDialog = false + modify(item: existingFanTalkCreatorReply) + } + func confirmDelete() { guard let deletingItem, isLoading == false else { return } isShowDeleteDialog = false @@ -328,9 +341,9 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { if let data = decoded.data, decoded.success { if let fanTalk = data.fanTalks.first(where: { $0.fanTalkId == parentFanTalk.fanTalkId }) { self.isLoading = false - self.replies = fanTalk.creatorReplies.map { + self.replies = fanTalk.latestCreatorReply.map { CreatorChannelReplyDetailDisplayItem(fanTalkReply: $0, creatorId: creatorId) - } + }.map { [$0] } ?? [] } else if data.hasNext { self.fetchFanTalkReplies(page: page + 1) } else { @@ -435,6 +448,11 @@ final class CreatorChannelReplyDetailViewModel: ObservableObject { return false } + private var existingFanTalkCreatorReply: CreatorChannelReplyDetailDisplayItem? { + guard case .fanTalk = context else { return nil } + return replies.first { $0.isCreatorReply } + } + private func communityRepliesPublisher(page: Int) -> AnyPublisher { switch context { case .community(_, _, let parentComment):