import SwiftUI struct CreatorChannelFanTalkTabView: View { let creatorId: Int let isOwnCreatorChannel: Bool @ObservedObject var viewModel: CreatorChannelFanTalkViewModel let onTapWrite: () -> Void let onTapEdit: (CreatorChannelFanTalkItemResponse) -> Void let onTapDetail: (CreatorChannelFanTalkItemResponse) -> 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) } 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 }, onTapDetail: { guard isOwnCreatorChannel else { return } actionPopupFanTalk = nil onTapDetail(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) CreatorChannelActionPopup( canEdit: isMine, canDelete: isMine || isOwnCreatorChannel, onTapEdit: { actionPopupFanTalk = nil onTapEdit(fanTalk) }, onTapDelete: { actionPopupFanTalk = nil viewModel.presentDeleteDialog(fanTalkId: fanTalk.fanTalkId) }, editTitle: I18n.Explorer.editAction, deleteTitle: I18n.Explorer.deleteAction ) .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) } ) } } 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 } } }