Compare commits

...

25 Commits

Author SHA1 Message Date
Yu Sung f0f1cd39b6 크리에이터만 라이브를 개설할 수 있도록 변경 2023-08-21 06:07:53 +09:00
Yu Sung c5fdfcafda 라이브 방 - 스피커 최대 10 -> 5명으로 수정 2023-08-21 05:38:53 +09:00
Yu Sung 5301aebe71 라이브 종료 - 방장이 라이브를 종료 했을 때 앱이 죽는 버그 수정 2023-08-21 05:23:27 +09:00
Yu Sung fe046e8006 라이브 방 - SwiftUI 기본 애니메이션 제거 2023-08-21 05:12:29 +09:00
Yu Sung 0daf638b68 라이브 메인 - 메시지 탭 보이도록 수정 2023-08-21 04:41:54 +09:00
Yu Sung 8b7f3fbd07 본인인증을 한 유저만 메시지 탭이 보이도록 수정 2023-08-21 04:20:13 +09:00
Yu Sung 3ab5735fde 라이브 상세 - 제목 왼쪽에 19금 표시 추가 2023-08-21 04:08:12 +09:00
Yu Sung 550bf0c1c7 로딩 애니메이션 변경 2023-08-21 03:07:57 +09:00
Yu Sung f018bd30e6 푸시토큰 업데이트 수정 2023-08-21 02:48:43 +09:00
Yu Sung 4e62d752f9 마이페이지 - 사용방법 배너 간격 수정 2023-08-21 02:28:25 +09:00
Yu Sung d93c3e8836 라이브 메인 - 이벤트 클릭 이벤트 추가 2023-08-21 01:29:01 +09:00
Yu Sung 38df12872a 라이브 방 - 입장메시지 방장만 보이도록 수정 2023-08-21 00:45:46 +09:00
Yu Sung 8c3a9e25a0 라이브 방 - 19금 방인 경우 제목 왼쪽에 19 표시 2023-08-20 23:44:18 +09:00
Yu Sung e8de5dd72b 라이브 상세 - 참여자 리스트 삭제 2023-08-20 23:24:03 +09:00
Yu Sung 30cceefbcf 소다라이브 이용방법 배너 - 라이브 탭에서 마이 탭으로 이동 2023-08-20 23:21:02 +09:00
Yu Sung ce55449f2d 캔내역 - 충전하기 버튼색 변경 2023-08-20 23:18:23 +09:00
Yu Sung 792e029f0d 캔 아이콘 변경 2023-08-20 23:14:00 +09:00
Yu Sung 6bc5356ac1 메인 하단 탭 - 라이브, 콘텐츠 순서 변경 2023-08-20 20:42:14 +09:00
Yu Sung a5b954ada2 크리에이터 채널 - 콘텐츠 영역 추가 2023-08-20 20:36:17 +09:00
Yu Sung 154b5826c6 인 앱 결제 상품 추가 2023-08-20 20:22:55 +09:00
Yu Sung 3dfe4f3b8a 19 표시 제거 2023-08-20 20:22:35 +09:00
Yu Sung d75367c78b data parsing시 이름이 일치하지 않아 에러나던 버그 수정 2023-08-20 00:08:15 +09:00
Yu Sung 78f80cebd5 팔로잉 채널 전체 리스트 페이지 추가 2023-08-19 23:14:54 +09:00
Yu Sung 633e1bfd92 라이브 메인 - 추천 채널 아이템 터치시 크리에이터 채널로 이동하도록 수정 2023-08-19 23:00:46 +09:00
Yu Sung 5a771f14bd 푸시 혹은 공유링크를 타고 앱을 실행했을 때 처리되는 로직 수정 2023-08-19 22:57:10 +09:00
53 changed files with 699 additions and 323 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -94,7 +94,7 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
let roomIdString = userInfo["suda_room_id"] as? String
let roomIdString = userInfo["room_id"] as? String
let audioContentIdString = userInfo["audio_content_id"] as? String
if let roomIdString = roomIdString, let roomId = Int(roomIdString), roomId > 0 {

View File

@ -58,6 +58,8 @@ enum AppStep {
case modifyContent(contentId: Int)
case contentListAll(userId: Int)
case contentDetail(contentId: Int)
case liveReservationComplete(response: MakeLiveReservationResponse)
@ -101,4 +103,6 @@ enum AppStep {
case changeNickname
case profileUpdate(refresh: () -> Void)
case followingList
}

View File

@ -8,57 +8,18 @@
import SwiftUI
struct LoadingView: View {
@State var index = 0
@State var timer = Timer.publish(every: 0.35, on: .current, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary.opacity(0.2)
.ignoresSafeArea()
ZStack {
Image("loading_1")
.resizable()
ActivityIndicatorView()
.frame(width: 100, height: 100)
.opacity(index == 0 ? 1.0 : 0.0)
Image("loading_2")
.resizable()
.frame(width: 100, height: 100)
.opacity(index == 1 ? 1.0 : 0.0)
Image("loading_3")
.resizable()
.frame(width: 100, height: 100)
.opacity(index == 2 ? 1.0 : 0.0)
Image("loading_4")
.resizable()
.frame(width: 100, height: 100)
.opacity(index == 3 ? 1.0 : 0.0)
Image("loading_5")
.resizable()
.frame(width: 100, height: 100)
.opacity(index == 4 ? 1.0 : 0.0)
}
.frame(width: 150, height: 150)
.background(Color.white)
.cornerRadius(14)
.shadow(color: Color.primary.opacity(0.07), radius: 5, x: 5, y: 5)
.shadow(color: Color.primary.opacity(0.07), radius: 5, x: -5, y: -5)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(timer) { _ in
DispatchQueue.main.async {
if index == 4 {
index = 0
} else {
index += 1
}
}
}
}
}
@ -67,3 +28,16 @@ struct LoadingView_Previews: PreviewProvider {
LoadingView()
}
}
struct ActivityIndicatorView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIActivityIndicatorView {
let activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.color = UIColor(hex: "80D8FF")
return activityIndicator
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.startAnimating()
}
}

