feat(creator-profile): 라이브 섹션 UI 변경

This commit is contained in:
Yu Sung
2025-10-17 06:17:36 +09:00
parent 491238a7eb
commit 3de1b2a7d6
5 changed files with 182 additions and 193 deletions

View File

@@ -61,6 +61,7 @@ struct LiveRoomResponse: Decodable {
let content: String
let isPaid: Bool
let beginDateTime: String
let beginDateTimeUtc: String
let coverImageUrl: String
let isAdult: Bool
let price: Int
@@ -68,6 +69,7 @@ struct LiveRoomResponse: Decodable {
let managerNickname: String
let isReservation: Bool
let isActive: Bool
let isPrivateRoom: Bool
}
struct GetAudioContentListResponse: Decodable {

View File

@@ -18,163 +18,136 @@ struct UserProfileLiveView: View {
var body: some View {
VStack(spacing: 13.3) {
ForEach(0..<liveRoomList.count, id: \.self) {
let liveRoom = liveRoomList[$0]
VStack(spacing: 13.3) {
HStack(spacing: 20) {
ZStack(alignment: .topLeading) {
KFImage(URL(string: liveRoom.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 80,
height: 116.7
)
)
let item = liveRoomList[$0]
let dateDic = item.beginDateTimeUtc.parseUtcIsoLocalDateTime()
HStack(spacing: 16) {
ZStack(alignment: .topLeading) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.resizable()
.scaledToFill()
.frame(width: 107, height: 107, alignment: .top)
.cornerRadius(16)
.clipped()
if item.isPrivateRoom {
Image("ic_lock")
.resizable()
.scaledToFill()
.frame(width: 80, height: 116.7, alignment: .center)
.clipped()
.cornerRadius(4.7)
if !liveRoom.isActive {
Rectangle()
.frame(width: 80, height: 116.7, alignment: .top)
.foregroundColor(Color.gray90.opacity(0.5))
.cornerRadius(4.7)
}
}
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
Text(liveRoom.beginDateTime)
.font(.custom(Font.medium.rawValue, size: 9.3))
.foregroundColor(Color(hex: "ffd300"))
Text(liveRoom.managerNickname)
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color.graybb)
.padding(.top, 10)
Text(liveRoom.title)
.font(.custom(Font.medium.rawValue, size: 15.3))
.foregroundColor(Color.graye2)
.padding(.top, 6.7)
.lineLimit(1)
}
Spacer()
if liveRoom.isActive {
if liveRoom.channelName != nil {
Text("Live")
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color.mainRed)
.padding(.horizontal, 7)
.padding(.vertical, 4)
.overlay(
RoundedRectangle(cornerRadius: 3.3)
.stroke(Color.mainRed, lineWidth: 1)
)
} else {
Text("예정")
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color(hex: "fdca2f"))
.padding(.horizontal, 9)
.padding(.vertical, 4)
.overlay(
RoundedRectangle(cornerRadius: 3.3)
.stroke(Color(hex: "fdca2f"), lineWidth: 1)
)
}
} else {
Text("종료")
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color.gray77)
.padding(.horizontal, 9)
.padding(.vertical, 4)
.overlay(
RoundedRectangle(cornerRadius: 3.3)
.stroke(Color.gray77, lineWidth: 1)
)
}
}
Spacer()
if liveRoom.isActive {
if liveRoom.channelName != nil {
if liveRoom.isPaid || liveRoom.price <= 0 {
Text("지금 참여하기")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.white)
.frame(
width: screenSize().width - 26.7 - 100,
height: 36.7
)
.background(Color.mainRed3)
.cornerRadius(5.3)
.onTapGesture {
onClickParticipant(liveRoom)
}
} else {
Text("\(liveRoom.price)캔으로 지금 참여하기")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.white)
.frame(
width: screenSize().width - 26.7 - 100,
height: 36.7
)
.background(Color.mainRed3)
.cornerRadius(5.3)
.onTapGesture {
onClickParticipant(liveRoom)
}
}
} else {
if liveRoom.isReservation {
Text("예약완료")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.gray77)
.frame(
width: screenSize().width - 26.7 - 100,
height: 36.7
)
.background(Color.gray52)
.cornerRadius(5.3)
} else {
Text("\(liveRoom.price > 0 ? "\(liveRoom.price)캔으로 " : "")예약하기")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.black)
.frame(
width: screenSize().width - 26.7 - 100,
height: 36.7
)
.background(Color(hex: "fdca2f"))
.cornerRadius(5.3)
.onTapGesture {
onClickReservation(liveRoom)
}
}
}
} else {
Text("다시듣기를 지원하지 않습니다")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.gray77)
.frame(
width: screenSize().width - 26.7 - 100,
height: 36.7
)
.background(Color.gray52)
.cornerRadius(5.3)
}
.frame(width: 20, height: 20)
.padding(4)
.background(Color(hex: "333333").opacity(0.7))
.clipShape(Circle())
.padding(.leading, 4)
.padding(.top, 4)
}
}
.frame(height: 116.7)
Rectangle()
.frame(height: 1)
.foregroundColor(Color.gray90.opacity(0.5))
VStack(alignment: .leading, spacing: 8) {
Text(item.managerNickname)
.font(.custom(Font.preRegular.rawValue, size: 18))
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Text(item.title)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(1)
.truncationMode(.tail)
HStack(spacing: 4) {
Text("\(dateDic["dayOfWeek"] ?? "")")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "78909C"))
Text("|")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "78909C"))
let time = dateDic["time"] ?? ""
Text("\(item.isActive && !item.channelName.isNullOrBlank() ? "On Air" : time)")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "98A2F6"))
}
}
Spacer()
VStack(alignment: .trailing, spacing: 8) {
if item.isActive && !item.channelName.isNullOrBlank() {
Text("ON\nAIR")
.font(.custom(Font.preBold.rawValue, size: 14))
.foregroundColor(Color.white)
.frame(width: 52, height: 52)
.background(Color(hex: "ff5c49"))
.clipShape(Circle())
} else {
VStack(spacing: 0) {
Text("\(dateDic["month"] ?? "")")
.font(.custom(Font.preBold.rawValue, size: 14))
.foregroundColor(.white)
.padding(.vertical, 6)
.frame(maxWidth: .infinity)
.background(Color(hex: "FF5C49"))
.cornerRadius(16, corners: [.topLeft, .topRight])
Text("\(dateDic["day"] ?? "")")
.font(.custom(Font.preBold.rawValue, size: 14))
.foregroundColor(Color(hex: "263238"))
.padding(.vertical, 6)
.frame(maxWidth: .infinity)
.background(Color(hex: "FFFFFF"))
.cornerRadius(16, corners: [.bottomLeft, .bottomRight])
}
}
if item.isReservation {
Text("예약완료")
.font(.custom(Font.preBold.rawValue, size: 12))
.foregroundColor(Color.white)
.padding(4)
.frame(maxWidth: .infinity)
.background(Color(hex: "2E6279"))
.cornerRadius(4)
} else if item.price > 0 {
HStack(spacing: 2) {
Image("ic_can")
.resizable()
.scaledToFit()
.frame(width: 12)
Text("\(item.price)")
.font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(.white)
}
.padding(4)
.frame(maxWidth: .infinity)
.background(Color(hex: "3b5ff1"))
.cornerRadius(4)
} else {
Text("무료")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "#263238"))
.padding(4)
.frame(maxWidth: .infinity)
.background(Color.white)
.cornerRadius(4)
}
}
.frame(width: 55)
}
.padding(10)
.background(Color(hex: "263238"))
.cornerRadius(16)
.contentShape(Rectangle())
.onTapGesture {
if item.isActive && !item.channelName.isNullOrBlank() {
onClickParticipant(item)
} else {
if !item.isReservation {
onClickReservation(item)
}
}
}
}
}

