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

View File

@@ -18,163 +18,136 @@ struct UserProfileLiveView: View {
var body: some View { var body: some View {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
ForEach(0..<liveRoomList.count, id: \.self) { ForEach(0..<liveRoomList.count, id: \.self) {
let liveRoom = liveRoomList[$0] let item = liveRoomList[$0]
VStack(spacing: 13.3) { let dateDic = item.beginDateTimeUtc.parseUtcIsoLocalDateTime()
HStack(spacing: 20) { HStack(spacing: 16) {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
KFImage(URL(string: liveRoom.coverImageUrl)) KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true) .cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 80,
height: 116.7
)
)
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 80, height: 116.7, alignment: .center) .frame(width: 107, height: 107, alignment: .top)
.cornerRadius(16)
.clipped() .clipped()
.cornerRadius(4.7)
if !liveRoom.isActive { if item.isPrivateRoom {
Rectangle() Image("ic_lock")
.frame(width: 80, height: 116.7, alignment: .top) .resizable()
.foregroundColor(Color.gray90.opacity(0.5)) .frame(width: 20, height: 20)
.cornerRadius(4.7) .padding(4)
.background(Color(hex: "333333").opacity(0.7))
.clipShape(Circle())
.padding(.leading, 4)
.padding(.top, 4)
} }
} }
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top, spacing: 0) { Text(item.managerNickname)
VStack(alignment: .leading, spacing: 0) { .font(.custom(Font.preRegular.rawValue, size: 18))
Text(liveRoom.beginDateTime) .foregroundColor(.white)
.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) .lineLimit(1)
} .truncationMode(.tail)
Spacer() Text(item.title)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(1)
.truncationMode(.tail)
if liveRoom.isActive { HStack(spacing: 4) {
if liveRoom.channelName != nil { Text("\(dateDic["dayOfWeek"] ?? "")")
Text("Live") .font(.custom(Font.preRegular.rawValue, size: 14))
.font(.custom(Font.medium.rawValue, size: 11.3)) .foregroundColor(Color(hex: "78909C"))
.foregroundColor(Color.mainRed)
.padding(.horizontal, 7) Text("|")
.padding(.vertical, 4) .font(.custom(Font.preRegular.rawValue, size: 14))
.overlay( .foregroundColor(Color(hex: "78909C"))
RoundedRectangle(cornerRadius: 3.3)
.stroke(Color.mainRed, lineWidth: 1) let time = dateDic["time"] ?? ""
) Text("\(item.isActive && !item.channelName.isNullOrBlank() ? "On Air" : time)")
} else { .font(.custom(Font.preRegular.rawValue, size: 14))
Text("예정") .foregroundColor(Color(hex: "98A2F6"))
.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() Spacer()
if liveRoom.isActive { VStack(alignment: .trailing, spacing: 8) {
if liveRoom.channelName != nil { if item.isActive && !item.channelName.isNullOrBlank() {
if liveRoom.isPaid || liveRoom.price <= 0 { Text("ON\nAIR")
Text("지금 참여하기") .font(.custom(Font.preBold.rawValue, size: 14))
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.white) .foregroundColor(Color.white)
.frame( .frame(width: 52, height: 52)
width: screenSize().width - 26.7 - 100, .background(Color(hex: "ff5c49"))
height: 36.7 .clipShape(Circle())
)
.background(Color.mainRed3)
.cornerRadius(5.3)
.onTapGesture {
onClickParticipant(liveRoom)
}
} else { } else {
Text("\(liveRoom.price)캔으로 지금 참여하기") VStack(spacing: 0) {
.font(.custom(Font.bold.rawValue, size: 13.3)) Text("\(dateDic["month"] ?? "")")
.foregroundColor(Color.white) .font(.custom(Font.preBold.rawValue, size: 14))
.frame( .foregroundColor(.white)
width: screenSize().width - 26.7 - 100, .padding(.vertical, 6)
height: 36.7 .frame(maxWidth: .infinity)
) .background(Color(hex: "FF5C49"))
.background(Color.mainRed3) .cornerRadius(16, corners: [.topLeft, .topRight])
.cornerRadius(5.3)
.onTapGesture { Text("\(dateDic["day"] ?? "")")
onClickParticipant(liveRoom) .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])
} }
} }
} else {
if liveRoom.isReservation { if item.isReservation {
Text("예약완료") Text("예약완료")
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.preBold.rawValue, size: 12))
.foregroundColor(Color.gray77) .foregroundColor(Color.white)
.frame( .padding(4)
width: screenSize().width - 26.7 - 100, .frame(maxWidth: .infinity)
height: 36.7 .background(Color(hex: "2E6279"))
) .cornerRadius(4)
.background(Color.gray52) } else if item.price > 0 {
.cornerRadius(5.3) HStack(spacing: 2) {
} else { Image("ic_can")
Text("\(liveRoom.price > 0 ? "\(liveRoom.price)캔으로 " : "")예약하기") .resizable()
.font(.custom(Font.bold.rawValue, size: 13.3)) .scaledToFit()
.foregroundColor(Color.black) .frame(width: 12)
.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(height: 116.7)
Rectangle() Text("\(item.price)")
.frame(height: 1) .font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(Color.gray90.opacity(0.5)) .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,10 +141,14 @@ struct UserProfileView: View {
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.liveRoomList.count > 0 { if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.liveRoomList.count > 0 {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 0) {
Text("라이브") Text("라이브")
.font(.custom(Font.preBold.rawValue, size: 26)) .font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(Color.white) .foregroundColor(Color.white)
Spacer()
}
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) { if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
HStack(spacing: 8) { HStack(spacing: 8) {
Text("룰렛 설정") Text("룰렛 설정")
@@ -193,6 +197,7 @@ struct UserProfileView: View {
) )
} }
} }
.padding(.horizontal, 24)
} }
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.contentList.count > 0 { if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.contentList.count > 0 {
@@ -355,6 +360,7 @@ struct UserProfileView: View {
startDateTime: viewModel.liveStartDate, startDateTime: viewModel.liveStartDate,
nowDateTime: viewModel.nowDate nowDateTime: viewModel.nowDate
) )
} }
if viewModel.isShowPasswordDialog { if viewModel.isShowPasswordDialog {
@@ -460,10 +466,18 @@ struct UserProfileView: View {
viewModel.errorMessage = message viewModel.errorMessage = message
viewModel.isShowPopup = true 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 { if isShowMenuSettings {
MenuSettingsView(isShowing: $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) let dec = NSDecimalNumber(string: self)
return formatter.string(from: dec) ?? "\(currencyCode) \(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")) .background(Color(hex: "263238"))
.cornerRadius(16) .cornerRadius(16)
.onAppear { .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 { struct LiveReservationItemView_Previews: PreviewProvider {