From 6fc6360f230153a7347ac013ce92a3d991d8962a Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 22 Jul 2025 05:29:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=20-=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=EC=A4=91=20UI=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/Home/HomeLiveItemView.swift | 1 + .../Sources/Live/GetRoomListResponse.swift | 1 + .../Live/Now/All/LiveNowAllItemView.swift | 1 + .../Sources/Live/Now/LiveNowItemView.swift | 1 + .../Reservation/LiveReservationItemView.swift | 217 ++++++++++++------ .../MyLiveReservationItemView.swift | 196 ++++++++++++---- .../SectionLiveReservationView.swift | 24 +- 7 files changed, 304 insertions(+), 137 deletions(-) diff --git a/SodaLive/Sources/Home/HomeLiveItemView.swift b/SodaLive/Sources/Home/HomeLiveItemView.swift index d22470a..505793e 100644 --- a/SodaLive/Sources/Home/HomeLiveItemView.swift +++ b/SodaLive/Sources/Home/HomeLiveItemView.swift @@ -67,6 +67,7 @@ struct HomeLiveItemView: View { title: "네네코 마사로네네코 마사로네네코 마사로네네코 마사로", content: "테스트", beginDateTime: "2025-08-10 15:00:00", + beginDateTimeUtc: "2025-08-10T15:00:00", numberOfParticipate: 1, numberOfPeople: 10, coverImageUrl: "https://cf.sodalive.net/live_room_cover/18038/18038-cover-8c3cb985-733d-4425-8eaf-ef753064d371-2283-1751800412922", diff --git a/SodaLive/Sources/Live/GetRoomListResponse.swift b/SodaLive/Sources/Live/GetRoomListResponse.swift index a34559a..0a2399f 100644 --- a/SodaLive/Sources/Live/GetRoomListResponse.swift +++ b/SodaLive/Sources/Live/GetRoomListResponse.swift @@ -12,6 +12,7 @@ struct GetRoomListResponse: Decodable, Hashable { let title: String let content: String let beginDateTime: String + let beginDateTimeUtc: String let numberOfParticipate: Int let numberOfPeople: Int let coverImageUrl: String diff --git a/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift b/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift index 88be69b..5cd90e1 100644 --- a/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift +++ b/SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift @@ -136,6 +136,7 @@ struct LiveNowAllItemView_Previews: PreviewProvider { title: "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest", content: "testtest", beginDateTime: "2022.05.23 Mon 03:00 PM", + beginDateTimeUtc: "2025-08-10T15:00:00", numberOfParticipate: 3, numberOfPeople: 5, coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", diff --git a/SodaLive/Sources/Live/Now/LiveNowItemView.swift b/SodaLive/Sources/Live/Now/LiveNowItemView.swift index ee24131..7e4f5b8 100644 --- a/SodaLive/Sources/Live/Now/LiveNowItemView.swift +++ b/SodaLive/Sources/Live/Now/LiveNowItemView.swift @@ -106,6 +106,7 @@ struct LiveNowItemView_Previews: PreviewProvider { title: "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest", content: "testtest", beginDateTime: "2022.05.23 Mon 03:00 PM", + beginDateTimeUtc: "2025-08-10T15:00:00", numberOfParticipate: 3, numberOfPeople: 5, coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", diff --git a/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift b/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift index 556e03b..2bab37d 100644 --- a/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift +++ b/SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift @@ -12,87 +12,153 @@ struct LiveReservationItemView: View { let item: GetRoomListResponse + @State var dateDic: [String: String] = [:] + var body: some View { - VStack(alignment: .leading, spacing: 13.3) { - HStack(spacing: 20) { - ZStack(alignment: .topLeading) { - KFImage(URL(string: item.coverImageUrl)) - .cancelOnDisappear(true) - .downsampling( - size: CGSize( - width: 80, - height: 116 - ) - ) - .resizable() - .scaledToFill() - .frame(width: 80, height: 116, alignment: .top) - .cornerRadius(4.7) - .clipped() - } + HStack(spacing: 16) { + ZStack(alignment: .topLeading) { + KFImage(URL(string: item.creatorProfileImage)) + .cancelOnDisappear(true) + .resizable() + .scaledToFill() + .frame(width: 100, height: 100, alignment: .top) + .cornerRadius(16) + .clipped() - HStack(alignment: .top, spacing: 0) { - VStack(alignment: .leading, spacing: 0) { - Text(item.beginDateTime) - .font(.custom(Font.medium.rawValue, size: 9.3)) - .foregroundColor(Color(hex: "ffd300")) - - Text(item.creatorNickname) - .font(.custom(Font.medium.rawValue, size: 11.3)) - .foregroundColor(Color(hex: "bbbbbb")) - .padding(.top, 10) - - Text(item.title) - .font(.custom(Font.medium.rawValue, size: 15.3)) - .foregroundColor(Color(hex: "e2e2e2")) - .padding(.top, 4.3) - - Spacer() - - if item.isReservation { - Text("예약완료") - .font(.custom(Font.medium.rawValue, size: 11.3)) - .foregroundColor(Color(hex: "d2d2d2")) - .padding(.horizontal, 7) - .padding(.vertical, 4) - .background(Color(hex: "533d89")) - .cornerRadius(10) - } else { - if item.price > 0 { - Text("\(item.price)캔") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor( - Color(hex: "e2e2e2") - .opacity(0.5) - ) - - } else { - Text("무료") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor( - Color(hex: "e2e2e2") - .opacity(0.5) - ) - } - } - } - - Spacer() - - if item.isPrivateRoom { - Image("ic_lock") - .resizable() - .frame(width: 20, height: 20) - } + if item.isPrivateRoom { + Image("ic_lock") + .resizable() + .frame(width: 20, height: 20) + .padding(4) + .background(Color(hex: "333333").opacity(0.7)) + .clipShape(Circle()) + .padding(.leading, 4) + .padding(.top, 4) } - .padding(.vertical, 6.7) } - Divider() - .frame(height: 1) - .background(Color(hex: "909090").opacity(0.5)) + VStack(alignment: .leading, spacing: 8) { + Text(item.creatorNickname) + .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")) + + Text("\(dateDic["time"] ?? "")") + .font(.custom(Font.preRegular.rawValue, size: 14)) + .foregroundColor(Color(hex: "98A2F6")) + } + } + + Spacer() + + VStack(alignment: .trailing, spacing: 8) { + 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.price > 0 { + HStack(spacing: 2) { + Image("ic_can_circle") + .resizable() + .frame(width: 12, height: 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) } - .frame(width: screenSize().width - 26.7, height: 130, alignment: .center) + .padding(10) + .background(Color(hex: "263238")) + .cornerRadius(16) + .onAppear { + self.dateDic = parseUtcIsoLocalDateTime(item.beginDateTimeUtc) + } + } + + 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.current + 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 + ] } } @@ -103,6 +169,7 @@ struct LiveReservationItemView_Previews: PreviewProvider { title: "test", content: "testtest", beginDateTime: "2022.05.23 Mon 03:00 PM", + beginDateTimeUtc: "2025-08-10T15:00:00", numberOfParticipate: 0, numberOfPeople: 5, coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", diff --git a/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift index 0685711..b5dddf9 100644 --- a/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift +++ b/SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift @@ -13,6 +13,8 @@ struct MyLiveReservationItemView: View { let item: GetRoomListResponse let index: Int + @State var dateDic: [String: String] = [:] + var body: some View { VStack(alignment: .leading, spacing: 8) { if index == 0 { @@ -20,68 +22,161 @@ struct MyLiveReservationItemView: View { Image("ic_mic_colored") Text("내가 개설한 라이브") - .font(.custom(Font.bold.rawValue, size: 16)) - .foregroundColor(Color(hex: "3bb9f1")) + .font(.custom(Font.preBold.rawValue, size: 18)) + .foregroundColor(Color(hex: "80D8FF")) } } - VStack(alignment: .leading, spacing: 13.3) { - HStack(alignment: .top, spacing: 20) { - ZStack(alignment: .topLeading) { - KFImage(URL(string: item.coverImageUrl)) - .cancelOnDisappear(true) - .downsampling( - size: CGSize( - width: 80, - height: 116 - ) - ) - .resizable() - .scaledToFill() - .frame(width: 80, height: 116, alignment: .top) - .cornerRadius(4.7) - .clipped() - } - - HStack(alignment: .center, spacing: 0) { - VStack(alignment: .leading, spacing: 0) { - Text(item.beginDateTime) - .font(.custom(Font.medium.rawValue, size: 9.3)) - .foregroundColor(Color(hex: "ffd300")) - - Text(item.creatorNickname) - .font(.custom(Font.medium.rawValue, size: 11.3)) - .foregroundColor(Color(hex: "bbbbbb")) - .padding(.top, 10) - - Text(item.title) - .font(.custom(Font.medium.rawValue, size: 15.3)) - .foregroundColor(Color(hex: "e2e2e2")) - .padding(.top, 4.3) - } - }.frame(height: 116) - - Spacer() + HStack(spacing: 16) { + ZStack(alignment: .topLeading) { + KFImage(URL(string: item.creatorProfileImage)) + .cancelOnDisappear(true) + .resizable() + .scaledToFill() + .frame(width: 100, height: 100, alignment: .top) + .cornerRadius(16) + .clipped() if item.isPrivateRoom { Image("ic_lock") .resizable() .frame(width: 20, height: 20) - .padding(.trailing, 13.3) - .padding(.top, 13.3) + .padding(4) + .background(Color(hex: "333333").opacity(0.7)) + .clipShape(Circle()) + .padding(.leading, 4) + .padding(.top, 4) } } - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(hex: "3bb9f1"), lineWidth: 1) - ) - Divider() - .frame(height: 1) - .background(Color(hex: "909090").opacity(0.5)) - }.frame(width: screenSize().width - 26.7) + VStack(alignment: .leading, spacing: 8) { + Text(item.creatorNickname) + .font(.custom(Font.preRegular.rawValue, size: 18)) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + + Text(item.title) + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(Color(hex: "B0BEC5")) + .lineLimit(1) + .truncationMode(.tail) + + HStack(spacing: 0) { + 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")) + + Text("\(dateDic["time"] ?? "")") + .font(.custom(Font.preRegular.rawValue, size: 14)) + .foregroundColor(Color(hex: "98A2F6")) + } + } + + Spacer() + + VStack(alignment: .trailing, spacing: 8) { + VStack(spacing: 4) { + 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.price > 0 { + HStack(spacing: 2) { + Image("ic_can_circle") + .resizable() + .frame(width: 12, height: 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) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color(hex: "3bb9f1"), lineWidth: 1) + ) + .onAppear { + self.dateDic = parseUtcIsoLocalDateTime(item.beginDateTimeUtc) + } } } + + 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.current + 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 MyLiveReservationItemView_Previews: PreviewProvider { @@ -92,11 +187,12 @@ struct MyLiveReservationItemView_Previews: PreviewProvider { title: "test", content: "testtest", beginDateTime: "2022.05.23 Mon 03:00 PM", + beginDateTimeUtc: "2025-08-10T15:00:00", numberOfParticipate: 0, numberOfPeople: 5, coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", isAdult: false, - price: 0, + price: 10000, tags: ["팬미팅", "힐링"], channelName: nil, creatorProfileImage: "https://test-cf.sodalive.net/profile/default-profile.png", diff --git a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift index 84f7f67..7c27a0d 100644 --- a/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift +++ b/SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift @@ -20,20 +20,20 @@ struct SectionLiveReservationView: View { var body: some View { VStack(spacing: 13.3) { HStack(spacing: 0) { - Text("지금 ") - .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "eeeeee")) + Text("라이브 ") + .font(.custom(Font.preBold.rawValue, size: 24)) + .foregroundColor(.button) Text("예약중") - .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "3bb9f1")) + .font(.custom(Font.preBold.rawValue, size: 24)) + .foregroundColor(.white) Spacer() if items.count > 0 { Text("전체보기") - .font(.custom(Font.light.rawValue, size: 11.3)) - .foregroundColor(Color(hex: "bbbbbb")) + .font(.custom(Font.preRegular.rawValue, size: 14)) + .foregroundColor(Color(hex: "78909C")) .onTapGesture { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { AppState.shared.setAppStep( @@ -50,11 +50,11 @@ struct SectionLiveReservationView: View { } } } - .padding(.horizontal, 13.3) - .frame(width: screenSize().width) + .padding(.horizontal, 24) + .frame(maxWidth: .infinity) if items.count > 0 { - VStack(spacing: 13.3) { + VStack(spacing: 14) { ForEach(0..