View File

@@ -141,9 +141,13 @@ struct UserProfileView: View {
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.liveRoomList.count > 0 {
VStack(alignment: .leading, spacing: 14) {
Text("라이브")
.font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(Color.white)
HStack(spacing: 0) {
Text("라이브")
.font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(Color.white)
Spacer()
}
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
HStack(spacing: 8) {
@@ -193,6 +197,7 @@ struct UserProfileView: View {
)
}
}
.padding(.horizontal, 24)
}
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.contentList.count > 0 {
@@ -355,6 +360,7 @@ struct UserProfileView: View {
startDateTime: viewModel.liveStartDate,
nowDateTime: viewModel.nowDate
)
}
if viewModel.isShowPasswordDialog {
@@ -460,10 +466,18 @@ struct UserProfileView: View {
viewModel.errorMessage = message
viewModel.isShowPopup = true
}
.padding(.top, proxy.safeAreaInsets.top)
.padding(.bottom, proxy.safeAreaInsets.bottom)
.padding(.trailing, proxy.safeAreaInsets.trailing)
.padding(.leading, proxy.safeAreaInsets.leading)
}
if isShowMenuSettings {
MenuSettingsView(isShowing: $isShowMenuSettings)
.padding(.top, proxy.safeAreaInsets.top)
.padding(.bottom, proxy.safeAreaInsets.bottom)
.padding(.trailing, proxy.safeAreaInsets.trailing)
.padding(.leading, proxy.safeAreaInsets.leading)
}
}
}