View File

@ -22,17 +22,6 @@ struct ContentListItemView: View {
.frame(width: 66.7, height: 66.7, alignment: .top)
.clipped()
.cornerRadius(5.3)
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.clipShape(Circle())
.padding(.top, 4.3)
.padding(.leading, 4.3)
}
}
VStack(alignment: .leading, spacing: 0) {

View File

@ -0,0 +1,177 @@
//
// ContentListView.swift
// SodaLive
//
// Created by klaus on 2023/08/20.
//
import SwiftUI
struct ContentListView: View {
let userId: Int
@StateObject var viewModel = ContentListViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {
Button {
AppState.shared.back()
} label: {
Image("ic_back")
.resizable()
.frame(width: 20, height: 20)
Text("콘텐츠 전체보기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
Spacer()
}
.padding(.horizontal, 13.3)
.frame(height: 50)
.background(Color.black)
if userId == UserDefaults.int(forKey: .userId) {
Text("새로운 콘텐츠 등록하기")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color(hex: "eeeeee"))
.padding(.vertical, 17)
.frame(maxWidth: .infinity)
.background(Color(hex: "9970ff"))
.cornerRadius(5.3)
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
.onTapGesture { AppState.shared.setAppStep(step: .createContent) }
}
HStack(spacing: 13.3) {
Spacer()
Text("최신순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .NEWEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .NEWEST {
viewModel.sort = .NEWEST
}
}
Text("높은 가격순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .PRICE_HIGH ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .PRICE_HIGH {
viewModel.sort = .PRICE_HIGH
}
}
Text("낮은 가격순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .PRICE_LOW ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .PRICE_LOW {
viewModel.sort = .PRICE_LOW
}
}
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
.background(Color(hex: "161616"))
.padding(.top, 13.3)
HStack(spacing: 0) {
Text("전체")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49"))
.padding(.leading, 8)
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
.padding(.leading, 2)
Spacer()
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
ScrollViewReader { reader in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 10.7) {
ScrollerToTop(reader: reader, scrollOnChange: $viewModel.scrollToTop)
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
let audioContent = viewModel.audioContentList[index]
ContentListItemView(item: audioContent)
.contentShape(Rectangle())
.onTapGesture {
AppState
.shared
.setAppStep(
step: .contentDetail(contentId: audioContent.contentId)
)
}
.onAppear {
if index == viewModel.audioContentList.count - 1 {
viewModel.getAudioContentList()
}
}
}
}
}
}
.padding(.top, 13.3)
}
.onAppear {
viewModel.userId = userId
viewModel.getAudioContentList()
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}
}
struct ScrollerToTop: View {
let reader: ScrollViewProxy
@Binding var scrollOnChange: Bool
var body: some View {
EmptyView()
.id("topScrollPoint")
.onChange(of: scrollOnChange) { _ in
withAnimation {
reader.scrollTo("topScrollPoint", anchor: .bottom)
}
}
}
}

View File

