feat(creator-profile): 라이브 섹션 UI 변경
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user