View File

@@ -54,4 +54,45 @@ extension String {
let dec = NSDecimalNumber(string: self)
return formatter.string(from: dec) ?? "\(currencyCode) \(self)"
}
func parseUtcIsoLocalDateTime() -> [String: String] {
// 1. : "yyyy-MM-dd'T'HH:mm:ss"
let utcFormatter = DateFormatter()
utcFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
utcFormatter.locale = Locale.current
utcFormatter.timeZone = TimeZone(abbreviation: "UTC")
// 2. Date
guard let date = utcFormatter.date(from: self) else {
return [:] //
}
// 3~6. Formatter ( )
let localFormatter = DateFormatter()
localFormatter.locale = Locale.autoupdatingCurrent
localFormatter.timeZone = TimeZone.current
// 3. (1~12)
localFormatter.dateFormat = "M"
let month = localFormatter.string(from: date)
// 4. (1~31)
localFormatter.dateFormat = "d"
let day = localFormatter.string(from: date)
// 5. (: "Mon", "")
localFormatter.dateFormat = "E"
let dayOfWeek = localFormatter.string(from: date)
// 6. (: "AM 05:00")
localFormatter.dateFormat = "a hh:mm"
let time = localFormatter.string(from: date)
return [
"month": month,
"day": day,
"dayOfWeek": dayOfWeek,
"time": time
]
}
}

View File

@@ -117,50 +117,9 @@ struct LiveReservationItemView: View {
.background(Color(hex: "263238"))
.cornerRadius(16)
.onAppear {
self.dateDic = parseUtcIsoLocalDateTime(item.beginDateTimeUtc)
self.dateDic = item.beginDateTimeUtc.parseUtcIsoLocalDateTime()
}
}
func parseUtcIsoLocalDateTime(_ utcString: String) -> [String: String] {
// 1. : "yyyy-MM-dd'T'HH:mm:ss"
let utcFormatter = DateFormatter()
utcFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
utcFormatter.locale = Locale.current
utcFormatter.timeZone = TimeZone(abbreviation: "UTC")
// 2. Date
guard let date = utcFormatter.date(from: utcString) else {
return [:] //
}
// 3~6. Formatter ( )
let localFormatter = DateFormatter()
localFormatter.locale = Locale.autoupdatingCurrent
localFormatter.timeZone = TimeZone.current
// 3. (1~12)
localFormatter.dateFormat = "M"
let month = localFormatter.string(from: date)
// 4. (1~31)
localFormatter.dateFormat = "d"
let day = localFormatter.string(from: date)
// 5. (: "Mon", "")
localFormatter.dateFormat = "E"
let dayOfWeek = localFormatter.string(from: date)
// 6. (: "AM 05:00")
localFormatter.dateFormat = "a hh:mm"
let time = localFormatter.string(from: date)
return [
"month": month,
"day": day,
"dayOfWeek": dayOfWeek,
"time": time
]
}
}
struct LiveReservationItemView_Previews: PreviewProvider {