@ -35,17 +35,6 @@ struct ContentOrderConfirmDialogView: View {
.frame(width: 88.7, height: 88.7, alignment: .center)
.clipped()
.cornerRadius(4)
if audioContent.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.clipShape(Circle())
.padding(.leading, 4.3)
.padding(.top, 4.3)
}
}
VStack(alignment: .leading, spacing: 0) {

View File

@ -24,8 +24,8 @@ struct ContentMainCurationItemView: View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(alignment: .top, spacing: 13.3) {
ForEach(0..<item.audioContents.count, id: \.self) {
let audioContent = item.audioContents[$0]
ForEach(0..<item.contents.count, id: \.self) {
let audioContent = item.contents[$0]
ContentMainItemView(item: audioContent)
}
}

View File

@ -20,17 +20,6 @@ struct ContentMainItemView: View {
.scaledToFill()
.frame(width: 133.3, height: 133.3, alignment: .top)
.cornerRadius(2.7)
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.clipShape(Circle())
.padding(.top, 4.3)
.padding(.leading, 4.3)
}
}
Text(item.title)

View File

@ -77,6 +77,7 @@ final class ContentMainViewModel: ObservableObject {
self.isShowPopup = true
}
} catch {
print(error)
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
self.isLoading = false

View File

@ -35,7 +35,7 @@ struct GetAudioContentMainItem: Decodable {
struct GetAudioContentCurationResponse: Decodable {
let title: String
let description: String
let audioContents: [GetAudioContentMainItem]
let contents: [GetAudioContentMainItem]
}
struct GetAudioContentBannerResponse: Decodable {

View File

@ -101,6 +101,9 @@ struct ContentView: View {
case .modifyContent(let contentId):
ContentModifyView(contentId: contentId)
case .contentListAll(let userId):
ContentListView(userId: userId)
case .contentDetail(let contentId):
ContentDetailView(contentId: contentId)
@ -142,6 +145,9 @@ struct ContentView: View {
case .profileUpdate(let refresh):
ProfileUpdateView(refresh: refresh)
case .followingList:
FollowCreatorView()
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)

View File

@ -70,6 +70,8 @@ struct LiveRoomPasswordDialog: View {
.foregroundColor(Color(hex: "ffffff"))
Image("ic_can")
.resizable()
.frame(width: 20, height: 20)
Text("으로 입장")
.font(.custom(Font.bold.rawValue, size: 15.3))

View File

@ -12,6 +12,7 @@ struct GetCreatorProfileResponse: Decodable {
let userDonationRanking: [UserDonationRankingResponse]
let similarCreatorList: [SimilarCreatorResponse]
let liveRoomList: [LiveRoomResponse]
let contentList: [GetAudioContentListItem]
let notice: String
let cheers: GetCheersResponse
let activitySummary: GetCreatorActivitySummary

View File

@ -24,7 +24,9 @@ struct UserProfileContentView: View {
Text("전체보기")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
.onTapGesture {}
.onTapGesture {
AppState.shared.setAppStep(step: .contentListAll(userId: userId))
}
}
if userId == UserDefaults.int(forKey: .userId) {
@ -44,7 +46,13 @@ struct UserProfileContentView: View {
let item = items[index]
ContentListItemView(item: item)
.contentShape(Rectangle())
.onTapGesture {}
.onTapGesture {
AppState
.shared
.setAppStep(
step: .contentDetail(contentId: item.contentId)
)
}
}
}
.padding(.top, 21)

View File

@ -37,17 +37,6 @@ struct UserProfileLiveView: View {
.clipped()
.cornerRadius(4.7)
if liveRoom.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.clipShape(Circle())
.padding(.top, 6.7)
.padding(.leading, 6.7)
}
if !liveRoom.isActive {
Rectangle()
.frame(width: 80, height: 116.7, alignment: .top)

View File

@ -88,6 +88,17 @@ struct UserProfileView: View {
}
}
if creatorProfile.contentList.count > 0 ||
userId == UserDefaults.int(forKey: .userId)
{
UserProfileContentView(
userId: userId,
items: creatorProfile.contentList
)
.padding(.top, 46.7)
.padding(.horizontal, 13.3)
}
if creatorProfile.liveRoomList.count > 0 {
UserProfileLiveView(
userId: userId,

View File

@ -0,0 +1,54 @@
//
// FollowCreatorItemView.swift
// SodaLive
//
// Created by klaus on 2023/08/19.
//
import SwiftUI
import Kingfisher
struct FollowCreatorItemView: View {
let creator: GetCreatorFollowingAllListItem
let onClickFollow: (Int) -> Void
let onClickUnFollow: (Int) -> Void
@State private var isFollow = true
var body: some View {
VStack(spacing: 13.3) {
HStack(spacing: 0) {
KFImage(URL(string: creator.profileImageUrl))
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
Text(creator.nickname)
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.padding(.leading, 13.3)
Spacer()
Image(isFollow ? "btn_notification_selected" : "btn_notification")
.onTapGesture {
if isFollow {
onClickUnFollow(creator.creatorId)
} else {
onClickFollow(creator.creatorId)
}
isFollow = !isFollow
}
}
Rectangle()
.foregroundColor(Color(hex: "595959"))
.frame(height: 0.5)
}
.onAppear {
isFollow = creator.isFollow
}
}
}

View File

@ -0,0 +1,19 @@
//
// FollowCreatorRepository.swift
// SodaLive
//
// Created by klaus on 2023/08/19.
//
import Foundation
import CombineMoya
import Combine
import Moya
final class FollowCreatorRepository {
private let api = MoyaProvider<LiveRecommendApi>()
func getFollowedCreatorAllList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getFollowedCreatorAllList(page: page, size: size))
}
}

View File

@ -0,0 +1,81 @@
//
// FollowCreatorView.swift
// SodaLive
//
// Created by klaus on 2023/08/19.
//
import SwiftUI
struct FollowCreatorView: View {
@StateObject var viewModel = FollowCreatorViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "팔로잉 채널리스트")
HStack(spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "dd4500"))
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
}
.padding(.horizontal, 13.3)
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 13.3) {
ForEach(0..<viewModel.creatorList.count, id: \.self) { index in
let creator = viewModel.creatorList[index]
FollowCreatorItemView(
creator: creator,
onClickFollow: { viewModel.creatorFollow(userId: $0) },
onClickUnFollow: { viewModel.creatorUnFollow(userId: $0) }
)
.padding(.horizontal, 20)
.onTapGesture {
AppState.shared
.setAppStep(step: .creatorDetail(userId: creator.creatorId))
}
.onAppear {
if index == viewModel.creatorList.count - 1 {
viewModel.getFollowedCreatorAllList()
}
}
}
}
.padding(.top, 13.3)
}
}
.onAppear {
viewModel.getFollowedCreatorAllList()
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}
}

