import SwiftUI struct MainHomeFollowingContentNewsCard: View { enum Variant { case audio case photo var tagTitle: String { switch self { case .audio: return I18n.HomeFollowing.audioTag case .photo: return I18n.HomeFollowing.photoTag } } var thumbnailAspectRatio: CGFloat { switch self { case .audio: return 1 case .photo: return 88.0 / 118.0 } } var thumbnailHeight: CGFloat { switch self { case .audio: return 88 case .photo: return 118 } } } let content: FollowingContentNewsResponse let visibleFromAtUtc: String let variant: Variant let onTapContent: (Int) -> Void init( content: FollowingContentNewsResponse, visibleFromAtUtc: String, variant: Variant, onTapContent: @escaping (Int) -> Void = { _ in } ) { self.content = content self.visibleFromAtUtc = visibleFromAtUtc self.variant = variant self.onTapContent = onTapContent } var body: some View { Button { onTapContent(content.contentId) } label: { HStack(alignment: .center, spacing: SodaSpacing.s14) { thumbnail ZStack(alignment: .bottom) { VStack(alignment: .leading, spacing: SodaSpacing.s6) { creatorProfile Text(content.title) .appFont(size: 18, weight: .bold) .foregroundColor(.white) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) ZStack(alignment: .center) { ContentTagView(title: variant.tagTitle) .frame(maxWidth: .infinity, alignment: .leading) Text(relativeVisibleFromText()) .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray500) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .trailing) } .frame(height: 23) } .frame(height: variant.thumbnailHeight, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading) } .padding(SodaSpacing.s14) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14, style: .continuous)) } .buttonStyle(.plain) } private var thumbnail: some View { DownsampledKFImage( url: content.contentImageUrl.flatMap(URL.init(string:)), size: CGSize(width: 88, height: variant.thumbnailHeight) ) .background(Color.gray800) .aspectRatio(variant.thumbnailAspectRatio, contentMode: .fit) .frame(width: 88) .clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14, style: .continuous)) } private var creatorProfile: some View { HStack(spacing: SodaSpacing.s4) { DownsampledKFImage( url: URL(string: content.creatorProfileImageUrl), size: CGSize(width: 20, height: 20) ) .clipShape(Circle()) Text(content.creatorNickname) .appFont(size: 12, weight: .regular) .foregroundColor(.white) .lineLimit(1) .truncationMode(.tail) } } private func relativeVisibleFromText(now: Date = Date()) -> String { DateParser.relativeTimeText(fromUTC: visibleFromAtUtc, fallback: visibleFromAtUtc, now: now) } } private struct ContentTagView: View { let title: String var body: some View { Text(title) .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray100) .lineLimit(1) .padding(.horizontal, SodaSpacing.s4) .padding(.vertical, 2) .background(Color.gray700) .clipShape(RoundedRectangle(cornerRadius: SodaRadius.r4, style: .continuous)) } } struct MainHomeFollowingContentNewsCard_Previews: PreviewProvider { private static let previewImageUrl = "https://picsum.photos/500/500" static var previews: some View { VStack(spacing: SodaSpacing.s8) { MainHomeFollowingContentNewsCard( content: FollowingContentNewsResponse( contentId: 1, contentImageUrl: previewImageUrl, title: "콘텐츠 이름", creatorProfileImageUrl: previewImageUrl, creatorNickname: "크리에이터이름" ), visibleFromAtUtc: "2026-07-01T00:00:00Z", variant: .audio ) MainHomeFollowingContentNewsCard( content: FollowingContentNewsResponse( contentId: 2, contentImageUrl: previewImageUrl, title: "콘텐츠 이름", creatorProfileImageUrl: previewImageUrl, creatorNickname: "크리에이터이름" ), visibleFromAtUtc: "2026-07-01T00:00:00Z", variant: .photo ) } .padding(SodaSpacing.s14) .background(Color.black) .previewLayout(.sizeThatFits) } }