feat(content): 추천 오디오 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-06 14:03:09 +09:00
parent f3cbf42328
commit b42ca61ce4
28 changed files with 1841 additions and 460 deletions

View File

@@ -0,0 +1,231 @@
import SwiftUI
struct AudioContentThumbnailCardItem: Identifiable {
let id: Int
let title: String
let subtitle: String
let imageUrl: String?
let price: Int
let isAdult: Bool
let isPointAvailable: Bool
let isFirstContent: Bool
let isOriginalSeries: Bool
}
struct AudioContentThumbnailCard: View {
let item: AudioContentThumbnailCardItem
let width: CGFloat
let titleTypography: SodaTypography
let subtitleTypography: SodaTypography
let labelHorizontalPadding: CGFloat
init(
item: AudioContentThumbnailCardItem,
size: AudioContentCardSize
) {
self.item = item
self.width = size.width
self.titleTypography = size.titleTypography
self.subtitleTypography = size.subtitleTypography
self.labelHorizontalPadding = size.labelHorizontalPadding
}
init(
item: AudioContentThumbnailCardItem,
width: CGFloat,
titleTypography: SodaTypography = .heading4,
subtitleTypography: SodaTypography = .body5,
labelHorizontalPadding: CGFloat = 0
) {
self.item = item
self.width = width
self.titleTypography = titleTypography
self.subtitleTypography = subtitleTypography
self.labelHorizontalPadding = labelHorizontalPadding
}
var body: some View {
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
thumbnail
labelContent
}
.frame(width: width, alignment: .leading)
}
private var thumbnail: some View {
ZStack(alignment: .topLeading) {
DownsampledKFImage(
url: item.imageUrl.flatMap(URL.init(string:)),
size: CGSize(width: width, height: width)
)
.background(Color.gray800)
VStack(alignment: .leading, spacing: 0) {
topTagRow
Spacer(minLength: 0)
bottomTagRow
}
}
.frame(width: width, height: width)
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
}
private var labelContent: some View {
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.appFont(titleTypography)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Text(item.subtitle)
.appFont(subtitleTypography)
.foregroundColor(Color.gray500)
.lineLimit(1)
.truncationMode(.tail)
}
.frame(width: width - (labelHorizontalPadding * 2), alignment: .leading)
.padding(.horizontal, labelHorizontalPadding)
.frame(width: width, alignment: .leading)
}
@ViewBuilder
private var topTagRow: some View {
HStack(spacing: 0) {
if item.isOriginalSeries {
originalTag
}
if item.isFirstContent {
firstTag
}
Spacer(minLength: 0)
if item.isAdult {
adultTag
.padding(.top, SodaSpacing.s6)
.padding(.trailing, SodaSpacing.s6)
}
}
}
@ViewBuilder
private var bottomTagRow: some View {
HStack(spacing: 0) {
if item.isPointAvailable {
pointTag
}
if item.price == 0 {
freeTag
}
}
}
private var originalTag: some View {
Image("ic_content_tag_original")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
private var firstTag: some View {
ZStack {
Image("ic_content_tag_first_star")
.resizable()
.scaledToFit()
.frame(width: 17, height: 17)
}
.padding(4)
.background(Color(hex: "FF34B8"))
}
private var adultTag: some View {
ZStack {
Color(hex: "FF4C3C")
Image("ic_new_shield_small")
.resizable()
.scaledToFit()
.padding(2)
}
.frame(width: 18, height: 18)
.cornerRadius(4)
}
private var pointTag: some View {
Image("ic_content_tag_point")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
private var freeTag: some View {
Text("무료")
.appFont(size: 14, weight: .semibold)
.foregroundColor(.white)
.lineLimit(1)
.padding(.horizontal, SodaSpacing.s4)
.frame(height: 24)
.background(Color.soda900)
}
}
extension AudioContentThumbnailCardItem {
init(audioCard: AudioCardResponse) {
self.init(
id: audioCard.audioContentId,
title: audioCard.title,
subtitle: audioCard.creatorNickname,
imageUrl: audioCard.imageUrl,
price: audioCard.price,
isAdult: audioCard.isAdult,
isPointAvailable: audioCard.isPointAvailable,
isFirstContent: audioCard.isFirstContent,
isOriginalSeries: audioCard.isOriginalSeries
)
}
}
struct AudioContentThumbnailCard_Previews: PreviewProvider {
static var previews: some View {
HStack(alignment: .top, spacing: SodaSpacing.s14) {
AudioContentThumbnailCard(
item: AudioContentThumbnailCardItem(
id: 1,
title: "오디오 콘텐츠 제목입니다",
subtitle: "크리에이터명",
imageUrl: "https://picsum.photos/300/300",
price: 0,
isAdult: true,
isPointAvailable: true,
isFirstContent: true,
isOriginalSeries: true
),
size: .medium
)
AudioContentThumbnailCard(
item: AudioContentThumbnailCardItem(
id: 2,
title: "추천 오디오",
subtitle: "크리에이터명",
imageUrl: "https://picsum.photos/300/300",
price: 10,
isAdult: false,
isPointAvailable: false,
isFirstContent: true,
isOriginalSeries: true
),
width: 170
)
}
.padding(SodaSpacing.s20)
.background(Color.black)
.previewLayout(.sizeThatFits)
}
}