import SwiftUI struct MainContentAudioRankingThumbnailCard: View { let item: AudioRankingItemResponse let showRankChange: Bool let columnsPerRow: Int let onTapContent: (Int) -> Void init( item: AudioRankingItemResponse, showRankChange: Bool, columnsPerRow: Int, onTapContent: @escaping (Int) -> Void = { _ in } ) { self.item = item self.showRankChange = showRankChange self.columnsPerRow = columnsPerRow self.onTapContent = onTapContent } var body: some View { Button { onTapContent(item.contentId) } label: { GeometryReader { proxy in let side = min(proxy.size.width, proxy.size.height) ZStack(alignment: .topLeading) { coverImage(size: CGSize(width: side, height: side)) LinearGradient( colors: [Color.black.opacity(0), Color.black.opacity(0.72)], startPoint: .top, endPoint: .bottom ) rankColumn(side: side) .padding(.leading, rankColumnLeadingPadding(side)) .padding(.top, rankColumnTopPadding(side)) VStack(spacing: SodaSpacing.s4) { Spacer() Text(item.title) .appFont(size: titleFontSize(side), weight: .bold) .foregroundColor(.white) .lineLimit(1) .truncationMode(.tail) Text(item.creatorNickname) .appFont(size: creatorFontSize(side), weight: .regular) .foregroundColor(Color.gray200) .lineLimit(1) .truncationMode(.tail) } .frame(maxWidth: .infinity, alignment: .center) .padding(.horizontal, textHorizontalPadding(side)) .padding(.bottom, textBottomPadding(side)) } .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } .aspectRatio(1, contentMode: .fit) } .buttonStyle(.plain) } private func coverImage(size: CGSize) -> some View { DownsampledKFImage(url: URL(string: item.coverImageUrl ?? ""), size: size) .background(Color.gray800) } private func rankColumn(side: CGFloat) -> some View { VStack(alignment: .center, spacing: rankColumnSpacing) { Text("\(item.rank)") .appFont(size: rankFontSize(side), family: .pattaya) .foregroundStyle( LinearGradient( colors: [.white, Color.gray200], startPoint: .top, endPoint: .bottom ) ) .shadow(color: Color.black.opacity(0.48), radius: 2) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .center) MainContentRankChangeBadge( showRankChange: showRankChange, rankChange: item.rankChange, isNew: item.isNew ) .frame(maxWidth: .infinity, alignment: .center) } .frame(width: rankColumnWidth(side), alignment: .center) } private var rankColumnSpacing: CGFloat { switch columnsPerRow { case 2: return -10 case 3: return -5 default: return -10 } } private func rankFontSize(_ side: CGFloat) -> CGFloat { max(32, side * 0.26) } private func titleFontSize(_ side: CGFloat) -> CGFloat { max(14, side * 0.085) } private func creatorFontSize(_ side: CGFloat) -> CGFloat { max(12, side * 0.07) } private func rankColumnWidth(_ side: CGFloat) -> CGFloat { max(49, side * 0.19) } private func rankColumnLeadingPadding(_ side: CGFloat) -> CGFloat { max(0, side * 0.035) } private func rankColumnTopPadding(_ side: CGFloat) -> CGFloat { max(-8, side * 0.02) } private func textHorizontalPadding(_ side: CGFloat) -> CGFloat { max(SodaSpacing.s8, side * 0.08) } private func textBottomPadding(_ side: CGFloat) -> CGFloat { max(SodaSpacing.s12, side * 0.08) } }