View File

@ -0,0 +1,108 @@
//
// FollowCreatorViewModel.swift
// SodaLive
//
// Created by klaus on 2023/08/19.
//
import Foundation
import Moya
import Combine
final class FollowCreatorViewModel: ObservableObject {
private let repository = FollowCreatorRepository()
private var userRepository = UserRepository()
private var subscription = Set<AnyCancellable>()
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var creatorList = [GetCreatorFollowingAllListItem]()
@Published var totalCount = 0
var page = 1
var isLast = false
private let pageSize = 10
func getFollowedCreatorAllList() {
if (!isLast && !isLoading) {
isLoading = true
repository.getFollowedCreatorAllList(page: page, size: pageSize)
.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<GetCreatorFollowingAllListResponse>.self, from: responseData)
self.isLoading = false
if let data = decoded.data, decoded.success {
if page == 1 {
creatorList.removeAll()
}
self.totalCount = data.totalCount
if !data.items.isEmpty {
page += 1
self.creatorList.append(contentsOf: data.items)
} else {
isLast = true
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
self.isLoading = false
}
}
.store(in: &subscription)
} else {
isLoading = false
}
}
func creatorFollow(userId: Int) {
userRepository.creatorFollow(creatorId: userId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { _ in }
.store(in: &subscription)
}
func creatorUnFollow(userId: Int) {
userRepository.creatorUnFollow(creatorId: userId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { _ in }
.store(in: &subscription)
}
}

View File

@ -0,0 +1,20 @@
//
// GetCreatorFollowingAllListResponse.swift
// SodaLive
//
// Created by klaus on 2023/08/19.
//
import Foundation
struct GetCreatorFollowingAllListResponse: Decodable {
let totalCount: Int
let items: [GetCreatorFollowingAllListItem]
}
struct GetCreatorFollowingAllListItem: Decodable {
let creatorId: Int
let nickname: String
let profileImageUrl: String
let isFollow: Bool
}

View File

@ -34,7 +34,14 @@ class StoreManager: NSObject, ObservableObject {
products.removeAll()
let request = SKProductsRequest(productIdentifiers: [
"\(Bundle.main.bundleIdentifier!).can_100"
"\(Bundle.main.bundleIdentifier!).can_35",
"\(Bundle.main.bundleIdentifier!).can_55",
"\(Bundle.main.bundleIdentifier!).can_105",
"\(Bundle.main.bundleIdentifier!).can_350",
"\(Bundle.main.bundleIdentifier!).can_550",
"\(Bundle.main.bundleIdentifier!).can_1170",
"\(Bundle.main.bundleIdentifier!).can_3580",
"\(Bundle.main.bundleIdentifier!).can_5750",
])
request.delegate = self
request.start()

View File

@ -26,4 +26,8 @@ final class KeyboardHandler: ObservableObject {
.subscribe(on: DispatchQueue.main)
.assign(to: \.self.keyboardHeight, on: self)
}
deinit {
cancellable = nil
}
}

View File

@ -28,6 +28,7 @@ struct SectionEventBannerView: View {
.tag(index)
.onTapGesture {
if let _ = item.detailImageUrl {
AppState.shared.setAppStep(step: .eventDetail(event: item))
} else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
@ -39,6 +40,7 @@ struct SectionEventBannerView: View {
.tag(index)
.onTapGesture {
if let _ = item.detailImageUrl {
AppState.shared.setAppStep(step: .eventDetail(event: item))
} else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}

View File

@ -31,20 +31,6 @@ struct LiveView: View {
.padding(.top, 13.3)
}
if let url = URL(string: "https://blog.naver.com/sodalive_official"),
UIApplication.shared.canOpenURL(url) {
Image("img_how_to_use")
.resizable()
.frame(
width: screenSize().width,
height: (200 * screenSize().width) / 1080
)
.padding(.top, 21.3)
.onTapGesture {
UIApplication.shared.open(url)
}
}
if viewModel.recommendChannelItems.count > 0 {
SectionRecommendChannelView(
items: viewModel.isFollowingList ?
@ -99,7 +85,7 @@ struct LiveView: View {
viewModel.getSummary()
}
if !appState.isShowPlayer {
if !appState.isShowPlayer && UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
Image("btn_make_live")
.padding(.trailing, 16)
.padding(.bottom, 16)
@ -155,6 +141,44 @@ struct LiveView: View {
}
}
}
.valueChanged(value: appState.pushRoomId) { value in
DispatchQueue.main.async {
appState.setAppStep(step: .main)
if value > 0 {
viewModel.enterRoom(roomId: value)
}
}
}
.valueChanged(value: appState.pushChannelId) { value in
DispatchQueue.main.async {
appState.setAppStep(step: .main)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .creatorDetail(userId: value))
}
}
}
}
.valueChanged(value: appState.pushAudioContentId) { value in
appState.setAppStep(step: .main)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .contentDetail(contentId: value))
}
}
}
.onAppear {
if appState.pushRoomId > 0 {
viewModel.enterRoom(roomId: appState.pushRoomId)
} else if appState.pushChannelId > 0 {
appState.setAppStep(step: .creatorDetail(userId: appState.pushChannelId))
} else if appState.pushAudioContentId > 0 {
appState.setAppStep(step: .contentDetail(contentId: appState.pushAudioContentId))
}
}
}
private func onCreateSuccess(response: CreateLiveRoomResponse) {

View File

@ -22,17 +22,6 @@ struct LiveNowAllItemView: View {
.frame(width: 80, height: 116.7, alignment: .top)
.cornerRadius(4.7)
.clipped()
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.cornerRadius(20)
.padding(.top, 3.3)
.padding(.leading, 3.3)
}
}
VStack(alignment: .leading, spacing: 0) {

View File

@ -47,15 +47,6 @@ struct LiveNowItemView: View {
.resizable()
.frame(width: 20, height: 20)
}
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.clipShape(Circle())
}
}
.padding(.horizontal, 3.3)
.padding(.top, 3.3)

