import SwiftUI struct CreatorChannelNoticeSection: View { let notices: [CreatorChannelCommunityPostResponse] let onTapNotice: (Int) -> Void let onTapComment: (Int) -> Void let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void init( notices: [CreatorChannelCommunityPostResponse], onTapNotice: @escaping (Int) -> Void = { _ in }, onTapComment: @escaping (Int) -> Void = { _ in }, onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in } ) { self.notices = notices self.onTapNotice = onTapNotice self.onTapComment = onTapComment self.onTapPurchase = onTapPurchase } var body: some View { if !notices.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s14) { SectionTitle(title: I18n.CreatorChannelHome.notice) ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: SodaSpacing.s4) { ForEach(notices) { notice in CreatorChannelNoticeCard( notice: notice, onTapNotice: { onTapNotice(notice.postId) }, onTapComment: { onTapComment(notice.postId) }, onTapPurchase: { onTapPurchase(notice) } ) .frame(width: 374) } } .padding(.horizontal, SodaSpacing.s14) } } .padding(.top, SodaSpacing.s20) } } } private struct CreatorChannelNoticeCard: View { let notice: CreatorChannelCommunityPostResponse let onTapNotice: () -> Void let onTapComment: () -> Void let onTapPurchase: () -> Void @StateObject private var playManager = CreatorCommunityMediaPlayerManager.shared @StateObject private var contentPlayManager = ContentPlayManager.shared var body: some View { VStack(alignment: .leading, spacing: SodaSpacing.s14) { noticeLabel header bodyContent reactionBar } .padding(SodaSpacing.s14) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) .contentShape(Rectangle()) .onTapGesture(perform: onTapNotice) } private var noticeLabel: some View { HStack(spacing: 2) { Image("ic_pin") .resizable() .renderingMode(.template) .foregroundColor(Color(hex: "73FF01")) .scaledToFit() .frame(width: 18, height: 18) Text("Notice") .appFont(size: 14, weight: .medium) .foregroundColor(Color(hex: "73FF01")) .lineLimit(1) } } private var header: some View { HStack(spacing: SodaSpacing.s8) { DownsampledKFImage( url: URL(string: notice.creatorProfileUrl), size: CGSize(width: 42, height: 42) ) .background(Color.white.opacity(0.6)) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text(notice.creatorNickname) .appFont(size: 14, weight: .medium) .foregroundColor(.white) .lineLimit(1) .truncationMode(.tail) Text(notice.relativeTimeText()) .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray500) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) } } private var bodyContent: some View { HStack(alignment: .center, spacing: SodaSpacing.s8) { Text(notice.content) .appFont(size: 16, weight: .regular) .foregroundColor(.white) .lineLimit(4) .truncationMode(.tail) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) if notice.hasImage { thumbnail } } .frame(minHeight: 92, alignment: .center) } private var thumbnail: some View { ZStack { DownsampledKFImage( url: URL(string: notice.imageUrl ?? ""), size: CGSize(width: 92, height: 92) ) .background(Color.gray800) .frame(width: 92, height: 92) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) .blur(radius: notice.isLocked ? 12 : 0) if notice.isLocked { paidLockOverlay } else if notice.hasAudio { audioPlayButton } } } private var audioPlayButton: some View { Button { guard let audioUrl = notice.audioUrl, !audioUrl.isEmpty else { return } contentPlayManager.pauseAudio() playManager.toggleContent(item: CreatorCommunityContentItem(contentId: notice.postId, url: audioUrl)) } label: { Image(playManager.isPlaying && playManager.currentPlayingContentId == notice.postId ? "btn_audio_content_pause" : "btn_audio_content_play") .resizable() .frame(width: 48, height: 48) } .buttonStyle(.plain) } private var paidLockOverlay: some View { Button(action: onTapPurchase) { ZStack { Color.gray700.opacity(0.35) VStack(spacing: SodaSpacing.s4) { Image("ic_new_community_lock") .resizable() .renderingMode(.template) .foregroundColor(.white) .frame(width: 24, height: 24) HStack(spacing: SodaSpacing.s6) { Image("ic_bar_cash") .resizable() .frame(width: 20, height: 20) Text("\(notice.price)") .appFont(.body2) .foregroundColor(.black) } .padding(SodaSpacing.s8) .background(Color.white) .clipShape(Capsule()) } } } .buttonStyle(.plain) .frame(width: 92, height: 92) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } private var reactionBar: some View { HStack(spacing: 15) { Button(action: onTapComment) { HStack(spacing: SodaSpacing.s4) { Image("ic_feed_community_reply") .resizable() .frame(width: 18, height: 18) Text("\(notice.commentCount)") .appFont(.body2) .foregroundColor(Color.gray400) } } .buttonStyle(.plain) HStack(spacing: SodaSpacing.s4) { Image(notice.isLiked ? "ic_feed_community_heart_fill" : "ic_feed_community_heart") .resizable() .frame(width: 18, height: 18) Text("\(notice.likeCount)") .appFont(.body2) .foregroundColor(Color.gray400) } } .frame(height: 24) } } private extension CreatorChannelCommunityPostResponse { func relativeTimeText(now: Date = Date()) -> String { DateParser.relativeTimeText(fromUTC: dateUtc, fallback: dateUtc, now: now) } var hasImage: Bool { !(imageUrl ?? "").isEmpty } var hasAudio: Bool { !(audioUrl ?? "").isEmpty } var isLocked: Bool { price > 0 && !existOrdered } } struct CreatorChannelNoticeSection_Previews: PreviewProvider { private static let previewImageUrl = "https://picsum.photos/300/300" static var previews: some View { VStack(spacing: SodaSpacing.s20) { CreatorChannelNoticeSection(notices: []) CreatorChannelNoticeSection( notices: [ CreatorChannelCommunityPostResponse( postId: 1, creatorId: 10, creatorNickname: "크리에이터 이름", creatorProfileUrl: previewImageUrl, imageUrl: previewImageUrl, audioUrl: nil, content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분크리에이터가 커뮤니티에 올린 글이 보이는 부분 말줄임...", price: 0, dateUtc: "2026-07-03T03:44:00Z", existOrdered: true, likeCount: 5, commentCount: 2, isLiked: false ), CreatorChannelCommunityPostResponse( postId: 2, creatorId: 10, creatorNickname: "크리에이터 이름", creatorProfileUrl: previewImageUrl, imageUrl: nil, audioUrl: nil, content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", price: 10, dateUtc: "2026-07-03T03:42:00Z", existOrdered: false, likeCount: 3, commentCount: 1, isLiked: false ) ] ) } .background(Color.black) .previewLayout(.sizeThatFits) } }