feat(creator): 팬Talk 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-05 00:44:03 +09:00
parent 7c98af0c7d
commit f5ead536ea
16 changed files with 1250 additions and 28 deletions

View File

@@ -0,0 +1,159 @@
import SwiftUI
struct CreatorChannelFanTalkTabView: View {
let creatorId: Int
let isOwnCreatorChannel: Bool
@ObservedObject var viewModel: CreatorChannelFanTalkViewModel
let onTapWrite: () -> Void
@State private var actionPopupFanTalk: CreatorChannelFanTalkItemResponse?
@State private var fanTalkItemFrames = [Int: CGRect]()
var body: some View {
ZStack(alignment: .topTrailing) {
content
actionPopupLayer
.zIndex(1)
}
.coordinateSpace(name: Self.coordinateSpaceName)
.background(Color.black)
.onPreferenceChange(CreatorChannelFanTalkItemFramePreferenceKey.self) { frames in
fanTalkItemFrames.merge(frames) { _, new in new }
}
.onAppear {
if viewModel.hasLoaded == false {
viewModel.fetchFirstPage(creatorId: creatorId)
}
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1)
.overlay(reportDialog)
.overlay(deleteDialog)
}
private var content: some View {
VStack(spacing: 0) {
CreatorChannelFanTalkCountBar(fanTalkCount: viewModel.fanTalkCount)
if isEmptyState {
CreatorChannelFanTalkEmptyView(
showsWriteButton: isOwnCreatorChannel == false,
onTapWrite: onTapWrite
)
} else {
LazyVStack(spacing: 0) {
ForEach(viewModel.fanTalks) { fanTalk in
fanTalkItem(fanTalk)
.onAppear {
viewModel.fetchNextPageIfNeeded(
creatorId: creatorId,
currentItem: fanTalk
)
}
}
if viewModel.isLoadingNextPage {
ProgressView()
.padding(.vertical, SodaSpacing.s20)
}
}
.padding(.bottom, SodaSpacing.s20)
}
}
}
private func fanTalkItem(_ fanTalk: CreatorChannelFanTalkItemResponse) -> some View {
let isMine = fanTalk.writerId == UserDefaults.int(forKey: .userId)
return CreatorChannelFanTalkListItem(
fanTalk: fanTalk,
isMine: isMine,
isOwnCreatorChannel: isOwnCreatorChannel,
onTapReport: {
actionPopupFanTalk = nil
viewModel.presentReportDialog(fanTalkId: fanTalk.fanTalkId)
},
onTapMore: {
actionPopupFanTalk = actionPopupFanTalk?.fanTalkId == fanTalk.fanTalkId ? nil : fanTalk
}
)
.background {
GeometryReader { proxy in
Color.clear.preference(
key: CreatorChannelFanTalkItemFramePreferenceKey.self,
value: [fanTalk.fanTalkId: proxy.frame(in: .named(Self.coordinateSpaceName))]
)
}
}
}
@ViewBuilder
private var actionPopupLayer: some View {
if let fanTalk = actionPopupFanTalk,
let itemFrame = fanTalkItemFrames[fanTalk.fanTalkId] {
let isMine = fanTalk.writerId == UserDefaults.int(forKey: .userId)
CreatorChannelFanTalkActionPopup(
isMine: isMine,
isOwnCreatorChannel: isOwnCreatorChannel,
onTapEdit: {
actionPopupFanTalk = nil
},
onTapDelete: {
actionPopupFanTalk = nil
viewModel.presentDeleteDialog(fanTalkId: fanTalk.fanTalkId)
}
)
.padding(.top, itemFrame.minY + 44)
.padding(.trailing, SodaSpacing.s14)
}
}
private var isEmptyState: Bool {
guard viewModel.hasLoaded else { return false }
return viewModel.fanTalkCount == 0
}
@ViewBuilder
private var reportDialog: some View {
if viewModel.isShowReportDialog {
CheersReportDialogView(
isShowing: $viewModel.isShowReportDialog,
confirmAction: { reason in
viewModel.report(reason: reason)
}
)
}
}
@ViewBuilder
private var deleteDialog: some View {
if viewModel.isShowDeleteDialog {
SodaDialog(
title: I18n.MemberChannel.cheersDeleteTitle,
desc: I18n.Common.confirmDeleteQuestion,
confirmButtonTitle: I18n.Common.delete,
confirmButtonAction: {
viewModel.isShowDeleteDialog = false
viewModel.deleteSelectedFanTalk(creatorId: creatorId)
},
cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: {
viewModel.isShowDeleteDialog = false
viewModel.selectedFanTalkId = 0
},
textAlignment: .center
)
}
}
private static let coordinateSpaceName = "CreatorChannelFanTalkTabView"
}
private struct CreatorChannelFanTalkItemFramePreferenceKey: PreferenceKey {
static var defaultValue: [Int: CGRect] = [:]
static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
value.merge(nextValue()) { _, new in new }
}
}