View File

@ -9,6 +9,7 @@ import Foundation
import Moya
enum LiveRecommendApi {
case getFollowedCreatorAllList(page: Int, size: Int)
case getFollowedChannelList
case getRecommendChannelList
case getRecommendLive
@ -21,6 +22,9 @@ extension LiveRecommendApi: TargetType {
var path: String {
switch self {
case .getFollowedCreatorAllList:
return "/live/recommend/following/channel/all/list"
case .getFollowedChannelList:
return "/live/recommend/following/channel/list"
@ -34,7 +38,8 @@ extension LiveRecommendApi: TargetType {
var method: Moya.Method {
switch self {
case .getFollowedChannelList,
case .getFollowedCreatorAllList,
.getFollowedChannelList,
.getRecommendChannelList,
.getRecommendLive:
return .get
@ -47,6 +52,15 @@ extension LiveRecommendApi: TargetType {
.getRecommendChannelList,
.getRecommendLive:
return .requestPlain
case .getFollowedCreatorAllList(let page, let size):
let parameters = [
"page": page - 1,
"size": size,
"timezone": TimeZone.current.identifier,
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
}
}

View File

@ -86,6 +86,9 @@ struct SectionRecommendChannelView: View {
.lineLimit(1)
}
.onTapGesture {
AppState.shared.setAppStep(
step: .creatorDetail(userId: item.creatorId)
)
}
}
@ -102,6 +105,7 @@ struct SectionRecommendChannelView: View {
.lineLimit(1)
}
.onTapGesture {
AppState.shared.setAppStep(step: .followingList)
}
}
}

View File

@ -22,17 +22,6 @@ struct LiveReservationAllItemView: View {
.frame(width: 80, height: 116.7, alignment: .top)
.cornerRadius(4.7)
.clipped()
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.cornerRadius(20)
.padding(.top, 3.3)
.padding(.leading, 3.3)
}
}
HStack(alignment: .top, spacing: 0) {

View File

@ -22,17 +22,6 @@ struct LiveReservationItemView: View {
.frame(width: 80, height: 116, alignment: .top)
.cornerRadius(4.7)
.clipped()
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.cornerRadius(20)
.padding(.top, 3.3)
.padding(.leading, 3.3)
}
}
HStack(alignment: .top, spacing: 0) {

View File

@ -34,17 +34,6 @@ struct MyLiveReservationItemView: View {
.frame(width: 80, height: 116, alignment: .top)
.cornerRadius(4.7)
.clipped()
if item.isAdult {
Text("19")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(Color.white)
.padding(4)
.background(Color(hex: "e53621"))
.cornerRadius(20)
.padding(.top, 3.3)
.padding(.leading, 3.3)
}
}
HStack(alignment: .center, spacing: 0) {

View File

@ -23,6 +23,8 @@ struct LiveRoomDonationChatItemView: View {
.cornerRadius(23.3)
Image("ic_can")
.resizable()
.frame(width: 20, height: 20)
}
VStack(alignment: .leading, spacing: 6.7) {

View File

@ -13,6 +13,7 @@ struct GetRoomDetailResponse: Decodable {
let title: String
let notice: String
let isPaid: Bool
let isAdult: Bool
let isPrivateRoom: Bool
let password: String?
let tags: [String]

View File

@ -75,9 +75,21 @@ struct LiveDetailView: View {
if let room = viewModel.room {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
HStack(spacing: 5.3) {
if room.isAdult {
Text("19")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "e33621"))
.padding(.horizontal, 5.3)
.padding(.vertical, 3.3)
.background(Color(hex: "601d14"))
.cornerRadius(2.6)
}
Text(room.title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
.frame(width: proxy.size.width - 26.7, alignment: .leading)
.padding(.top, 6.7)
@ -106,15 +118,6 @@ struct LiveDetailView: View {
.padding(.top, 16.7)
.frame(width: proxy.size.width - 26.7)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.padding(.top, 8)
.frame(width: proxy.size.width - 26.7)
ParticipantView(room: room)
.frame(width: proxy.size.width - 26.7)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
@ -418,88 +421,6 @@ struct LiveDetailView: View {
}
}
@ViewBuilder
private func ParticipantView(room: GetRoomDetailResponse) -> some View {
if isExpandParticipantArea {
HStack(spacing: 0) {
Text(room.channelName.isNullOrBlank() ? "예약자" : "참가자")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("\(room.numberOfParticipants)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "9970ff"))
Text("/\(room.numberOfParticipantsTotal)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
}
.padding(.top, 16.7)
LazyVGrid(columns: columns) {
ForEach(room.participatingUsers, id: \.self) { user in
VStack(spacing: 6.7) {
KFImage(URL(string: user.profileImageUrl))
.resizable()
.scaledToFill()
.frame(width: 46.7, height: 46.7, alignment: .top)
.clipShape(Circle())
Text(user.nickname)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
.lineLimit(1)
}
}
}
.padding(.top, 16.7)
} else {
let userCount = room.numberOfParticipants > 10 ? 10 : room.numberOfParticipants
HStack(spacing: -13.3) {
ForEach(0..<userCount, id: \.self) { index in
let user = room.participatingUsers[index]
KFImage(URL(string: user.profileImageUrl))
.resizable()
.scaledToFill()
.frame(width: 33.3, height: 33.3, alignment: .top)
.clipShape(Circle())
}
Spacer()
HStack(spacing: 0) {
Text("\(room.numberOfParticipants)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "9970ff"))
Text("/\(room.numberOfParticipantsTotal)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
}
}
.padding(.top, 22)
}
if room.numberOfParticipants > 0 {
HStack(spacing: 6.7) {
Image(isExpandParticipantArea ? "ic_live_detail_top" : "ic_live_detail_bottom")
.resizable()
.frame(width: 20, height: 20)
Text(isExpandParticipantArea ? "닫기" : "펼쳐보기")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb"))
}
.padding(.top, 13.3)
.onTapGesture {
isExpandParticipantArea.toggle()
}
}
}
private func hideView() {
if let close = onClickClose {
close()

View File

@ -28,7 +28,7 @@ struct LiveRoomProfileItemTitleView: View {
}
if let totalCount = totalCount {
Text("/\(totalCount > 9 ? 9 : totalCount - 1)")
Text("/\(totalCount > 4 ? 4 : totalCount - 1)")
.font(.custom(Font.medium.rawValue, size: 13))
.foregroundColor(Color(hex: "bbbbbb"))
}

View File

@ -150,7 +150,7 @@ struct LiveRoomProfilesDialogView: View {
role: listener.role,
onClickChangeListener: { _ in },
onClickInviteSpeaker: {
if viewModel.liveRoomInfo!.speakerList.count <= 9 {
if viewModel.liveRoomInfo!.speakerList.count <= 4 {
viewModel.inviteSpeaker(peerId: $0)
viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
viewModel.isShowPopup = true

View File

@ -17,6 +17,7 @@ struct GetRoomInfoResponse: Decodable {
let creatorNickname: String
let creatorProfileUrl: String
let isFollowing: Bool
let isAdult: Bool
let participantsCount: Int
let totalAvailableParticipantsCount: Int
let speakerList: [LiveRoomMember]

View File

@ -11,6 +11,7 @@ import Kingfisher
struct LiveRoomTopCreatorView: View {
let creatorId: Int
var nickname: String
var profileImageUrl: String
var isFollowing: Bool
@ -34,8 +35,10 @@ struct LiveRoomTopCreatorView: View {
Spacer()
if creatorId != UserDefaults.int(forKey: .userId) {
Image(isFollowing ? "btn_following" : "btn_follow")
.onTapGesture { onClickFollow(isFollowing) }
}
}
}
}

View File

@ -107,14 +107,27 @@ struct LiveRoomView: View {
if let liveRoomInfo = viewModel.liveRoomInfo {
ZStack {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 5.3) {
if liveRoomInfo.isAdult {
Text("19")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "e33621"))
.padding(.horizontal, 5.3)
.padding(.vertical, 3.3)
.background(Color(hex: "601d14"))
.cornerRadius(2.6)
}
Text(liveRoomInfo.title)
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "eeeeee"))
.lineLimit(1)
}
.padding(.top, 16.7)
.padding(.horizontal, 13.3)
LiveRoomTopCreatorView(
creatorId: liveRoomInfo.creatorId,
nickname: liveRoomInfo.creatorNickname,
profileImageUrl: liveRoomInfo.creatorProfileUrl,
isFollowing: liveRoomInfo.isFollowing,
@ -125,9 +138,9 @@ struct LiveRoomView: View {
},
onClickFollow: {
if $0 {
viewModel.unRegisterNotification()
viewModel.creatorUnFollow()
} else {
viewModel.registerNotification()
viewModel.creatorFollow()
}
}
)
@ -503,8 +516,8 @@ struct LiveRoomView: View {
onClickRequestSpeaker: {
viewModel.requestSpeaker()
},
registerNotification: { viewModel.registerNotification() },
unRegisterNotification: { viewModel.unRegisterNotification() },
registerNotification: { viewModel.creatorFollow() },
unRegisterNotification: { viewModel.creatorUnFollow() },
onClickProfile: {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
@ -526,8 +539,8 @@ struct LiveRoomView: View {
viewModel.setManager(userId: $0)
},
onClickReleaseManager: { viewModel.changeListener(peerId: $0, isFromManager: true) },
onClickFollow: { viewModel.registerNotification(creatorId: $0, isGetUserProfile: true) },
onClickUnFollow: { viewModel.unRegisterNotification(creatorId: $0, isGetUserProfile: true) },
onClickFollow: { viewModel.creatorFollow(creatorId: $0, isGetUserProfile: true) },
onClickUnFollow: { viewModel.creatorUnFollow(creatorId: $0, isGetUserProfile: true) },
onClickInviteSpeaker: { inviteSpeaker(peerId: $0) },
onClickChangeListener: {
viewModel.changeListener(peerId: $0)
@ -610,6 +623,9 @@ struct LiveRoomView: View {
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init())
.transaction { transaction in
transaction.animation = nil
}
.sheet(
isPresented: $viewModel.isShowShareView,
onDismiss: { viewModel.shareMessage = "" },
@ -674,7 +690,7 @@ struct LiveRoomView: View {
private func inviteSpeaker(peerId: Int) {
if viewModel.liveRoomInfo!.speakerList.count <= 9 {
if viewModel.liveRoomInfo!.speakerList.count <= 4 {
viewModel.inviteSpeaker(peerId: peerId)
self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
self.viewModel.isShowPopup = true
@ -722,7 +738,7 @@ struct LiveRoomView: View {
Spacer()
HStack(spacing: 4.7) {
Image("ic_donation_status")
Image("ic_can")
.resizable()
.frame(width: 16, height: 16)

View File

@ -162,14 +162,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
}
func quitRoom() {
let roomId = liveRoomInfo?.roomId
isLoading = true
if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
muteSpeakers.remove(at: index)
}
repository.quitRoom(roomId: roomId!)
repository.quitRoom(roomId: AppState.shared.roomId)
.sink { result in
switch result {
case .finished:
@ -236,7 +235,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
getTotalDonationCan()
if (userId > 0) {
if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) {
let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname
onSuccess(nickname)
}
@ -751,7 +750,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
.store(in: &subscription)
}
func registerNotification(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
var userId = 0
if let creatorId = creatorId {
@ -803,7 +802,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
}
}
func unRegisterNotification(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
func creatorUnFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
var userId = 0
if let creatorId = creatorId {
@ -1281,7 +1280,7 @@ extension LiveRoomViewModel: AgoraRtmDelegate {
self.popupConfirmTitle = "스피커로 초대"
self.popupConfirmAction = {
self.isShowPopup = false
if self.liveRoomInfo!.speakerList.count <= 9 {
if self.liveRoomInfo!.speakerList.count <= 4 {
self.requestSpeakerAllow(peerId)
} else {
self.errorMessage = "스피커 정원이 초과되었습니다."

View File

@ -15,31 +15,6 @@ struct BottomTabView: View {
HStack(spacing: 0) {
let tabWidth = width / 5
TabButton(
title: "라이브",
action: {
if currentTab != .live {
currentTab = .live
}
},
image: {
currentTab == .live ?
"ic_tabbar_live_selected" :
"ic_tabbar_live_normal"
},
fontName: {
currentTab == .live ?
Font.bold.rawValue :
Font.medium.rawValue
},
color: {
currentTab == .live ?
Color(hex: "9970ff") :
Color(hex: "909090")
},
width: tabWidth
)
TabButton(
title: "콘텐츠",
action: {
@ -65,6 +40,31 @@ struct BottomTabView: View {
width: tabWidth
)
TabButton(
title: "라이브",
action: {
if currentTab != .live {
currentTab = .live
}
},
image: {
currentTab == .live ?
"ic_tabbar_live_selected" :
"ic_tabbar_live_normal"
},
fontName: {
currentTab == .live ?
Font.bold.rawValue :
Font.medium.rawValue
},
color: {
currentTab == .live ?
Color(hex: "9970ff") :
Color(hex: "909090")
},
width: tabWidth
)
TabButton(
title: "탐색",
action: {

View File

@ -146,18 +146,12 @@ struct HomeView: View {
}
private func pushTokenUpdate() {
Messaging.messaging().token { token, error in
if let error = error {
DEBUG_LOG(error.localizedDescription)
} else {
if let token = token {
UserDefaults.set(token, forKey: .pushToken)
let token = UserDefaults.string(forKey: .pushToken)
if !token.trimmingCharacters(in: .whitespaces).isEmpty {
self.viewModel.pushTokenUpdate(pushToken: token)
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {

View File

@ -19,7 +19,7 @@ final class HomeViewModel: ObservableObject {
case content, live, explorer, message, mypage
}
@Published var currentTab: CurrentTab = .live
@Published var currentTab: CurrentTab = .content
func pushTokenUpdate(pushToken: String) {
userRepository.updatePushToken(pushToken: pushToken)

View File

@ -116,7 +116,7 @@ struct CanPaymentView: View {
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
Text("자세한 내용은 요즘라이브 이용약관에서 확인할 수 있습니다.")
Text("자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.fixedSize(horizontal: false, vertical: true)

View File

@ -205,7 +205,7 @@ struct CanPgPaymentView: View {
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
Text("자세한 내용은 요즘라이브 이용약관에서 확인할 수 있습니다.")
Text("자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.fixedSize(horizontal: false, vertical: true)

View File

@ -144,11 +144,11 @@ struct CanStatusView: View {
Text("충전하기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.black)
.foregroundColor(Color(hex: "1313bc"))
}
.padding(.vertical, 16)
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "fdca2f"))
.background(Color(hex: "80d8ff"))
.cornerRadius(10)
.padding(.vertical, 13.7)
.frame(width: screenSize().width)

View File

@ -20,6 +20,8 @@ struct CanCardView: View {
.foregroundColor(Color(hex: "eeeeee"))
Image("ic_can")
.resizable()
.frame(width: 20, height: 20)
Image("ic_forward")
.resizable()

View File

@ -96,15 +96,28 @@ struct MyPageView: View {
ServiceCenterButtonView()
.padding(.top, 40)
.padding(.bottom, data.isAuth ? 40 : 0)
if !data.isAuth {
AuthButtonView()
.padding(.vertical, 40)
.padding(.top, 40)
.onTapGesture {
viewModel.isShowAuthView = true
}
}
if let url = URL(string: "https://blog.naver.com/sodalive_official"),
UIApplication.shared.canOpenURL(url) {
Image("img_how_to_use")
.resizable()
.frame(
width: screenSize().width,
height: (200 * screenSize().width) / 1080
)
.padding(.vertical, 40)
.onTapGesture {
UIApplication.shared.open(url)
}
}
}
}
}

View File

@ -52,7 +52,7 @@ struct LiveReservationStatusView: View {
}
}
.onAppear {
viewModel.getSudaReservationStatus()
viewModel.getLiveReservationStatus()
}
}
}

View File

@ -26,7 +26,7 @@ final class LiveReservationStatusViewModel: ObservableObject {
@Published var isCancelComplete = false
func getSudaReservationStatus() {
func getLiveReservationStatus() {
isLoading = true
repository.getReservations(isActive: true)