하트 랭킹 추가
This commit is contained in:
		| @@ -39,6 +39,7 @@ enum LiveApi { | ||||
|     case getAllMenuPreset(creatorId: Int) | ||||
|     case likeHeart(request: LiveRoomLikeHeartRequest) | ||||
|     case getTotalHeartCount(roomId: Int) | ||||
|     case heartStatus(roomId: Int) | ||||
| } | ||||
|  | ||||
| extension LiveApi: TargetType { | ||||
| @@ -137,12 +138,15 @@ extension LiveApi: TargetType { | ||||
|              | ||||
|         case .getTotalHeartCount(let roomId): | ||||
|             return "/live/room/\(roomId)/heart-total" | ||||
|              | ||||
|         case .heartStatus(let roomId): | ||||
|             return "/live/room/\(roomId)/heart-list" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo, .getRoomInfo, .donationStatus, .donationTotal, .getDonationMessageList, .getUserProfile, .getAllMenuPreset, .getTotalHeartCount: | ||||
|         case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo, .getRoomInfo, .donationStatus, .donationTotal, .getDonationMessageList, .getUserProfile, .getAllMenuPreset, .getTotalHeartCount, .heartStatus: | ||||
|             return .get | ||||
|              | ||||
|         case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut, .likeHeart: | ||||
| @@ -174,7 +178,7 @@ extension LiveApi: TargetType { | ||||
|                 parameters: parameters, | ||||
|                 encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo, .getRoomInfo, .refundDonation, .donationStatus, .donationTotal, .getUserProfile, .getTotalHeartCount: | ||||
|         case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo, .getRoomInfo, .refundDonation, .donationStatus, .donationTotal, .getUserProfile, .getTotalHeartCount, .heartStatus: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .getReservations(let isActive): | ||||
|   | ||||
| @@ -128,4 +128,8 @@ final class LiveRepository { | ||||
|     func getTotalHeartCount(roomId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getTotalHeartCount(roomId: roomId)) | ||||
|     } | ||||
|      | ||||
|     func heartStatus(roomId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.heartStatus(roomId: roomId)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,132 @@ | ||||
| // | ||||
| //  LiveRoomHeartRankingDialog.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 11/11/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct LiveRoomHeartRankingDialog: View { | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|     @Binding var isShowPopup: Bool | ||||
|      | ||||
|     let errorMessage: String | ||||
|     let isLoading: Bool | ||||
|     let columns = [GridItem(.flexible())] | ||||
|     let heartStatus: GetLiveRoomHeartListResponse? | ||||
|     let getHeartStatus: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         ZStack { | ||||
|             VStack(spacing: 0) { | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("현재 라이브 하트랭킹") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(Color.grayee) | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Image("ic_close_white") | ||||
|                         .onTapGesture { isShowing = false } | ||||
|                 } | ||||
|                  | ||||
|                 if let heartStatus = heartStatus { | ||||
|                     HStack(alignment: .center, spacing: 0) { | ||||
|                         Text("합계") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(Color.grayd2) | ||||
|                          | ||||
|                         Spacer() | ||||
|                          | ||||
|                         Text("\(heartStatus.totalHeart)") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 14)) | ||||
|                             .foregroundColor(Color.button) | ||||
|                          | ||||
|                         Text("하트") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 10.7)) | ||||
|                             .foregroundColor(Color.graybb) | ||||
|                             .padding(.leading, 4) | ||||
|                     } | ||||
|                     .padding(.horizontal, 18.7) | ||||
|                     .padding(.vertical, 10.7) | ||||
|                     .background(Color.bg) | ||||
|                     .cornerRadius(8) | ||||
|                     .padding(.top, 25) | ||||
|                      | ||||
|                     HStack(spacing: 0) { | ||||
|                         Text("전체") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                             .foregroundColor(Color.grayee) | ||||
|                          | ||||
|                         Text("\(heartStatus.totalCount)") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                             .foregroundColor(Color.button) | ||||
|                             .padding(.leading, 6.7) | ||||
|                          | ||||
|                         Text(" 명") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                             .foregroundColor(Color.gray77) | ||||
|                          | ||||
|                         Spacer() | ||||
|                     } | ||||
|                     .padding(.top, 13.3) | ||||
|                      | ||||
|                     ScrollView(showsIndicators: false) { | ||||
|                         LazyVGrid(columns: columns, spacing: 0) { | ||||
|                             ForEach(0..<heartStatus.heartList.count, id: \.self) { index in | ||||
|                                 let item = heartStatus.heartList[index] | ||||
|                                 LiveRoomHeartRankingItemView( | ||||
|                                     index: index, | ||||
|                                     item: item, | ||||
|                                     itemCount: heartStatus.totalCount | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.top, 8) | ||||
|                 } | ||||
|             } | ||||
|             .padding(20) | ||||
|             .background(Color.gray22) | ||||
|             .cornerRadius(8) | ||||
|              | ||||
|             if isLoading { | ||||
|                 LoadingView() | ||||
|             } | ||||
|         } | ||||
|         .popup(isPresented: $isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|                 Text(errorMessage) | ||||
|                     .padding(.vertical, 13.3) | ||||
|                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .background(Color.button) | ||||
|                     .foregroundColor(Color.white) | ||||
|                     .multilineTextAlignment(.leading) | ||||
|                     .cornerRadius(20) | ||||
|                     .padding(.bottom, 66.7) | ||||
|                 Spacer() | ||||
|             } | ||||
|             .onDisappear { | ||||
|                 isShowing = false | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             getHeartStatus() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     LiveRoomHeartRankingDialog( | ||||
|         isShowing: .constant(true), | ||||
|         isShowPopup: .constant(false), | ||||
|         errorMessage: "", | ||||
|         isLoading: false, | ||||
|         heartStatus: nil, | ||||
|         getHeartStatus: {} | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,130 @@ | ||||
| // | ||||
| //  LiveRoomHeartRankingItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 11/11/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct LiveRoomHeartRankingItemView: View { | ||||
|      | ||||
|     let index: Int | ||||
|     let item: GetLiveRoomHeartListItem | ||||
|     let itemCount: Int | ||||
|      | ||||
|     let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"] | ||||
|     let rankingColors = [ | ||||
|         [Color(hex: "ffdc00"), Color(hex: "ffb600")], | ||||
|         [Color(hex: "ffffff"), Color(hex: "9f9f9f")], | ||||
|         [Color(hex: "e6a77a"), Color(hex: "c67e4a")], | ||||
|         [Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)] | ||||
|     ] | ||||
|      | ||||
|     var body: some View { | ||||
|         HStack(spacing: 0) { | ||||
|             ZStack { | ||||
|                 KFImage(URL(string: item.profileImage)) | ||||
|                     .cancelOnDisappear(true) | ||||
|                     .downsampling(size: CGSize(width: 60, height: 60)) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: 60, height: 60, alignment: .top) | ||||
|                     .clipShape(Circle()) | ||||
|                     .overlay( | ||||
|                         Circle() | ||||
|                             .stroke( | ||||
|                                 AngularGradient(colors: rankingColors[index < 4 ? index : 3], center: .center), | ||||
|                                 lineWidth: 3 | ||||
|                             ) | ||||
|                     ) | ||||
|                  | ||||
|                 if index < 3 { | ||||
|                     VStack(alignment: .trailing, spacing: 0) { | ||||
|                         Spacer() | ||||
|                          | ||||
|                         Image(rankingCrawns[index]) | ||||
|                             .resizable() | ||||
|                             .frame(width: 25, height: 25) | ||||
|                     } | ||||
|                     .frame(width: 63, height: 63, alignment: .trailing) | ||||
|                 } | ||||
|             } | ||||
|             .frame(width: 63, height: 63) | ||||
|              | ||||
|             Text("\(index + 1)") | ||||
|                 .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                 .foregroundColor(Color.grayee) | ||||
|                 .padding(.leading, 20) | ||||
|                 .padding(.trailing, 13.3) | ||||
|              | ||||
|             let nickname = item.nickname.count > 10 ? "\(String(item.nickname.prefix(10)))..." : item.nickname | ||||
|             Text(nickname) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                 .foregroundColor(Color.grayee) | ||||
|              | ||||
|             Spacer() | ||||
|              | ||||
|             VStack(alignment: .trailing, spacing: 8) { | ||||
|                 if item.heart > 0 { | ||||
|                     HStack(spacing: 4) { | ||||
|                         Text("\(item.heart)") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(Color.button) | ||||
|                          | ||||
|                         Text("하트") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(Color.grayee) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .padding(.horizontal, isTop3Index(index: index) ? 20 : 0) | ||||
|         .padding(.top, getTopPadding(index: index)) | ||||
|         .padding(.bottom, getBottomPadding(index: index)) | ||||
|         .background(Color.bg.opacity(isTop3Index(index: index) ? 1 : 0)) | ||||
|         .cornerRadius(4.7, corners: cornerRadiusConers(index: index)) | ||||
|         .padding(.horizontal, isTop3Index(index: index) ? 0 : 6.7) | ||||
|     } | ||||
|      | ||||
|     private func isTop3Index(index: Int) -> Bool { | ||||
|         return index == 0 || index == 1 || index == 2 | ||||
|     } | ||||
|      | ||||
|     private func getTopPadding(index: Int) -> CGFloat { | ||||
|         if index == 0 || index == 3 { | ||||
|             return 20 | ||||
|         } else { | ||||
|             return 6.7 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func getBottomPadding(index: Int) -> CGFloat { | ||||
|         if (index == 0 && itemCount == 1) || (index == 1 && itemCount == 2) || index == 2 { | ||||
|             return 20 | ||||
|         } else { | ||||
|             return 6.7 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func cornerRadiusConers(index: Int) -> UIRectCorner { | ||||
|         if (index == 0 && itemCount == 1) { | ||||
|             return [.topLeft, .topRight, .bottomLeft, .bottomRight] | ||||
|         } else if index == 0 { | ||||
|             return [.topLeft, .topRight] | ||||
|         } else if (index == 1 && itemCount == 2) || index == 2 { | ||||
|             return [.bottomLeft, .bottomRight] | ||||
|         } else { | ||||
|             return [] | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     LiveRoomHeartRankingItemView( | ||||
|         index: 0, | ||||
|         item: GetLiveRoomHeartListItem(profileImage: "", nickname: "테스트", heart: 10), | ||||
|         itemCount: 3 | ||||
|     ) | ||||
| } | ||||
| @@ -97,6 +97,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|      | ||||
|     @Published var isShowDonationRankingPopup = false | ||||
|      | ||||
|     @Published var isShowHeartRankingPopup = false | ||||
|      | ||||
|     @Published var isSpeakerFold = false | ||||
|      | ||||
|     @Published var isShowQuitPopup = false | ||||
| @@ -130,6 +132,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     @Published var isBgOn = true | ||||
|     @Published var isSignatureOn = true | ||||
|     @Published var donationStatus: GetLiveRoomDonationStatusResponse? | ||||
|     @Published var heartStatus: GetLiveRoomHeartListResponse? | ||||
|      | ||||
|     @Published private(set) var offset: CGFloat = 0 | ||||
|     @Published private(set) var originOffset: CGFloat = 0 | ||||
| @@ -1027,6 +1030,39 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     func getHeartStatus() { | ||||
|         isLoading = true | ||||
|         repository.heartStatus(roomId: AppState.shared.roomId) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 self.isLoading = false | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomHeartListResponse>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.heartStatus = data | ||||
|                     } else { | ||||
|                         self.errorMessage = "하트 랭킹을 가져오지 못했습니다\n다시 시도해 주세요." | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.isLoading = false | ||||
|                     self.errorMessage = "하트 랭킹을 가져오지 못했습니다\n다시 시도해 주세요." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) { | ||||
|         var userId = 0 | ||||
|          | ||||
|   | ||||
| @@ -36,6 +36,7 @@ struct LiveRoomInfoGuestView: View { | ||||
|     let onClickProfile: (Int) -> Void | ||||
|     let onClickNotice: () -> Void | ||||
|     let onClickMenuPan: () -> Void | ||||
|     let onClickTotalHeart: () -> Void | ||||
|     let onClickTotalDonation: () -> Void | ||||
|     let onClickChangeListener: () -> Void | ||||
|     let onClickToggleSignature: () -> Void | ||||
| @@ -179,6 +180,7 @@ struct LiveRoomInfoGuestView: View { | ||||
|                         RoundedRectangle(cornerRadius: 5.3) | ||||
|                             .stroke(Color.graybb, lineWidth: 1) | ||||
|                     ) | ||||
|                     .onTapGesture { onClickTotalHeart() } | ||||
|                      | ||||
|                     HStack(spacing: 6.7) { | ||||
|                         Image("ic_can") | ||||
| @@ -260,6 +262,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider { | ||||
|             onClickProfile: { _ in }, | ||||
|             onClickNotice: {}, | ||||
|             onClickMenuPan: {}, | ||||
|             onClickTotalHeart: {}, | ||||
|             onClickTotalDonation: {}, | ||||
|             onClickChangeListener: {}, | ||||
|             onClickToggleSignature: {} | ||||
|   | ||||
| @@ -37,6 +37,7 @@ struct LiveRoomInfoHostView: View { | ||||
|     let onClickProfile: (Int) -> Void | ||||
|     let onClickNotice: () -> Void | ||||
|     let onClickMenuPan: () -> Void | ||||
|     let onClickTotalHeart: () -> Void | ||||
|     let onClickTotalDonation: () -> Void | ||||
|     let onClickParticipants: () -> Void | ||||
|     let onClickToggleSignature: () -> Void | ||||
| @@ -177,6 +178,7 @@ struct LiveRoomInfoHostView: View { | ||||
|                         RoundedRectangle(cornerRadius: 5.3) | ||||
|                             .stroke(Color.graybb, lineWidth: 1) | ||||
|                     ) | ||||
|                     .onTapGesture { onClickTotalHeart() } | ||||
|                      | ||||
|                     HStack(spacing: 6.7) { | ||||
|                         Image("ic_can") | ||||
| @@ -269,6 +271,7 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider { | ||||
|             onClickProfile: { _ in }, | ||||
|             onClickNotice: {}, | ||||
|             onClickMenuPan: {}, | ||||
|             onClickTotalHeart: {}, | ||||
|             onClickTotalDonation: {}, | ||||
|             onClickParticipants: {}, | ||||
|             onClickToggleSignature: {} | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  GetLiveRoomHeartListResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 11/11/24. | ||||
| // | ||||
|  | ||||
| struct GetLiveRoomHeartListResponse: Decodable { | ||||
|     let heartList: [GetLiveRoomHeartListItem] | ||||
|     let totalCount: Int | ||||
|     let totalHeart: Int | ||||
| } | ||||
|  | ||||
| struct GetLiveRoomHeartListItem: Decodable { | ||||
|     let profileImage: String | ||||
|     let nickname: String | ||||
|     let heart: Int | ||||
| } | ||||
| @@ -64,6 +64,9 @@ struct LiveRoomViewV2: View { | ||||
|                             onClickMenuPan: { | ||||
|                                 viewModel.isShowMenuPan.toggle() | ||||
|                             }, | ||||
|                             onClickTotalHeart: { | ||||
|                                 viewModel.isShowHeartRankingPopup = true | ||||
|                             }, | ||||
|                             onClickTotalDonation: { | ||||
|                                 viewModel.isShowDonationRankingPopup = true | ||||
|                             }, | ||||
| @@ -119,6 +122,9 @@ struct LiveRoomViewV2: View { | ||||
|                             onClickMenuPan: { | ||||
|                                 viewModel.isShowMenuPan.toggle() | ||||
|                             }, | ||||
|                             onClickTotalHeart: { | ||||
|                                 viewModel.isShowHeartRankingPopup = true | ||||
|                             }, | ||||
|                             onClickTotalDonation: { | ||||
|                                 viewModel.isShowDonationRankingPopup = true | ||||
|                             }, | ||||
| @@ -747,6 +753,17 @@ struct LiveRoomViewV2: View { | ||||
|         .sheet(isPresented: $viewModel.isShowDonationRankingPopup) { | ||||
|             LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup) | ||||
|         } | ||||
|         .sheet(isPresented: $viewModel.isShowHeartRankingPopup) { | ||||
|             LiveRoomHeartRankingDialog( | ||||
|                 isShowing: $viewModel.isShowHeartRankingPopup, | ||||
|                 isShowPopup: $viewModel.isShowPopup, | ||||
|                 errorMessage: viewModel.errorMessage, | ||||
|                 isLoading: viewModel.isLoading, | ||||
|                 heartStatus: viewModel.heartStatus | ||||
|             ) { | ||||
|                 viewModel.getHeartStatus() | ||||
|             } | ||||
|         } | ||||
|         .sheet(isPresented: $viewModel.isShowDonationMessagePopup) { | ||||
|             LiveRoomDonationMessageDialog(viewModel: viewModel, isShowing: $viewModel.isShowDonationMessagePopup) | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung