Compare commits
48 Commits
3c6ac1c060
...
043a583985
Author | SHA1 | Date |
---|---|---|
![]() |
043a583985 | |
![]() |
568d7f2284 | |
![]() |
fd2230dbe1 | |
![]() |
6bf5fcde7f | |
![]() |
602ee790eb | |
![]() |
523295648b | |
![]() |
6ba59ae852 | |
![]() |
c459c96aac | |
![]() |
ec8c1fdb71 | |
![]() |
44e9e07716 | |
![]() |
0a773ab99f | |
![]() |
c2172b29ae | |
![]() |
cab719c774 | |
![]() |
57abeea432 | |
![]() |
8abd4f3c87 | |
![]() |
70c478baa9 | |
![]() |
511bb11550 | |
![]() |
bcba83a8a7 | |
![]() |
83d51a525b | |
![]() |
2590f5471b | |
![]() |
250f169b42 | |
![]() |
1bf4e59eed | |
![]() |
86f0d466fa | |
![]() |
3d625a4fa0 | |
![]() |
def95286c2 | |
![]() |
ee34c9a0f8 | |
![]() |
b55d2c22f8 | |
![]() |
9f3eb8a995 | |
![]() |
f97917f407 | |
![]() |
38653247b8 | |
![]() |
feaeb275e4 | |
![]() |
70dae4f646 | |
![]() |
17c827f55e | |
![]() |
871d03b15b | |
![]() |
1e5ee80ca2 | |
![]() |
a08b463c11 | |
![]() |
93110eff8c | |
![]() |
101b04b6a9 | |
![]() |
ffbdbbaa06 | |
![]() |
75b9c76987 | |
![]() |
f4f8f47bd0 | |
![]() |
ba11b8c842 | |
![]() |
8505d444e2 | |
![]() |
51c16d49ec | |
![]() |
a3415ba8e7 | |
![]() |
e77a068a8e | |
![]() |
2f96ad1321 | |
![]() |
07b97b987b |
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"originHash" : "ea70de34a77d120c5868e05deb1fc4fa7fd97d4e8022e60ec6ff31d158171441",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c",
|
||||
"version" : "1.2022062300.0"
|
||||
"revision" : "748c7837511d0e6a507737353af268484e1745e2",
|
||||
"version" : "1.2024011601.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -41,8 +42,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2",
|
||||
"version" : "10.18.1"
|
||||
"revision" : "076b241a625e25eac22f8849be256dfb960fcdfe",
|
||||
"version" : "10.19.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -50,8 +51,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
"state" : {
|
||||
"revision" : "be49849dcba96f2b5ee550d4eceb2c0fa27dade4",
|
||||
"version" : "10.22.1"
|
||||
"revision" : "9d17b500cd98d9a7009751ad62f802e152e97021",
|
||||
"version" : "10.26.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -59,8 +60,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "482cfa4e5880f0a29f66ecfd60c5a62af28bd1f0",
|
||||
"version" : "10.22.1"
|
||||
"revision" : "16244d177c4e989f87b25e9db1012b382cfedc55",
|
||||
"version" : "10.25.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -86,8 +87,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98",
|
||||
"version" : "1.49.1"
|
||||
"revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359",
|
||||
"version" : "1.62.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -225,6 +226,15 @@
|
|||
"version" : "1.22.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-flow-layout",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/globulus/swiftui-flow-layout",
|
||||
"state" : {
|
||||
"revision" : "de7da3440c3b87ba94adfa98c698828d7746a76d",
|
||||
"version" : "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-sliders",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -235,5 +245,5 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
"version" : 3
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 996 B |
|
@ -9,7 +9,7 @@
|
|||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "btn_minus_round_rect.png",
|
||||
"filename" : "ic_select_check_black.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_select_check_black.imageset/ic_select_check_black.png
vendored
Normal file
After Width: | Height: | Size: 418 B |
|
@ -9,7 +9,7 @@
|
|||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash_bg_2024_03.png",
|
||||
"filename" : "splash_bg.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
After Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 3.1 MiB |
|
@ -9,7 +9,7 @@
|
|||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "btn_plus_round_rect.png",
|
||||
"filename" : "splash_text.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/splash/splash_text.imageset/splash_text.png
vendored
Normal file
After Width: | Height: | Size: 90 KiB |
|
@ -9,7 +9,7 @@
|
|||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash_text_2024_03.png",
|
||||
"filename" : "splash_text_2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
BIN
SodaLive/Resources/Assets.xcassets/splash/splash_text_2.imageset/splash_text_2.png
vendored
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 29 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash_text_logo_2024_03.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.5 KiB |
|
@ -37,6 +37,8 @@ class AppState: ObservableObject {
|
|||
}
|
||||
|
||||
@Published var eventPopup: EventItem? = nil
|
||||
@Published var purchasedContentId = 0
|
||||
@Published var purchasedContentOrderType = OrderType.KEEP
|
||||
|
||||
func setAppStep(step: AppStep) {
|
||||
switch step {
|
||||
|
|
|
@ -121,4 +121,12 @@ enum AppStep {
|
|||
case canCoupon
|
||||
|
||||
case contentAllByTheme(themeId: Int)
|
||||
|
||||
case seriesDetail(seriesId: Int)
|
||||
|
||||
case seriesAll(creatorId: Int)
|
||||
|
||||
case seriesContentAll(seriesId: Int, seriesTitle: String)
|
||||
|
||||
case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ struct ContentDetailCreatorProfileView: View {
|
|||
|
||||
Text(creator.nickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.horizontal, 5.3)
|
||||
|
||||
Spacer()
|
||||
|
|
|
@ -67,7 +67,7 @@ struct ContentDetailInfoView: View {
|
|||
.foregroundColor(
|
||||
orderType == .KEEP ?
|
||||
Color(hex: "b1ef2c") :
|
||||
Color(hex: "9970ff")
|
||||
Color.button
|
||||
)
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 3.3)
|
||||
|
@ -82,7 +82,7 @@ struct ContentDetailInfoView: View {
|
|||
|
||||
Text(audioContent.title)
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
.foregroundColor(Color.grayd2)
|
||||
.lineSpacing(5)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
@ -90,57 +90,59 @@ struct ContentDetailInfoView: View {
|
|||
}
|
||||
.padding(.top, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 4) {
|
||||
Image(
|
||||
audioContent.isLike ?
|
||||
"ic_audio_content_heart_pressed" :
|
||||
"ic_audio_content_heart_normal"
|
||||
)
|
||||
|
||||
Text("\(audioContent.likeCount)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color(hex: "ffffff").opacity(0.1))
|
||||
.cornerRadius(26.7)
|
||||
.onTapGesture { onClickLike() }
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image("ic_audio_content_share")
|
||||
|
||||
Text("공유")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color(hex: "ffffff").opacity(0.1))
|
||||
.cornerRadius(26.7)
|
||||
.onTapGesture { onClickShare() }
|
||||
|
||||
if audioContent.isCommentAvailable {
|
||||
if !audioContent.contentUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && audioContent.releaseDate == nil {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 4) {
|
||||
Image("ic_donation_white")
|
||||
.resizable()
|
||||
.frame(width: 13.3, height: 13.3)
|
||||
Image(
|
||||
audioContent.isLike ?
|
||||
"ic_audio_content_heart_pressed" :
|
||||
"ic_audio_content_heart_normal"
|
||||
)
|
||||
|
||||
Text("후원")
|
||||
Text("\(audioContent.likeCount)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color(hex: "ffffff").opacity(0.1))
|
||||
.cornerRadius(26.7)
|
||||
.onTapGesture { onClickDonation() }
|
||||
.onTapGesture { onClickLike() }
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image("ic_audio_content_share")
|
||||
|
||||
Text("공유")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color(hex: "ffffff").opacity(0.1))
|
||||
.cornerRadius(26.7)
|
||||
.onTapGesture { onClickShare() }
|
||||
|
||||
if audioContent.isCommentAvailable {
|
||||
HStack(spacing: 4) {
|
||||
Image("ic_donation_white")
|
||||
.resizable()
|
||||
.frame(width: 13.3, height: 13.3)
|
||||
|
||||
Text("후원")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color(hex: "ffffff").opacity(0.1))
|
||||
.cornerRadius(26.7)
|
||||
.onTapGesture { onClickDonation() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
|
||||
if let totalContentCount = audioContent.totalContentCount, let remainingContentCount = audioContent.remainingContentCount {
|
||||
ContentDetailInfoLimitedEditionView(
|
||||
|
@ -155,13 +157,13 @@ struct ContentDetailInfoView: View {
|
|||
if audioContent.tag.count > 0 {
|
||||
Text(audioContent.tag)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
Text(audioContent.detail)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.lineLimit(isExpandDescription ? nil : 3)
|
||||
.lineSpacing(5)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -173,7 +175,7 @@ struct ContentDetailInfoView: View {
|
|||
HStack(spacing: 0) {
|
||||
Text("미리듣기 중입니다.\n콘텐츠 구매 후 전체를 감상해 보세요.")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.foregroundColor(Color.graybb)
|
||||
.lineSpacing(5)
|
||||
|
||||
Spacer()
|
||||
|
@ -187,7 +189,7 @@ struct ContentDetailInfoView: View {
|
|||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5.3)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,18 @@ struct ContentDetailPurchaseButton: View {
|
|||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
if UserDefaults.int(forKey: .userId) != 17958 {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
}
|
||||
|
||||
Text("\(price)")
|
||||
Text(UserDefaults.int(forKey: .userId) == 17958 ? "\(price * 110)" : "\(price)")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 5.3)
|
||||
|
||||
Text("캔으로")
|
||||
Text(UserDefaults.int(forKey: .userId) == 17958 ? "원으로": "캔으로")
|
||||
.font(.custom(Font.light.rawValue, size: 12))
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ struct ContentDetailView: View {
|
|||
|
||||
Text("콘텐츠 상세")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
@ -99,7 +99,7 @@ struct ContentDetailView: View {
|
|||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 48.7)
|
||||
.background(Color(hex: "525252"))
|
||||
.background(Color.gray52)
|
||||
.cornerRadius(5.3)
|
||||
.padding(.top, 18.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
@ -113,7 +113,7 @@ struct ContentDetailView: View {
|
|||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 48.7)
|
||||
.background(Color(hex: "525252"))
|
||||
.background(Color.gray52)
|
||||
.cornerRadius(5.3)
|
||||
.padding(.top, 18.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
@ -132,7 +132,7 @@ struct ContentDetailView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if audioContent.isCommentAvailable {
|
||||
if audioContent.isCommentAvailable && !audioContent.contentUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && audioContent.releaseDate == nil {
|
||||
ContentDetailCommentView(
|
||||
commentCount: audioContent.commentCount,
|
||||
commentList: audioContent.commentList,
|
||||
|
@ -162,7 +162,7 @@ struct ContentDetailView: View {
|
|||
.padding(.horizontal, 13.3)
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "232323"))
|
||||
.foregroundColor(Color.gray23)
|
||||
.frame(height: 6.7)
|
||||
.padding(.top, 24)
|
||||
|
||||
|
@ -207,7 +207,7 @@ struct ContentDetailView: View {
|
|||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +226,19 @@ struct ContentDetailView: View {
|
|||
orderType: orderType,
|
||||
isOnlyRental: audioContent.isOnlyRental,
|
||||
onClickConfirm: {
|
||||
viewModel.order(orderType: orderType)
|
||||
if UserDefaults.int(forKey: .userId) == 17958 {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .tempCanPayment(
|
||||
orderType: orderType,
|
||||
contentId: audioContent.contentId,
|
||||
title: audioContent.title,
|
||||
can: orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price
|
||||
)
|
||||
)
|
||||
} else {
|
||||
viewModel.order(orderType: orderType)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -272,7 +284,7 @@ struct ContentDetailView: View {
|
|||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,11 @@ final class ContentDetailViewModel: ObservableObject {
|
|||
let decoded = try jsonDecoder.decode(ApiResponse<GetAudioContentDetailResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.audioContent = data
|
||||
if AppState.shared.purchasedContentId > 0 && AppState.shared.purchasedContentId == data.contentId {
|
||||
self.order(orderType: AppState.shared.purchasedContentOrderType)
|
||||
} else {
|
||||
self.audioContent = data
|
||||
}
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
|
@ -298,6 +302,9 @@ final class ContentDetailViewModel: ObservableObject {
|
|||
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
|
||||
|
||||
if decoded.success {
|
||||
AppState.shared.purchasedContentId = 0
|
||||
AppState.shared.purchasedContentOrderType = .KEEP
|
||||
|
||||
self.orderType = nil
|
||||
self.errorMessage = orderType == .RENTAL ? "대여가 완료되었습니다." : "구매가 완료되었습니다."
|
||||
self.isShowPopup = true
|
||||
|
|
|
@ -27,7 +27,7 @@ struct ContentOrderConfirmDialogView: View {
|
|||
VStack(spacing: 0) {
|
||||
Text("구매확인")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 11) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
@ -48,7 +48,7 @@ struct ContentOrderConfirmDialogView: View {
|
|||
|
||||
Text(audioContent.title)
|
||||
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
.foregroundColor(Color.grayd2)
|
||||
.padding(.top, 2)
|
||||
|
||||
HStack(spacing: 4.3) {
|
||||
|
@ -60,13 +60,13 @@ struct ContentOrderConfirmDialogView: View {
|
|||
|
||||
Text(audioContent.creator.nickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 10))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
.padding(.top, 6.7)
|
||||
|
||||
Text(audioContent.duration)
|
||||
.font(.custom(Font.medium.rawValue, size: 11))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.top, 6.7)
|
||||
}
|
||||
|
||||
|
@ -77,52 +77,74 @@ struct ContentOrderConfirmDialogView: View {
|
|||
.cornerRadius(5.3)
|
||||
.padding(.top, 21.3)
|
||||
|
||||
Text("콘텐츠를 \(orderType == .RENTAL ? "대여" : "소장")하시겠습니까?\n아래 캔이 차감됩니다.")
|
||||
Text("콘텐츠를 \(orderType == .RENTAL ? "대여" : "소장")하시겠습니까?")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
if UserDefaults.int(forKey: .userId) != 17958 {
|
||||
Text("아래 캔이 차감됩니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
|
||||
HStack(spacing: 2.7) {
|
||||
Spacer()
|
||||
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
if UserDefaults.int(forKey: .userId) != 17958 {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
|
||||
if orderType == .RENTAL {
|
||||
Text("\(isOnlyRental ? audioContent.price : Int(ceil(Double(audioContent.price) * 0.6)))")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
if orderType == .RENTAL {
|
||||
Text("\(isOnlyRental ? audioContent.price : Int(ceil(Double(audioContent.price) * 0.6)))")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
} else {
|
||||
Text("\(audioContent.price)")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
} else {
|
||||
Text("\(audioContent.price)")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
if orderType == .RENTAL {
|
||||
Text("\(isOnlyRental ? audioContent.price * 110 : Int(ceil(Double(audioContent.price) * 0.6)) * 110)원")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
} else {
|
||||
Text("\(audioContent.price * 110)원")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color(hex: "333333"))
|
||||
.background(Color.gray33)
|
||||
.cornerRadius(6.7)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: CGFloat(6.7))
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "979797"))
|
||||
.foregroundColor(Color.gray97)
|
||||
)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Text("취소")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.vertical, 15.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: CGFloat(10))
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
)
|
||||
.onTapGesture { isShowing = false }
|
||||
|
||||
|
@ -131,7 +153,8 @@ struct ContentOrderConfirmDialogView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 15.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.contentShape(Rectangle())
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
onClickConfirm()
|
||||
|
@ -143,7 +166,7 @@ struct ContentOrderConfirmDialogView: View {
|
|||
.padding(.horizontal, 13.3)
|
||||
.padding(.top, 26.7)
|
||||
.padding(.bottom, 16.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(10)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
|
|
@ -40,17 +40,25 @@ struct ContentOrderDialogView: View {
|
|||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
if UserDefaults.int(forKey: .userId) != 17958 {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
}
|
||||
|
||||
Text(isOnlyRental ? "\(price)" : "\(Int(ceil(Double(price) * 0.6)))")
|
||||
Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
if UserDefaults.int(forKey: .userId) == 17958 {
|
||||
Text("원")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(5.3)
|
||||
.onTapGesture {
|
||||
onTapPurchase(.RENTAL)
|
||||
|
@ -73,17 +81,25 @@ struct ContentOrderDialogView: View {
|
|||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
if UserDefaults.int(forKey: .userId) != 17958 {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
}
|
||||
|
||||
Text("\(price)")
|
||||
Text("\(price * 110)")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
if UserDefaults.int(forKey: .userId) == 17958 {
|
||||
Text("원")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(5.3)
|
||||
.onTapGesture {
|
||||
onTapPurchase(.KEEP)
|
||||
|
@ -93,7 +109,7 @@ struct ContentOrderDialogView: View {
|
|||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ struct LiveRoomDonationDialogView: View {
|
|||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 5.3) {
|
||||
HStack(alignment: .center, spacing: 5.3) {
|
||||
Image("ic_donation_white")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
|
@ -48,7 +48,7 @@ struct LiveRoomDonationDialogView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
HStack(alignment: .center, spacing: 5.3) {
|
||||
Image("ic_can")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
|
@ -57,7 +57,17 @@ struct LiveRoomDonationDialogView: View {
|
|||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Image("ic_forward")
|
||||
Text("충전")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.main)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(6.7)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6.7)
|
||||
.stroke(Color.button, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .canCharge(refresh: {}, afterCompletionToGoBack: true))
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import RefreshableScrollView
|
||||
|
||||
struct ContentMainView: View {
|
||||
|
||||
|
@ -16,12 +15,7 @@ struct ContentMainView: View {
|
|||
ZStack(alignment: .bottomTrailing) {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
RefreshableScrollView(
|
||||
refreshing: $viewModel.isLoading,
|
||||
action: {
|
||||
viewModel.refresh()
|
||||
}
|
||||
) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("콘텐츠 마켓")
|
||||
.font(.custom(Font.bold.rawValue, size: 21.3))
|
||||
|
@ -30,9 +24,7 @@ struct ContentMainView: View {
|
|||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !viewModel.isLoading {
|
||||
ContentMainNewContentCreatorView()
|
||||
.padding(.bottom, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
ContentMainRecommendSeriesView()
|
||||
|
||||
ContentMainBannerView()
|
||||
.padding(.bottom, 26.7)
|
||||
|
@ -124,10 +116,10 @@ struct ContentMainView: View {
|
|||
AppState.shared.setAppStep(step: .createContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// ContentMainNewContentCreatorItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainNewContentCreatorItemView: View {
|
||||
|
||||
let item: GetNewContentUploadCreator
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .top)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.frame(width: screenSize().width * 0.18)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewContentCreatorItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainNewContentCreatorItemView(
|
||||
item: GetNewContentUploadCreator(
|
||||
creatorId: 2,
|
||||
creatorNickname: "수다친구112",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
//
|
||||
// ContentMainNewContentCreatorView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNewContentCreatorView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainNewContentCreatorViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !viewModel.newContentUploadCreatorList.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 21.3) {
|
||||
ForEach(0..<viewModel.newContentUploadCreatorList.count, id: \.self) { index in
|
||||
let item = viewModel.newContentUploadCreatorList[index]
|
||||
ContentMainNewContentCreatorItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicatorView()
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.onAppear {
|
||||
viewModel.getNewContentUploadCreatorList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewContentCreatorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainNewContentCreatorView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// ContentMainRecommendSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/7/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainRecommendSeriesView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainRecommendSeriesViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !viewModel.seriesList.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("추천 시리즈")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.seriesList.count, id: \.self) {
|
||||
let item = viewModel.seriesList[$0]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image("ic_refresh")
|
||||
|
||||
Text("새로고침")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.vertical, 11)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 26.7)
|
||||
.stroke(Color.gray90, lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.getRecommendSeriesList()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getRecommendSeriesList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainRecommendSeriesView()
|
||||
}
|
|
@ -1,28 +1,26 @@
|
|||
//
|
||||
// ContentMainNewContentCreatorViewModel.swift
|
||||
// ContentMainRecommendSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/12/11.
|
||||
// Created by klaus on 5/7/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainNewContentCreatorViewModel: ObservableObject {
|
||||
final class ContentMainRecommendSeriesViewModel: ObservableObject {
|
||||
|
||||
private let repository = ContentRepository()
|
||||
private let repository = SeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var newContentUploadCreatorList = [GetNewContentUploadCreator]()
|
||||
@Published var seriesList = [SeriesListItem]()
|
||||
|
||||
func getNewContentUploadCreatorList() {
|
||||
isLoading = true
|
||||
|
||||
repository.getNewContentUploadCreatorList()
|
||||
func getRecommendSeriesList() {
|
||||
repository.getRecommendSeriesList()
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
|
@ -36,23 +34,23 @@ final class ContentMainNewContentCreatorViewModel: ObservableObject {
|
|||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetNewContentUploadCreator]>.self, from: responseData)
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[SeriesListItem]>.self, from: responseData)
|
||||
self.isLoading = false
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newContentUploadCreatorList.removeAll()
|
||||
self.newContentUploadCreatorList.append(contentsOf: data)
|
||||
self.seriesList.removeAll()
|
||||
self.seriesList.append(contentsOf: data)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.errorMessage = "추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.errorMessage = "추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
self.isLoading = false
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// SeriesContentAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/30/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesContentAllView: View {
|
||||
|
||||
@ObservedObject var viewModel = SeriesContentAllViewModel()
|
||||
|
||||
let seriesId: Int
|
||||
let seriesTitle: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "\(seriesTitle) - 전체회차 듣기")
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(0..<viewModel.seriesContentList.count, id: \.self) { index in
|
||||
let item = viewModel.seriesContentList[index]
|
||||
|
||||
SeriesContentListItemView(item: item)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: item.contentId))
|
||||
}
|
||||
.onAppear {
|
||||
if index == viewModel.seriesContentList.count - 1 {
|
||||
viewModel.getSeriesContentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(13.3)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.seriesId = seriesId
|
||||
viewModel.getSeriesContentList()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// SeriesContentAllViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class SeriesContentAllViewModel: ObservableObject {
|
||||
private let repository = SeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
var seriesId = 0
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var seriesContentList = [GetSeriesContentListItem]()
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private let pageSize = 10
|
||||
|
||||
func getSeriesContentList() {
|
||||
if !isLoading && !isLast {
|
||||
repository
|
||||
.getSeriesContentList(seriesId: seriesId, 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
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetSeriesContentListResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
if page == 1 {
|
||||
self.seriesContentList.removeAll()
|
||||
}
|
||||
|
||||
if !data.items.isEmpty {
|
||||
page += 1
|
||||
self.seriesContentList.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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// SeriesContentListItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/30/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct SeriesContentListItemView: View {
|
||||
|
||||
let item: GetSeriesContentListItem
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack(spacing: 11) {
|
||||
KFImage(URL(string: item.coverImage))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 66.7, height: 66.7)
|
||||
.cornerRadius(5.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.7) {
|
||||
Text(item.duration)
|
||||
.font(.custom(Font.medium.rawValue, size: 10))
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(2.7)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(2.6)
|
||||
|
||||
Text(item.title)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if item.isOwned {
|
||||
Text("소장중")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.gray11)
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 2.7)
|
||||
.background(Color(hex: "b1ef2c"))
|
||||
.cornerRadius(2.6)
|
||||
} else if item.isRented {
|
||||
Text("대여중")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 2.7)
|
||||
.background(Color(hex: "660fd4"))
|
||||
.cornerRadius(2.6)
|
||||
} else if item.price > 0 {
|
||||
HStack(spacing: 5.3) {
|
||||
Image("ic_can")
|
||||
|
||||
Text("\(item.price)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
}
|
||||
} else {
|
||||
Text("무료")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 2.7)
|
||||
.background(Color(hex: "cf5c37"))
|
||||
.cornerRadius(2.6)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(Color.gray59)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("무료") {
|
||||
SeriesContentListItemView(
|
||||
item: GetSeriesContentListItem(
|
||||
contentId: 1,
|
||||
title: "[무료] 두근두근 연애 연구부 EP1",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 0,
|
||||
isRented: false,
|
||||
isOwned: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("유료") {
|
||||
SeriesContentListItemView(
|
||||
item: GetSeriesContentListItem(
|
||||
contentId: 1,
|
||||
title: "두근두근 연애 연구부 EP1",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 100,
|
||||
isRented: false,
|
||||
isOwned: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("대여") {
|
||||
SeriesContentListItemView(
|
||||
item: GetSeriesContentListItem(
|
||||
contentId: 1,
|
||||
title: "두근두근 연애 연구부 EP1",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 200,
|
||||
isRented: true,
|
||||
isOwned: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("소장") {
|
||||
SeriesContentListItemView(
|
||||
item: GetSeriesContentListItem(
|
||||
contentId: 1,
|
||||
title: "두근두근 연애 연구부 EP1",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 300,
|
||||
isRented: false,
|
||||
isOwned: true
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// GetSeriesContentListResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetSeriesContentListResponse: Decodable {
|
||||
let totalCount: Int
|
||||
let items: [GetSeriesContentListItem]
|
||||
}
|
||||
|
||||
struct GetSeriesContentListItem: Decodable {
|
||||
let contentId: Int
|
||||
let title: String
|
||||
let coverImage: String
|
||||
let releaseDate: String
|
||||
let duration: String
|
||||
let price: Int
|
||||
let isRented: Bool
|
||||
let isOwned: Bool
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// GetSeriesDetailResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetSeriesDetailResponse: Decodable {
|
||||
let seriesId: Int
|
||||
let title: String
|
||||
let coverImage: String
|
||||
let introduction: String
|
||||
let genre: String
|
||||
let isAdult: Bool
|
||||
let writer: String?
|
||||
let studio: String?
|
||||
let publishedDate: String
|
||||
let creator: GetSeriesDetailCreator
|
||||
let rentalMinPrice: Int
|
||||
let rentalMaxPrice: Int
|
||||
let rentalPeriod: Int
|
||||
let minPrice: Int
|
||||
let maxPrice: Int
|
||||
let keywordList: [String]
|
||||
let publishedDaysOfWeek: String
|
||||
let contentList: [GetSeriesContentListItem]
|
||||
let contentCount: Int
|
||||
}
|
||||
|
||||
struct GetSeriesDetailCreator: Decodable {
|
||||
let creatorId: Int
|
||||
let nickname: String
|
||||
let profileImage: String
|
||||
let isFollow: Bool
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// SeriesDetailHomeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesDetailHomeView: View {
|
||||
|
||||
let title: String
|
||||
let seriesId: Int
|
||||
let contentCount: Int
|
||||
let contentList: [GetSeriesContentListItem]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("전체회차 듣기")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Text(" (\(contentCount))")
|
||||
.font(.custom(Font.light.rawValue, size: 16))
|
||||
.foregroundColor(Color.button)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(5.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5.3)
|
||||
.stroke()
|
||||
.foregroundColor(Color.button)
|
||||
)
|
||||
.padding(.top, 16)
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesContentAll(seriesId: seriesId, seriesTitle: title))
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
ForEach(0..<contentList.count, id: \.self) {
|
||||
let item = contentList[$0]
|
||||
|
||||
SeriesContentListItemView(item: item)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: item.contentId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesDetailHomeView(
|
||||
title: "변호사 우영우",
|
||||
seriesId: 0,
|
||||
contentCount: 10,
|
||||
contentList: [
|
||||
GetSeriesContentListItem(
|
||||
contentId: 1,
|
||||
title: "[무료] 두근두근 연애 연구부 EP1",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 0,
|
||||
isRented: false,
|
||||
isOwned: false
|
||||
),
|
||||
GetSeriesContentListItem(
|
||||
contentId: 2,
|
||||
title: "두근두근 연애 연구부 EP2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 100,
|
||||
isRented: false,
|
||||
isOwned: false
|
||||
),
|
||||
GetSeriesContentListItem(
|
||||
contentId: 3,
|
||||
title: "두근두근 연애 연구부 EP3",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 100,
|
||||
isRented: true,
|
||||
isOwned: false
|
||||
),
|
||||
GetSeriesContentListItem(
|
||||
contentId: 4,
|
||||
title: "두근두근 연애 연구부 EP4",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
releaseDate: "",
|
||||
duration: "00:14:59",
|
||||
price: 100,
|
||||
isRented: false,
|
||||
isOwned: true
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// SeriesDetailIntroductionView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIFlowLayout
|
||||
|
||||
struct SeriesDetailIntroductionView: View {
|
||||
|
||||
let width: CGFloat
|
||||
let seriesDetail: GetSeriesDetailResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("키워드")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
FlowLayout(mode: .scrollable, items: seriesDetail.keywordList, itemSpacing: 5.3) {
|
||||
SeriesKeywordChipView(keyword: $0)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 6.7)
|
||||
.foregroundColor(Color.gray22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("작품소개")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Text(seriesDetail.introduction)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
.lineSpacing(4)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 6.7)
|
||||
.foregroundColor(Color.gray22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("상세정보")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("장르")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("연령제한")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
if let _ = seriesDetail.writer {
|
||||
Text("작가")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
|
||||
if let _ = seriesDetail.studio {
|
||||
Text("제작사")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
|
||||
Text("연재")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("출시일")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(seriesDetail.genre)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
|
||||
Text(seriesDetail.isAdult ? "19세 이상" : "전체연령가")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
|
||||
if let writer = seriesDetail.writer {
|
||||
Text(writer)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
|
||||
if let studio = seriesDetail.studio {
|
||||
Text(studio)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
|
||||
Text(seriesDetail.publishedDaysOfWeek == "랜덤" ? seriesDetail.publishedDaysOfWeek : "\(seriesDetail.publishedDaysOfWeek)요일")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
|
||||
Text(seriesDetail.publishedDate)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("가격")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("대여")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("소장")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("\(calculatePriceInfo(seriesDetail.rentalMinPrice, seriesDetail.rentalMaxPrice)) (15일)")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Text("\(calculatePriceInfo(seriesDetail.minPrice, seriesDetail.maxPrice))")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.button)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
|
||||
func calculatePriceInfo(_ minPrice: Int, _ maxPrice: Int) -> String {
|
||||
if minPrice == maxPrice {
|
||||
if maxPrice == 0 {
|
||||
return "무료"
|
||||
} else {
|
||||
return "\(maxPrice)"
|
||||
}
|
||||
} else {
|
||||
return "\(minPrice == 0 ? "무료" : "\(minPrice)") ~ \(maxPrice)캔"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
//
|
||||
// SeriesDetailView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct SeriesDetailView: View {
|
||||
|
||||
@ObservedObject var viewModel = SeriesDetailViewModel()
|
||||
|
||||
let seriesId: Int
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ZStack(alignment: .top) {
|
||||
Color.gray11.ignoresSafeArea()
|
||||
|
||||
if let seriesDetail = viewModel.seriesDetail {
|
||||
KFImage(URL(string: seriesDetail.coverImage))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity)
|
||||
.blur(radius: 25)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Image("ic_back")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture { AppState.shared.back() }
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(height: 50)
|
||||
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(Color.gray11)
|
||||
.cornerRadius(21.3, corners: [.topLeft, .topRight])
|
||||
.padding(.top, 94)
|
||||
|
||||
KFImage(URL(string: seriesDetail.coverImage))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(
|
||||
width: 400 * screenSize().width / 1080,
|
||||
height: 564 * screenSize().width / 1080
|
||||
)
|
||||
.cornerRadius(5)
|
||||
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(seriesDetail.title)
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.top, 24)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
Text(seriesDetail.genre)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "3bac6a"))
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 3.3)
|
||||
.background(Color(hex: "28312b"))
|
||||
.cornerRadius(2.6)
|
||||
|
||||
if seriesDetail.isAdult {
|
||||
Text("19세")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "f1291c"))
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 3.3)
|
||||
.background(Color(hex: "312827"))
|
||||
.cornerRadius(2.6)
|
||||
} else {
|
||||
Text("전체연령가")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
.padding(.horizontal, 5.3)
|
||||
.padding(.vertical, 3.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(2.6)
|
||||
}
|
||||
|
||||
Text("\(seriesDetail.publishedDaysOfWeek) 연재")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 5.3) {
|
||||
ForEach(0..<seriesDetail.keywordList.count, id: \.self) {
|
||||
SeriesKeywordChipView(keyword: seriesDetail.keywordList[$0])
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: seriesDetail.creator.profileImage))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.clipShape(Circle())
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
|
||||
Text(seriesDetail.creator.nickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
|
||||
Spacer()
|
||||
|
||||
if seriesDetail.creator.creatorId != UserDefaults.int(forKey: .userId) {
|
||||
Image(viewModel.isFollow ? "btn_following_big" : "btn_follow_big")
|
||||
.onTapGesture {
|
||||
if viewModel.isFollow {
|
||||
viewModel.unFollow(seriesDetail.creator.creatorId)
|
||||
} else {
|
||||
viewModel.follow(seriesDetail.creator.creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
SeriesDetailTabView(
|
||||
title: "홈",
|
||||
width: screenSize().width / 2,
|
||||
isSelected: viewModel.currentTab == .home
|
||||
) {
|
||||
if viewModel.currentTab != .home {
|
||||
viewModel.currentTab = .home
|
||||
}
|
||||
}
|
||||
|
||||
SeriesDetailTabView(
|
||||
title: "작품소개",
|
||||
width: screenSize().width / 2,
|
||||
isSelected: viewModel.currentTab == .introduction
|
||||
) {
|
||||
if viewModel.currentTab != .introduction {
|
||||
viewModel.currentTab = .introduction
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(Color.gray90.opacity(0.5))
|
||||
.frame(height: 1)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
switch(viewModel.currentTab) {
|
||||
case .introduction:
|
||||
SeriesDetailIntroductionView(
|
||||
width: screenSize().width - 26.7,
|
||||
seriesDetail: seriesDetail
|
||||
)
|
||||
|
||||
default:
|
||||
SeriesDetailHomeView(
|
||||
title: seriesDetail.title,
|
||||
seriesId: seriesDetail.seriesId,
|
||||
contentCount: seriesDetail.contentCount,
|
||||
contentList: seriesDetail.contentList
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
.background(Color.gray11)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.seriesId = seriesId
|
||||
viewModel.getSeriesDetail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesDetailView(seriesId: 0)
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// SeriesDetailViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class SeriesDetailViewModel: ObservableObject {
|
||||
private let repository = SeriesRepository()
|
||||
private let userRepository = UserRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
|
||||
@Published var isFollow: Bool = false
|
||||
@Published var seriesDetail: GetSeriesDetailResponse? = nil
|
||||
|
||||
var seriesId: Int = 0
|
||||
|
||||
func getSeriesDetail() {
|
||||
isLoading = true
|
||||
|
||||
repository
|
||||
.getSeriesDetail(seriesId: seriesId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetSeriesDetailResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.seriesDetail = data
|
||||
self.isFollow = data.creator.isFollow
|
||||
} 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)
|
||||
}
|
||||
|
||||
func follow(_ creatorId: Int) {
|
||||
isLoading = true
|
||||
|
||||
userRepository.creatorFollow(creatorId: creatorId)
|
||||
.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(ApiResponseWithoutData.self, from: responseData)
|
||||
|
||||
if decoded.success {
|
||||
self.isFollow = !self.isFollow
|
||||
} 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)
|
||||
}
|
||||
|
||||
func unFollow(_ creatorId: Int) {
|
||||
isLoading = true
|
||||
|
||||
userRepository.creatorUnFollow(creatorId: creatorId)
|
||||
.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(ApiResponseWithoutData.self, from: responseData)
|
||||
|
||||
if decoded.success {
|
||||
self.isFollow = !self.isFollow
|
||||
} 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)
|
||||
}
|
||||
|
||||
enum CurrentTab: String {
|
||||
case home, introduction
|
||||
}
|
||||
|
||||
@Published var currentTab: CurrentTab = .home
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// GetSeriesListResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetSeriesListResponse: Decodable {
|
||||
let totalCount: Int
|
||||
let items: [SeriesListItem]
|
||||
}
|
||||
|
||||
struct SeriesListItem: Decodable {
|
||||
let seriesId: Int
|
||||
let title: String
|
||||
let coverImage: String
|
||||
let publishedDaysOfWeek: String
|
||||
let isComplete: Bool
|
||||
let creator: SeriesListItemCreator
|
||||
let numberOfContent: Int
|
||||
let isNew: Bool
|
||||
let isPopular: Bool
|
||||
}
|
||||
|
||||
struct SeriesListItemCreator: Decodable {
|
||||
let creatorId: Int
|
||||
let nickname: String
|
||||
let profileImage: String
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// SeriesApi.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum SeriesApi {
|
||||
case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int)
|
||||
case getSeriesDetail(seriesId: Int)
|
||||
case getSeriesContentList(seriesId: Int, page: Int, size: Int)
|
||||
case getRecommendSeriesList
|
||||
}
|
||||
|
||||
extension SeriesApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .getSeriesList:
|
||||
return "/audio-content/series"
|
||||
|
||||
case .getSeriesDetail(let seriesId):
|
||||
return "/audio-content/series/\(seriesId)"
|
||||
|
||||
case .getSeriesContentList(let seriesId, _, _):
|
||||
return "/audio-content/series/\(seriesId)/content"
|
||||
|
||||
case .getRecommendSeriesList:
|
||||
return "/audio-content/series/recommend"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getSeriesList, .getSeriesDetail, .getSeriesContentList, .getRecommendSeriesList:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getSeriesList(let creatorId, let sortType, let page, let size):
|
||||
let parameters = [
|
||||
"creatorId": creatorId,
|
||||
"sortType": sortType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getSeriesDetail, .getRecommendSeriesList:
|
||||
return .requestPlain
|
||||
|
||||
case .getSeriesContentList(_, let page, let size):
|
||||
let parameters = [
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String : String]? {
|
||||
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// SeriesListAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesListAllView: View {
|
||||
|
||||
@ObservedObject var viewModel = SeriesListAllViewModel()
|
||||
@State var columns: [GridItem] = []
|
||||
|
||||
let creatorId: Int
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "시리즈 전체보기")
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 33.3) {
|
||||
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
||||
let item = viewModel.seriesList[index]
|
||||
SeriesListItemView(itemWidth: (screenSize().width - 40) / 3, item: item)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
columns = [
|
||||
GridItem(.fixed((screenSize().width - 40) / 3), alignment: .top),
|
||||
GridItem(.fixed((screenSize().width - 40) / 3), alignment: .top),
|
||||
GridItem(.fixed((screenSize().width - 40) / 3), alignment: .top)
|
||||
]
|
||||
viewModel.creatorId = creatorId
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesListAllView(creatorId: 0)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// SeriesListAllViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class SeriesListAllViewModel: ObservableObject {
|
||||
private let repository = SeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
enum SeriesSortType: String {
|
||||
case NEWEST, POPULAR
|
||||
}
|
||||
|
||||
var creatorId: Int = 0
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var seriesList = [SeriesListItem]()
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private let pageSize = 10
|
||||
|
||||
func getSeriesList() {
|
||||
if !isLoading && !isLast {
|
||||
isLoading = true
|
||||
repository
|
||||
.getSeriesList(creatorId: creatorId, sortType: .NEWEST, 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
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetSeriesListResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
if page == 1 {
|
||||
self.seriesList.removeAll()
|
||||
}
|
||||
|
||||
if !data.items.isEmpty {
|
||||
page += 1
|
||||
self.seriesList.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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// SeriesRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
class SeriesRepository {
|
||||
private let api = MoyaProvider<SeriesApi>()
|
||||
|
||||
func getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getSeriesList(creatorId: creatorId, sortType: sortType, page: page, size: size))
|
||||
}
|
||||
|
||||
func getSeriesDetail(seriesId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getSeriesDetail(seriesId: seriesId))
|
||||
}
|
||||
|
||||
func getSeriesContentList(seriesId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getSeriesContentList(seriesId: seriesId, page: page, size: size))
|
||||
}
|
||||
|
||||
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getRecommendSeriesList)
|
||||
}
|
||||
}
|
|
@ -178,6 +178,19 @@ struct ContentView: View {
|
|||
case .contentAllByTheme(let themeId):
|
||||
ContentAllByThemeView(themeId: themeId)
|
||||
|
||||
case .seriesAll(let creatorId):
|
||||
SeriesListAllView(creatorId: creatorId)
|
||||
|
||||
case .seriesDetail(let seriesId):
|
||||
SeriesDetailView(seriesId: seriesId)
|
||||
|
||||
case .seriesContentAll(let seriesId, let seriesTitle):
|
||||
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
|
||||
|
||||
case .tempCanPayment(let orderType, let contentId, let title, let can):
|
||||
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
|
||||
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||
|
|
|
@ -84,7 +84,10 @@ struct ExplorerView: View {
|
|||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 26.7) {
|
||||
ForEach(0..<viewModel.explorerSections.count, id: \.self) { index in
|
||||
ExplorerSectionView(section: viewModel.explorerSections[index])
|
||||
let section = viewModel.explorerSections[index]
|
||||
if !section.creators.isEmpty {
|
||||
ExplorerSectionView(section: section)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 40)
|
||||
|
|
|
@ -123,6 +123,7 @@ struct CreatorCommunityCommentReplyView: View {
|
|||
}
|
||||
)
|
||||
.padding(.horizontal, 26.7)
|
||||
.padding(.leading, 13.3)
|
||||
.onAppear {
|
||||
if index == viewModel.commentList.count - 1 {
|
||||
viewModel.getCommentList()
|
||||
|
|
|
@ -17,6 +17,7 @@ struct GetCreatorProfileResponse: Decodable {
|
|||
let communityPostList: [GetCommunityPostListResponse]
|
||||
let cheers: GetCheersResponse
|
||||
let activitySummary: GetCreatorActivitySummary
|
||||
let seriesList: [SeriesListItem]
|
||||
let isBlock: Bool
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// UserProfileSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserProfileSeriesView: View {
|
||||
let creatorId: Int
|
||||
let items: [SeriesListItem]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("시리즈")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("전체보기")
|
||||
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesAll(creatorId: creatorId))
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<items.count, id: \.self) {
|
||||
let item = items[$0]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
UserProfileSeriesView(
|
||||
creatorId: 1,
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
])
|
||||
}
|
|
@ -40,11 +40,11 @@ struct UserProfileActivitySummaryView: View {
|
|||
)
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color(hex: "9970ff"), lineWidth: 1)
|
||||
.stroke(Color.button, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -55,12 +55,12 @@ struct UserProfileActivitySummaryView: View {
|
|||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
.foregroundColor(Color.gray90)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(count)
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ struct UserProfileActivitySummaryView: View {
|
|||
func ActivitySummaryDividerView() -> some View {
|
||||
Rectangle()
|
||||
.frame(width: 1, height: 33.3)
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ struct UserProfileCreatorView: View {
|
|||
.padding(.horizontal, 16)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16.7)
|
||||
.stroke(Color(hex: "9970ff"), lineWidth: 1)
|
||||
.stroke(Color.button, lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 13.3)
|
||||
.onTapGesture {
|
||||
|
@ -79,7 +79,7 @@ struct UserProfileCreatorView: View {
|
|||
|
||||
Text(creator.tags.map { "#\($0)" }.joined(separator: " "))
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.top, creator.tags.count > 0 ? 13.3 : 0)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
|
|
|
@ -107,6 +107,15 @@ struct UserProfileView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if !creatorProfile.seriesList.isEmpty {
|
||||
UserProfileSeriesView(
|
||||
creatorId: creatorProfile.creator.creatorId,
|
||||
items: creatorProfile.seriesList
|
||||
)
|
||||
.padding(.top, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if creatorProfile.contentList.count > 0 ||
|
||||
userId == UserDefaults.int(forKey: .userId)
|
||||
{
|
||||
|
|
|
@ -95,7 +95,7 @@ extension LiveApi: TargetType {
|
|||
return "/live/room/info/\(roomId)"
|
||||
|
||||
case .donation:
|
||||
return "/live/room/donation"
|
||||
return "/live/room/donation/v2"
|
||||
|
||||
case .refundDonation(let roomId):
|
||||
return "/live/room/donation/refund/\(roomId)"
|
||||
|
|
|
@ -26,12 +26,6 @@ struct SectionLiveNowView: View {
|
|||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
|
||||
Image("ic_refresh")
|
||||
.padding(.leading, 10)
|
||||
.onTapGesture {
|
||||
onClickRefresh()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if items.count > 0 {
|
||||
|
@ -76,11 +70,12 @@ struct SectionLiveNowView: View {
|
|||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Text("🙀현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.")
|
||||
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||
Text("마이페이지에서 본인인증을 하거나\n라이브를 예약하고 참여해보세요.")
|
||||
.font(.custom(Font.medium.rawValue, size: 13))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
|
@ -88,6 +83,25 @@ struct SectionLiveNowView: View {
|
|||
.background(Color(hex: "13181b"))
|
||||
.cornerRadius(4.7)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image("ic_refresh")
|
||||
|
||||
Text("새로고침")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.vertical, 11)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 26.7)
|
||||
.stroke(Color.gray90, lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal, 13.3)
|
||||
.onTapGesture {
|
||||
onClickRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,10 +57,11 @@ struct LiveReservationAllView: View {
|
|||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Text("지금 예약중인 라이브가 없습니다.\n직접 라이브를 만들어 보세요!")
|
||||
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||
Text("마이페이지에서 본인인증을 하거나\n다른 날짜의 라이브를 예약하고 참여해 보세요")
|
||||
.font(.custom(Font.medium.rawValue, size: 13))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(8)
|
||||
.padding(.top, 8)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
|
|
|
@ -93,10 +93,11 @@ struct SectionLiveReservationView: View {
|
|||
.frame(width: 60, height: 60)
|
||||
|
||||
Text("지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.")
|
||||
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||
.font(.custom(Font.medium.rawValue, size: 13))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
|
|
|
@ -15,6 +15,7 @@ struct LiveRoomChatRawMessage: Codable {
|
|||
let type: LiveRoomChatRawMessageType
|
||||
let message: String
|
||||
let can: Int
|
||||
var signature: LiveRoomDonationResponse? = nil
|
||||
var signatureImageUrl: String? = nil
|
||||
let donationMessage: String?
|
||||
var isActiveRoulette: Bool? = nil
|
||||
|
|
|
@ -22,4 +22,5 @@ struct CreateLiveRoomRequest: Encodable {
|
|||
var menuPanId: Int = 0
|
||||
var menuPan: String = ""
|
||||
var isActiveMenuPan: Bool = false
|
||||
var isAvailableJoinCreator: Bool = true
|
||||
}
|
||||
|
|
|
@ -147,6 +147,23 @@ struct LiveRoomCreateView: View {
|
|||
.frame(width: screenSize().width - 26.7)
|
||||
.padding(.top, 33.3)
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
Text("크리에이터 입장 설정")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(width: screenSize().width - 26.7, alignment: .leading)
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
SelectedButtonView(title: "가능", isActive: true, isSelected: viewModel.isAvailableJoinCreator)
|
||||
.onTapGesture { viewModel.isAvailableJoinCreator = true }
|
||||
|
||||
SelectedButtonView(title: "불가능", isActive: true, isSelected: !viewModel.isAvailableJoinCreator)
|
||||
.onTapGesture { viewModel.isAvailableJoinCreator = false }
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.padding(.top, 33.3)
|
||||
|
||||
if UserDefaults.bool(forKey: .auth) {
|
||||
AdultSettingView()
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
|
|
|
@ -88,6 +88,8 @@ final class LiveRoomCreateViewModel: ObservableObject {
|
|||
@Published var isActivateMenu = false
|
||||
@Published var selectedMenu: SelectedMenu? = nil
|
||||
|
||||
@Published var isAvailableJoinCreator = true
|
||||
|
||||
private let repository = LiveRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
|
@ -178,7 +180,8 @@ final class LiveRoomCreateViewModel: ObservableObject {
|
|||
password: (roomType == .PRIVATE && !password.trimmingCharacters(in: .whitespaces).isEmpty) ? password : nil,
|
||||
menuPanId: isActivateMenu ? menuId : 0,
|
||||
menuPan: isActivateMenu ? menu : "",
|
||||
isActiveMenuPan: isActivateMenu
|
||||
isActiveMenuPan: isActivateMenu,
|
||||
isAvailableJoinCreator: isAvailableJoinCreator
|
||||
)
|
||||
|
||||
if timeSettingMode == .RESERVATION {
|
||||
|
|
|
@ -23,7 +23,7 @@ struct LiveRoomDonationMessageDialog: View {
|
|||
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("후원메시지")
|
||||
Text("후원 히스토리")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
|
@ -45,43 +45,19 @@ struct LiveRoomDonationMessageDialog: View {
|
|||
ForEach(0..<viewModel.donationMessageList.count, id: \.self) { index in
|
||||
let donationMessage = viewModel.donationMessageList[index]
|
||||
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("\(donationMessage.nickname)님이")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("\(donationMessage.canMessage)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("'\(donationMessage.donationMessage)'")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_close_white")
|
||||
.resizable()
|
||||
.frame(width: 13.3, height: 13.3)
|
||||
.onTapGesture {
|
||||
viewModel.deleteDonationMessage(uuid: donationMessage.uuid)
|
||||
}
|
||||
LiveRoomDonationMessageItemView(message: donationMessage) {
|
||||
viewModel.deleteDonationMessage(uuid: $0)
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(Color(hex: "333333"))
|
||||
.cornerRadius(5.3)
|
||||
.onTapGesture {
|
||||
UIPasteboard.general.string = donationMessage.donationMessage
|
||||
self.viewModel.errorMessage = "후원 메시지가 복사되었습니다."
|
||||
self.viewModel.errorMessage = "후원 히스토리가 복사되었습니다."
|
||||
self.viewModel.isShowPopup = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 18.7)
|
||||
} else {
|
||||
Text("후원메시지가 없습니다.")
|
||||
Text("후원 히스토리가 없습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.top, 30)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// LiveRoomDonationMessageItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/11/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LiveRoomDonationMessageItemView: View {
|
||||
|
||||
let message: LiveRoomDonationMessage
|
||||
let deleteDonationMessage: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("\(message.nickname)님이")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
if !message.canMessage.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
Text("\(message.canMessage)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text("'\(message.donationMessage)'")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_close_white")
|
||||
.resizable()
|
||||
.frame(width: 13.3, height: 13.3)
|
||||
.onTapGesture { deleteDonationMessage(message.uuid) }
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(message.canMessage.trimmingCharacters(in: .whitespaces).isEmpty ? Color(hex: "c25264").opacity(0.8) : Color.gray33)
|
||||
.cornerRadius(5.3)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("일반후원 메시지") {
|
||||
LiveRoomDonationMessageItemView(
|
||||
message: LiveRoomDonationMessage(
|
||||
uuid: "",
|
||||
nickname: "유저2님이",
|
||||
canMessage: "10캔을 후원하셨습니다",
|
||||
donationMessage: "ㅅㅅㅅ"
|
||||
),
|
||||
deleteDonationMessage: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("룰렛후원 메시지") {
|
||||
LiveRoomDonationMessageItemView(
|
||||
message: LiveRoomDonationMessage(
|
||||
uuid: "",
|
||||
nickname: "유저2님의 룰렛 결과?",
|
||||
canMessage: "",
|
||||
donationMessage: "[테스트] 당첨!"
|
||||
),
|
||||
deleteDonationMessage: { _ in }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// LiveRoomDonationResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LiveRoomDonationResponse: Codable {
|
||||
let imageUrl: String
|
||||
let time: Int
|
||||
}
|
|
@ -119,6 +119,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
@Published var noticeViewHeight: CGFloat = UIFont.systemFontSize
|
||||
|
||||
@Published var isBgOn = true
|
||||
@Published var isSignatureOn = true
|
||||
@Published var donationStatus: GetLiveRoomDonationStatusResponse?
|
||||
|
||||
@Published private(set) var offset: CGFloat = 0
|
||||
|
@ -150,7 +151,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
@Published var isShowRouletteSettings = false
|
||||
|
||||
@Published var isShowRoulettePreview = false
|
||||
@Published var roulettePreview: RoulettePreview? = nil
|
||||
@Published var roulettePreviewList = [RoulettePreview]()
|
||||
|
||||
@Published var isShowRoulette = false
|
||||
@Published var rouletteItems = [String]()
|
||||
|
@ -159,17 +160,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
|
||||
@Published var signatureImageUrl = "" {
|
||||
didSet {
|
||||
if signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
|
||||
if let imageUrl = self.signatureImageUrls.first {
|
||||
self.signatureImageUrl = imageUrl
|
||||
self.signatureImageUrls.removeFirst()
|
||||
} else {
|
||||
self.signatureImageUrl = ""
|
||||
self.isShowSignatureImage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
showSignatureImage()
|
||||
}
|
||||
}
|
||||
|
||||
@Published var signature: LiveRoomDonationResponse? = nil {
|
||||
didSet {
|
||||
showSignatureImage()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,6 +184,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
@Published var selectedMenu: SelectedMenu? = nil
|
||||
|
||||
var signatureImageUrls = [String]()
|
||||
var signatureList = [LiveRoomDonationResponse]()
|
||||
var isShowSignatureImage = false
|
||||
|
||||
var timer: DispatchSourceTimer?
|
||||
|
@ -399,7 +397,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<String>.self, from: responseData)
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<LiveRoomDonationResponse>.self, from: responseData)
|
||||
|
||||
self.isLoading = false
|
||||
|
||||
|
@ -409,7 +407,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
type: .DONATION,
|
||||
message: rawMessage,
|
||||
can: can,
|
||||
signatureImageUrl: decoded.data,
|
||||
signature: decoded.data,
|
||||
signatureImageUrl: decoded.data?.imageUrl,
|
||||
donationMessage: message
|
||||
)
|
||||
|
||||
|
@ -431,7 +430,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
)
|
||||
|
||||
totalDonationCan += can
|
||||
showSignatureImage(imageUrl: decoded.data ?? "")
|
||||
addSignature(signature: decoded.data)
|
||||
|
||||
self.messageChangeFlag.toggle()
|
||||
if self.messages.count > 100 {
|
||||
|
@ -815,50 +814,6 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func shareRoom() {
|
||||
guard let link = URL(string: "https://sodalive.net/?room_id=\(AppState.shared.roomId)") else { return }
|
||||
let dynamicLinksDomainURIPrefix = "https://sodalive.page.link"
|
||||
guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
|
||||
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||
self.isShowErrorPopup = true
|
||||
return
|
||||
}
|
||||
|
||||
linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.sodalive")
|
||||
linkBuilder.iOSParameters?.appStoreID = "6461721697"
|
||||
|
||||
linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.sodalive")
|
||||
|
||||
guard let longDynamicLink = linkBuilder.url else {
|
||||
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||
self.isShowErrorPopup = true
|
||||
return
|
||||
}
|
||||
DEBUG_LOG("The long URL is: \(longDynamicLink)")
|
||||
|
||||
DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in
|
||||
let shortUrl = url?.absoluteString
|
||||
|
||||
if let liveRoomInfo = self.liveRoomInfo {
|
||||
let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString
|
||||
if liveRoomInfo.isPrivateRoom {
|
||||
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 비공개라이브에 초대하였습니다.\n" +
|
||||
"※ 라이브 참여: \(urlString)\n" +
|
||||
"(입장 비밀번호: \(liveRoomInfo.password!))"
|
||||
} else {
|
||||
shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 소다라이브 공개라이브에 초대하였습니다.\n" +
|
||||
"※ 라이브 참여: \(urlString)"
|
||||
}
|
||||
|
||||
isShowShareView = true
|
||||
} else {
|
||||
self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
|
||||
self.isShowErrorPopup = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func kickOut() {
|
||||
repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
|
||||
.sink { result in
|
||||
|
@ -1501,6 +1456,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
|
||||
func showRoulette() {
|
||||
if let liveRoomInfo = liveRoomInfo, !isLoading {
|
||||
self.roulettePreviewList.removeAll()
|
||||
isLoading = true
|
||||
|
||||
rouletteRepository.getRoulette(creatorId: liveRoomInfo.creatorId)
|
||||
|
@ -1517,10 +1473,14 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetRouletteResponse>.self, from: responseData)
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetRouletteResponse]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success, !data.items.isEmpty {
|
||||
self.roulettePreview = RoulettePreview(can: data.can, items: calculatePercentages(options: data.items))
|
||||
if let data = decoded.data, decoded.success, !data.isEmpty {
|
||||
let roulettePreviewList = data
|
||||
.filter { $0.isActive }
|
||||
.filter { !$0.items.isEmpty}
|
||||
.map { RoulettePreview(id: $0.id, can: $0.can, items: calculatePercentages(options: $0.items)) }
|
||||
self.roulettePreviewList.append(contentsOf: roulettePreviewList)
|
||||
self.isShowRoulettePreview = true
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
|
@ -1539,10 +1499,10 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func spinRoulette() {
|
||||
func spinRoulette(rouletteId: Int) {
|
||||
if !isLoading {
|
||||
isLoading = true
|
||||
rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId))
|
||||
rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId, rouletteId: rouletteId))
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
|
@ -1556,11 +1516,15 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetRouletteResponse>.self, from: responseData)
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<SpinRouletteResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success, !data.items.isEmpty {
|
||||
UserDefaults.set(UserDefaults.int(forKey: .can) - data.can, forKey: .can)
|
||||
randomSelectRouletteItem(can: data.can, items: data.items)
|
||||
self.rouletteItems.removeAll()
|
||||
self.rouletteItems.append(contentsOf: data.items.map { $0.title })
|
||||
self.rouletteSelectedItem = data.result
|
||||
self.rouletteCan = data.can
|
||||
self.isShowRoulette = true
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
|
@ -1619,35 +1583,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
private func calculatePercentages(options: [RouletteItem]) -> [RoulettePreviewItem] {
|
||||
let totalWeight = options.reduce(0) { $0 + $1.weight }
|
||||
let updatedOptions = options.map { option in
|
||||
let percent = floor(Double(option.weight) / Double(totalWeight) * 10000) / 100
|
||||
return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", percent))%")
|
||||
return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", option.percentage))%")
|
||||
}
|
||||
|
||||
return updatedOptions
|
||||
}
|
||||
|
||||
private func randomSelectRouletteItem(can: Int, items: [RouletteItem]) {
|
||||
isLoading = true
|
||||
|
||||
var rouletteItems = [String]()
|
||||
items.forEach {
|
||||
var i = 1
|
||||
while (i < $0.weight * 10) {
|
||||
rouletteItems.append($0.title)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
self.rouletteItems.removeAll()
|
||||
self.rouletteItems.append(contentsOf: items.map { $0.title })
|
||||
self.rouletteSelectedItem = rouletteItems.randomElement()!
|
||||
self.rouletteCan = can
|
||||
self.isShowRoulette = true
|
||||
}
|
||||
|
||||
private func refundRouletteDonation() {
|
||||
isLoading = true
|
||||
|
||||
|
@ -1689,7 +1631,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
private func showSignatureImage(imageUrl: String) {
|
||||
private func addSignatureImage(imageUrl: String) {
|
||||
if imageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
|
||||
if !isShowSignatureImage {
|
||||
isShowSignatureImage = true
|
||||
|
@ -1699,6 +1641,41 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addSignature(signature: LiveRoomDonationResponse?) {
|
||||
if let signature = signature {
|
||||
if !isShowSignatureImage {
|
||||
self.signature = signature
|
||||
isShowSignatureImage = true
|
||||
} else {
|
||||
self.signatureList.append(signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showSignatureImage() {
|
||||
if let signature = signature {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(signature.time)) {
|
||||
if let nextSignature = self.signatureList.first {
|
||||
self.signature = nextSignature
|
||||
self.signatureList.removeFirst()
|
||||
} else {
|
||||
self.signature = nil
|
||||
self.isShowSignatureImage = false
|
||||
}
|
||||
}
|
||||
} else if signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
|
||||
if let imageUrl = self.signatureImageUrls.first {
|
||||
self.signatureImageUrl = imageUrl
|
||||
self.signatureImageUrls.removeFirst()
|
||||
} else {
|
||||
self.signatureImageUrl = ""
|
||||
self.isShowSignatureImage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
||||
|
@ -1863,7 +1840,12 @@ extension LiveRoomViewModel: AgoraRtmChannelDelegate {
|
|||
)
|
||||
|
||||
self.totalDonationCan += decoded.can
|
||||
self.showSignatureImage(imageUrl: decoded.signatureImageUrl ?? "")
|
||||
|
||||
if let signature = decoded.signature {
|
||||
self.addSignature(signature: signature)
|
||||
} else if let imageUrl = decoded.signatureImageUrl {
|
||||
self.addSignatureImage(imageUrl: imageUrl)
|
||||
}
|
||||
} else if decoded.type == .ROULETTE_DONATION {
|
||||
self.messages.append(
|
||||
LiveRoomRouletteDonationChat(
|
||||
|
|
|
@ -9,11 +9,10 @@ import SwiftUI
|
|||
|
||||
class RouletteOption: ObservableObject {
|
||||
var title: String
|
||||
var weight: Int
|
||||
var percentage: String = "50.00"
|
||||
var percentage: String = ""
|
||||
|
||||
init(title: String, weight: Int) {
|
||||
init(title: String, percentage: String) {
|
||||
self.title = title
|
||||
self.weight = weight
|
||||
self.percentage = percentage
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,8 @@ struct RouletteSettingsOptionView: View {
|
|||
@ObservedObject var option: RouletteOption
|
||||
|
||||
let index: Int
|
||||
|
||||
let onClickPlus: () -> Void
|
||||
let onClickDelete: () -> Void
|
||||
let onClickSubstract: () -> Void
|
||||
let calculateTotalPercentage: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6.7) {
|
||||
|
@ -47,19 +45,30 @@ struct RouletteSettingsOptionView: View {
|
|||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(6.7)
|
||||
|
||||
Text("\(option.percentage)%")
|
||||
HStack(spacing: 0) {
|
||||
TextField("0.00", text: $option.percentage)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(6.7)
|
||||
.keyboardType(.decimalPad)
|
||||
.onChange(of: option.percentage) { newValue in
|
||||
if newValue.count > 5 {
|
||||
option.percentage = String(newValue.prefix(5))
|
||||
}
|
||||
|
||||
Image("btn_minus_round_rect")
|
||||
.onTapGesture { onClickSubstract() }
|
||||
calculateTotalPercentage()
|
||||
}
|
||||
|
||||
Image("btn_plus_round_rect")
|
||||
.onTapGesture { onClickPlus() }
|
||||
Text("%")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 16.7)
|
||||
.frame(maxWidth: 85)
|
||||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,11 +77,10 @@ struct RouletteSettingsOptionView: View {
|
|||
struct RouletteSettingsOptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RouletteSettingsOptionView(
|
||||
option: RouletteOption(title: "옵션1", weight: 1),
|
||||
option: RouletteOption(title: "옵션1", percentage: ""),
|
||||
index: 2,
|
||||
onClickPlus: {},
|
||||
onClickDelete: {},
|
||||
onClickSubstract: {}
|
||||
calculateTotalPercentage: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,11 @@ struct RouletteSettingsView: View {
|
|||
SelectedButtonView(
|
||||
title: "룰렛 2",
|
||||
isActive: viewModel.rouletteList.count > 0,
|
||||
isSelected: viewModel.selectedRoulette == .ROULETTE_2
|
||||
isSelected: viewModel.selectedRoulette == .ROULETTE_2,
|
||||
checkImage: "ic_select_check_black",
|
||||
bgSelectedColor: Color(hex: "ffcb14"),
|
||||
textSelectedColor: Color.black,
|
||||
textDefaultColor: Color(hex: "ffcb14")
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.selectRoulette(selectedRoulette: .ROULETTE_2)
|
||||
|
@ -47,7 +51,9 @@ struct RouletteSettingsView: View {
|
|||
SelectedButtonView(
|
||||
title: "룰렛 3",
|
||||
isActive: viewModel.rouletteList.count > 1,
|
||||
isSelected: viewModel.selectedRoulette == .ROULETTE_3
|
||||
isSelected: viewModel.selectedRoulette == .ROULETTE_3,
|
||||
bgSelectedColor: Color(hex: "ff14d9"),
|
||||
textDefaultColor: Color(hex: "ff14d9")
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.selectRoulette(selectedRoulette: .ROULETTE_3)
|
||||
|
@ -58,7 +64,7 @@ struct RouletteSettingsView: View {
|
|||
HStack(spacing: 0) {
|
||||
Text("룰렛을 활성화 하시겠습니까?")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
@ -74,7 +80,7 @@ struct RouletteSettingsView: View {
|
|||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("룰렛 금액 설정")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
TextField("룰렛 금액을 입력해 주세요 (최소 5캔)", text: Binding(
|
||||
|
@ -88,19 +94,19 @@ struct RouletteSettingsView: View {
|
|||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.keyboardType(.numberPad)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 16.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(6.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("캔")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
.padding(.top, 26.7)
|
||||
|
@ -108,12 +114,12 @@ struct RouletteSettingsView: View {
|
|||
VStack(alignment: .leading, spacing: 21.3) {
|
||||
Text("룰렛 옵션 설정")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("※ 룰렛 옵션은 최소 2개,\n최대 10개까지 설정할 수 있습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
.foregroundColor(Color.mainRed)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
@ -123,14 +129,26 @@ struct RouletteSettingsView: View {
|
|||
}
|
||||
.padding(.top, 26.7)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("옵션 확률 합계")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("( \(String(format: "%.2f", viewModel.totalPercentage))% )")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
.padding(.top, 21.3)
|
||||
|
||||
LazyVStack(spacing: 21.3) {
|
||||
ForEach(viewModel.options.indices, id: \.self) { index in
|
||||
RouletteSettingsOptionView(
|
||||
option: viewModel.options[index],
|
||||
index: index,
|
||||
onClickPlus: { viewModel.plusWeight(index: index) },
|
||||
onClickDelete: { viewModel.deleteOption(index: index) },
|
||||
onClickSubstract: { viewModel.subtractWeight(index: index) }
|
||||
calculateTotalPercentage: { viewModel.calculateTotalPercentage() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -145,13 +163,13 @@ struct RouletteSettingsView: View {
|
|||
HStack(spacing: 13.3) {
|
||||
Text("미리보기")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.vertical, 16)
|
||||
.frame(maxWidth: .infinity)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.foregroundColor(Color.button)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.onClickPreview()
|
||||
|
@ -162,7 +180,7 @@ struct RouletteSettingsView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(hex: "3bb9f1"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.createOrUpdateRoulette {
|
||||
|
@ -172,12 +190,12 @@ struct RouletteSettingsView: View {
|
|||
}
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: screenSize().width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +208,7 @@ struct RouletteSettingsView: View {
|
|||
isShowing: $viewModel.isShowPreview,
|
||||
title: "룰렛 미리보기",
|
||||
onClickSpin: nil,
|
||||
preview: preview
|
||||
previewList: [preview]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +220,7 @@ struct RouletteSettingsView: View {
|
|||
.padding(.vertical, 13.3)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.cornerRadius(20)
|
||||
|
|
|
@ -46,52 +46,19 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
private var rouletteId = 0
|
||||
@Published var rouletteList = [GetNewRouletteResponse]()
|
||||
|
||||
func plusWeight(index: Int) {
|
||||
options[index].weight += 1
|
||||
recalculatePercentages()
|
||||
}
|
||||
|
||||
func subtractWeight(index: Int) {
|
||||
if options[index].weight > 1 {
|
||||
options[index].weight -= 1
|
||||
recalculatePercentages()
|
||||
}
|
||||
}
|
||||
@Published var totalPercentage = Float(0)
|
||||
|
||||
func addOption() {
|
||||
if (options.count >= 10) {
|
||||
return
|
||||
}
|
||||
options.append(RouletteOption(title: "", weight: 1))
|
||||
recalculatePercentages()
|
||||
options.append(RouletteOption(title: "", percentage: ""))
|
||||
calculateTotalPercentage()
|
||||
}
|
||||
|
||||
func deleteOption(index: Int) {
|
||||
options.remove(at: index)
|
||||
recalculatePercentages()
|
||||
}
|
||||
|
||||
private func recalculatePercentages() {
|
||||
let options = options
|
||||
|
||||
var totalWeight = 0
|
||||
for option in options {
|
||||
totalWeight += option.weight
|
||||
}
|
||||
|
||||
guard totalWeight > 0 else { return }
|
||||
|
||||
for i in 0..<options.count {
|
||||
let percent = floor(Double(options[i].weight) / Double(totalWeight) * 10000) / 100
|
||||
options[i].percentage = String(format: "%.2f", percent)
|
||||
}
|
||||
|
||||
removeAllAndAddOptions(options: options)
|
||||
}
|
||||
|
||||
private func removeAllAndAddOptions(options: [RouletteOption]) {
|
||||
self.options.removeAll()
|
||||
self.options.append(contentsOf: options)
|
||||
calculateTotalPercentage()
|
||||
}
|
||||
|
||||
func getAllRoulette(creatorId: Int) {
|
||||
|
@ -143,7 +110,7 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
items.append(RoulettePreviewItem(title: option.title, percent: "\(option.percentage)%"))
|
||||
}
|
||||
|
||||
previewData = RoulettePreview(can: self.can, items: items)
|
||||
previewData = RoulettePreview(id: 0, can: self.can, items: items)
|
||||
isLoading = false
|
||||
isShowPreview = true
|
||||
}
|
||||
|
@ -152,25 +119,48 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
if !isLoading {
|
||||
isLoading = true
|
||||
|
||||
if rouletteId > 0 {
|
||||
updateRoulette(onSuccess: onSuccess)
|
||||
} else {
|
||||
createRoulette(onSuccess: onSuccess)
|
||||
if validationOptions() {
|
||||
if rouletteId > 0 {
|
||||
updateRoulette(onSuccess: onSuccess)
|
||||
} else {
|
||||
createRoulette(onSuccess: onSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createRoulette(onSuccess: @escaping (Bool, String) -> Void) {
|
||||
var items = [RouletteItem]()
|
||||
private func validationOptions() -> Bool {
|
||||
var totalPercentage = Float(0)
|
||||
|
||||
for option in options {
|
||||
if option.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
isLoading = false
|
||||
errorMessage = "옵션은 빈칸일 수 없습니다."
|
||||
isShowErrorPopup = true
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
items.append(RouletteItem(title: option.title, weight: option.weight))
|
||||
if let percentage = Float(option.percentage) {
|
||||
totalPercentage += percentage
|
||||
}
|
||||
}
|
||||
|
||||
if totalPercentage != Float(100) {
|
||||
isLoading = false
|
||||
errorMessage = "확률이 100%가 아닙니다"
|
||||
isShowErrorPopup = true
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func createRoulette(onSuccess: @escaping (Bool, String) -> Void) {
|
||||
var items = [RouletteItem]()
|
||||
for option in options {
|
||||
if let percentage = Float(option.percentage) {
|
||||
items.append(RouletteItem(title: option.title, percentage: percentage))
|
||||
}
|
||||
}
|
||||
|
||||
let selectedRouletteTitle: String
|
||||
|
@ -227,14 +217,9 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
private func updateRoulette(onSuccess: @escaping (Bool, String) -> Void) {
|
||||
var items = [RouletteItem]()
|
||||
for option in options {
|
||||
if option.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
isLoading = false
|
||||
errorMessage = "옵션은 빈칸일 수 없습니다."
|
||||
isShowErrorPopup = true
|
||||
return
|
||||
if let percentage = Float(option.percentage) {
|
||||
items.append(RouletteItem(title: option.title, percentage: percentage))
|
||||
}
|
||||
|
||||
items.append(RouletteItem(title: option.title, weight: option.weight))
|
||||
}
|
||||
|
||||
let selectedRoulette = rouletteList[selectedRoulette!.rawValue]
|
||||
|
@ -259,24 +244,10 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
selectedRouletteTitle = "룰렛 1"
|
||||
}
|
||||
|
||||
var isAllActive = false
|
||||
|
||||
rouletteList
|
||||
.filter {
|
||||
$0.id != selectedRoulette.id
|
||||
}
|
||||
.forEach {
|
||||
if $0.isActive {
|
||||
isAllActive = true
|
||||
}
|
||||
}
|
||||
|
||||
if isActive {
|
||||
successMessage = "\(selectedRouletteTitle)로 설정하였습니다."
|
||||
} else if !isAllActive {
|
||||
successMessage = "\(selectedRouletteTitle)이 비활성화 되었습니다."
|
||||
successMessage = "\(selectedRouletteTitle)을 활성화 했습니다."
|
||||
} else {
|
||||
successMessage = "\(selectedRouletteTitle)을 설정했습니다."
|
||||
successMessage = "\(selectedRouletteTitle)을 비활성화 했습니다."
|
||||
}
|
||||
|
||||
let request = UpdateRouletteRequest(id: rouletteId, can: can, isActive: isActive, items: items)
|
||||
|
@ -337,10 +308,11 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
self.rouletteId = roulette.id
|
||||
self.isActive = roulette.isActive
|
||||
let options = roulette.items.map {
|
||||
RouletteOption(title: $0.title, weight: $0.weight)
|
||||
RouletteOption(title: $0.title, percentage: String($0.percentage))
|
||||
}
|
||||
removeAllAndAddOptions(options: options)
|
||||
recalculatePercentages()
|
||||
|
||||
self.options.removeAll()
|
||||
self.options.append(contentsOf: options)
|
||||
} else {
|
||||
self.canText = ""
|
||||
self.isActive = false
|
||||
|
@ -351,5 +323,21 @@ final class RouletteSettingsViewModel: ObservableObject {
|
|||
self.addOption()
|
||||
}
|
||||
}
|
||||
|
||||
calculateTotalPercentage()
|
||||
}
|
||||
|
||||
func calculateTotalPercentage() {
|
||||
let totalPercent = options.map {
|
||||
let percentage = Float($0.percentage)
|
||||
|
||||
if let percentage = percentage {
|
||||
return percentage
|
||||
} else {
|
||||
return Float(0)
|
||||
}
|
||||
}.reduce(0, +)
|
||||
|
||||
self.totalPercentage = totalPercent
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
struct GetRouletteResponse: Decodable {
|
||||
let id: Int
|
||||
let can: Int
|
||||
let isActive: Bool
|
||||
let items: [RouletteItem]
|
||||
|
@ -13,5 +14,5 @@ struct GetRouletteResponse: Decodable {
|
|||
|
||||
struct RouletteItem: Codable, Equatable {
|
||||
let title: String
|
||||
let weight: Int
|
||||
let percentage: Float
|
||||
}
|
||||
|
|
|
@ -25,16 +25,16 @@ extension RouletteApi: TargetType {
|
|||
var path: String {
|
||||
switch self {
|
||||
case .getRoulette, .createRoulette, .updateRoulette:
|
||||
return "/new-roulette"
|
||||
return "/v2/roulette"
|
||||
|
||||
case .getAllRoulette:
|
||||
return "/new-roulette/creator"
|
||||
return "/v2/roulette/creator"
|
||||
|
||||
case .spinRoulette:
|
||||
return "/new-roulette/spin"
|
||||
return "/v2/roulette/spin"
|
||||
|
||||
case .refundRouletteDonation(let roomId):
|
||||
return "/new-roulette/refund/\(roomId)"
|
||||
return "/v2/roulette/refund/\(roomId)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
|
||||
struct RoulettePreview {
|
||||
let id: Int
|
||||
let can: Int
|
||||
let items: [RoulettePreviewItem]
|
||||
}
|
||||
|
|
|
@ -12,13 +12,61 @@ struct RoulettePreviewDialog: View {
|
|||
@Binding var isShowing: Bool
|
||||
|
||||
let title: String?
|
||||
let onClickSpin: (() -> Void)?
|
||||
let preview: RoulettePreview
|
||||
let onClickSpin: ((Int) -> Void)?
|
||||
let previewList: [RoulettePreview]
|
||||
|
||||
@State var selectedRoulette = SelectedRoulette.ROULETTE_1
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 16.7) {
|
||||
if previewList.count > 1 {
|
||||
HStack(spacing: 13.3) {
|
||||
SelectedButtonView(
|
||||
title: "룰렛 1",
|
||||
isActive: true,
|
||||
isSelected: selectedRoulette == .ROULETTE_1
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedRoulette != .ROULETTE_1 {
|
||||
selectedRoulette = .ROULETTE_1
|
||||
}
|
||||
}
|
||||
|
||||
SelectedButtonView(
|
||||
title: "룰렛 2",
|
||||
isActive: true,
|
||||
isSelected: selectedRoulette == .ROULETTE_2,
|
||||
checkImage: "ic_select_check_black",
|
||||
bgSelectedColor: Color(hex: "ffcb14"),
|
||||
textSelectedColor: Color.black,
|
||||
textDefaultColor: Color(hex: "ffcb14")
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedRoulette != .ROULETTE_2 {
|
||||
selectedRoulette = .ROULETTE_2
|
||||
}
|
||||
}
|
||||
|
||||
if previewList.count > 2 {
|
||||
SelectedButtonView(
|
||||
title: "룰렛 3",
|
||||
isActive: true,
|
||||
isSelected: selectedRoulette == .ROULETTE_3,
|
||||
bgSelectedColor: Color(hex: "ff14d9"),
|
||||
textDefaultColor: Color(hex: "ff14d9")
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedRoulette != .ROULETTE_3 {
|
||||
selectedRoulette = .ROULETTE_3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16.7)
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text(title ?? "룰렛")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
|
@ -42,54 +90,66 @@ struct RoulettePreviewDialog: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16.7)
|
||||
.padding(.top, previewList.count > 1 ? 0 : 16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LazyVStack(alignment: .leading, spacing: 13.3) {
|
||||
ForEach(preview.items.indices, id: \.self) { index in
|
||||
ForEach(previewList[selectedRoulette.rawValue].items.indices, id: \.self) { index in
|
||||
HStack(spacing:13.3) {
|
||||
Text("\(index + 1)")
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
|
||||
Text("\(preview.items[index].title) (\(preview.items[index].percent))")
|
||||
Text("\(previewList[selectedRoulette.rawValue].items[index].title) (\(previewList[selectedRoulette.rawValue].items[index].percent))")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
Text("취소")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.foregroundColor(
|
||||
selectedRoulette == .ROULETTE_2 ? Color(hex: "ffcb14") :
|
||||
selectedRoulette == .ROULETTE_3 ? Color(hex: "ff14d9") :
|
||||
Color.button
|
||||
)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 16)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color(hex: "3bb9f1"), lineWidth: 1)
|
||||
.stroke(
|
||||
selectedRoulette == .ROULETTE_2 ? Color(hex: "ffcb14") :
|
||||
selectedRoulette == .ROULETTE_3 ? Color(hex: "ff14d9") :
|
||||
Color.button,
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.onTapGesture {
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
Text("\(preview.can)캔으로 룰렛 돌리기")
|
||||
Text("\(previewList[selectedRoulette.rawValue].can)캔으로 룰렛 돌리기")
|
||||
.font(.custom(Font.bold.rawValue, size: 16))
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(selectedRoulette == .ROULETTE_2 ? .black : .white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(hex: "3bb9f1"))
|
||||
.background(
|
||||
selectedRoulette == .ROULETTE_2 ? Color(hex: "ffcb14") :
|
||||
selectedRoulette == .ROULETTE_3 ? Color(hex: "ff14d9") :
|
||||
Color.button
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
if let onClickSpin = onClickSpin {
|
||||
onClickSpin()
|
||||
onClickSpin(previewList[selectedRoulette.rawValue].id)
|
||||
}
|
||||
isShowing = false
|
||||
}
|
||||
}
|
||||
.padding(.top, 26.7)
|
||||
.padding(.top, 6.7)
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(Color(hex: "222222"))
|
||||
|
@ -108,13 +168,32 @@ struct RoulettePreviewDialog_Previews: PreviewProvider {
|
|||
isShowing: .constant(true),
|
||||
title: nil,
|
||||
onClickSpin: nil,
|
||||
preview: RoulettePreview(
|
||||
can: 100,
|
||||
items: [
|
||||
RoulettePreviewItem(title: "옵션1", percent: "10%"),
|
||||
RoulettePreviewItem(title: "옵션2", percent: "90%"),
|
||||
]
|
||||
)
|
||||
previewList: [
|
||||
RoulettePreview(
|
||||
id: 0,
|
||||
can: 100,
|
||||
items: [
|
||||
RoulettePreviewItem(title: "옵션1", percent: "33.40%"),
|
||||
RoulettePreviewItem(title: "옵션2", percent: "66.60%"),
|
||||
]
|
||||
),
|
||||
RoulettePreview(
|
||||
id: 1,
|
||||
can: 10,
|
||||
items: [
|
||||
RoulettePreviewItem(title: "옵션3", percent: "10.8%"),
|
||||
RoulettePreviewItem(title: "옵션4", percent: "89.2%"),
|
||||
]
|
||||
),
|
||||
RoulettePreview(
|
||||
id: 2,
|
||||
can: 1000,
|
||||
items: [
|
||||
RoulettePreviewItem(title: "옵션5", percent: "10%"),
|
||||
RoulettePreviewItem(title: "옵션6", percent: "90%"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ import Foundation
|
|||
|
||||
struct SpinRouletteRequest: Encodable {
|
||||
let roomId: Int
|
||||
let rouletteId: Int
|
||||
let container: String = "ios"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// SpinRouletteResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/11/24.
|
||||
//
|
||||
|
||||
struct SpinRouletteResponse: Decodable {
|
||||
let can: Int
|
||||
let result: String
|
||||
let items: [RouletteItem]
|
||||
}
|
|
@ -46,7 +46,7 @@ struct LiveRoomInfoCreatorView: View {
|
|||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.7) {
|
||||
VStack(alignment: .leading, spacing: 6.7) {
|
||||
HStack(spacing: 2.7) {
|
||||
if isAdult {
|
||||
Text("19")
|
||||
|
|
|
@ -15,6 +15,7 @@ struct LiveRoomInfoGuestView: View {
|
|||
let isOnBg: Bool
|
||||
let isOnNotice: Bool
|
||||
let isOnMenuPan: Bool
|
||||
let isOnSignature: Bool
|
||||
let isShowMenuPanButton: Bool
|
||||
|
||||
let creatorId: Int
|
||||
|
@ -29,13 +30,13 @@ struct LiveRoomInfoGuestView: View {
|
|||
|
||||
let onClickQuit: () -> Void
|
||||
let onClickToggleBg: () -> Void
|
||||
let onClickShare: () -> Void
|
||||
let onClickFollow: (Bool) -> Void
|
||||
let onClickProfile: (Int) -> Void
|
||||
let onClickNotice: () -> Void
|
||||
let onClickMenuPan: () -> Void
|
||||
let onClickTotalDonation: () -> Void
|
||||
let onClickChangeListener: () -> Void
|
||||
let onClickToggleSignature: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
@ -61,6 +62,18 @@ struct LiveRoomInfoGuestView: View {
|
|||
) { onClickChangeListener() }
|
||||
}
|
||||
|
||||
LiveRoomOverlayStrokeTextToggleButton(
|
||||
isOn: isOnSignature,
|
||||
onText: "시그 ON",
|
||||
onTextColor: Color.button,
|
||||
onStrokeColor: Color.button,
|
||||
offText: "시그 OFF",
|
||||
offTextColor: Color.graybb,
|
||||
offStrokeColor: Color.graybb,
|
||||
strokeWidth: 1,
|
||||
strokeCornerRadius: 5.3
|
||||
) { onClickToggleSignature() }
|
||||
|
||||
LiveRoomOverlayStrokeTextToggleButton(
|
||||
isOn: isOnBg,
|
||||
onText: "배경 ON",
|
||||
|
@ -72,13 +85,6 @@ struct LiveRoomInfoGuestView: View {
|
|||
strokeWidth: 1,
|
||||
strokeCornerRadius: 5.3
|
||||
) { onClickToggleBg() }
|
||||
|
||||
LiveRoomOverlayStrokeImageButton(
|
||||
imageName: "ic_share",
|
||||
strokeColor: Color.graybb,
|
||||
strokeWidth: 1,
|
||||
strokeCornerRadius: 5.3
|
||||
) { onClickShare() }
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
|
@ -181,6 +187,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
|
|||
isOnBg: true,
|
||||
isOnNotice: false,
|
||||
isOnMenuPan: false,
|
||||
isOnSignature: false,
|
||||
isShowMenuPanButton: false,
|
||||
creatorId: 1,
|
||||
creatorNickname: "도화",
|
||||
|
@ -211,13 +218,13 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
|
|||
isAdult: false,
|
||||
onClickQuit: {},
|
||||
onClickToggleBg: {},
|
||||
onClickShare: {},
|
||||
onClickFollow: { _ in },
|
||||
onClickProfile: { _ in },
|
||||
onClickNotice: {},
|
||||
onClickMenuPan: {},
|
||||
onClickTotalDonation: {},
|
||||
onClickChangeListener: {}
|
||||
onClickChangeListener: {},
|
||||
onClickToggleSignature: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ struct LiveRoomInfoHostView: View {
|
|||
let isOnBg: Bool
|
||||
let isOnNotice: Bool
|
||||
let isOnMenuPan: Bool
|
||||
let isOnSignature: Bool
|
||||
let isShowMenuPanButton: Bool
|
||||
|
||||
let creatorId: Int
|
||||
|
@ -30,13 +31,13 @@ struct LiveRoomInfoHostView: View {
|
|||
|
||||
let onClickQuit: () -> Void
|
||||
let onClickToggleBg: () -> Void
|
||||
let onClickShare: () -> Void
|
||||
let onClickEdit: () -> Void
|
||||
let onClickProfile: (Int) -> Void
|
||||
let onClickNotice: () -> Void
|
||||
let onClickMenuPan: () -> Void
|
||||
let onClickTotalDonation: () -> Void
|
||||
let onClickParticipants: () -> Void
|
||||
let onClickToggleSignature: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
@ -52,6 +53,18 @@ struct LiveRoomInfoHostView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
LiveRoomOverlayStrokeTextToggleButton(
|
||||
isOn: isOnSignature,
|
||||
onText: "시그 ON",
|
||||
onTextColor: Color.button,
|
||||
onStrokeColor: Color.button,
|
||||
offText: "시그 OFF",
|
||||
offTextColor: Color.graybb,
|
||||
offStrokeColor: Color.graybb,
|
||||
strokeWidth: 1,
|
||||
strokeCornerRadius: 5.3
|
||||
) { onClickToggleSignature() }
|
||||
|
||||
LiveRoomOverlayStrokeTextToggleButton(
|
||||
isOn: isOnBg,
|
||||
onText: "배경 ON",
|
||||
|
@ -64,13 +77,6 @@ struct LiveRoomInfoHostView: View {
|
|||
strokeCornerRadius: 5.3
|
||||
) { onClickToggleBg() }
|
||||
|
||||
LiveRoomOverlayStrokeImageButton(
|
||||
imageName: "ic_share",
|
||||
strokeColor: Color.graybb,
|
||||
strokeWidth: 1,
|
||||
strokeCornerRadius: 5.3
|
||||
) { onClickShare() }
|
||||
|
||||
LiveRoomOverlayStrokeImageButton(
|
||||
imageName: "ic_edit",
|
||||
strokeColor: Color.graybb,
|
||||
|
@ -197,6 +203,7 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
|
|||
isOnBg: true,
|
||||
isOnNotice: true,
|
||||
isOnMenuPan: false,
|
||||
isOnSignature: false,
|
||||
isShowMenuPanButton: false,
|
||||
creatorId: 1,
|
||||
creatorNickname: "도화",
|
||||
|
@ -226,13 +233,13 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
|
|||
isAdult: false,
|
||||
onClickQuit: {},
|
||||
onClickToggleBg: {},
|
||||
onClickShare: {},
|
||||
onClickEdit: {},
|
||||
onClickProfile: { _ in },
|
||||
onClickNotice: {},
|
||||
onClickMenuPan: {},
|
||||
onClickTotalDonation: {},
|
||||
onClickParticipants: {}
|
||||
onClickParticipants: {},
|
||||
onClickToggleSignature: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ struct LiveRoomViewV2: View {
|
|||
isOnBg: viewModel.isBgOn,
|
||||
isOnNotice: viewModel.isShowNotice,
|
||||
isOnMenuPan: viewModel.isShowMenuPan,
|
||||
isOnSignature: viewModel.isSignatureOn,
|
||||
isShowMenuPanButton: !liveRoomInfo.menuPan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
creatorId: liveRoomInfo.creatorId,
|
||||
creatorNickname: liveRoomInfo.creatorNickname,
|
||||
|
@ -44,9 +45,6 @@ struct LiveRoomViewV2: View {
|
|||
onClickToggleBg: {
|
||||
viewModel.isBgOn.toggle()
|
||||
},
|
||||
onClickShare: {
|
||||
viewModel.shareRoom()
|
||||
},
|
||||
onClickEdit: {
|
||||
viewModel.isShowEditRoomInfoDialog = true
|
||||
},
|
||||
|
@ -66,6 +64,9 @@ struct LiveRoomViewV2: View {
|
|||
},
|
||||
onClickParticipants: {
|
||||
viewModel.isShowProfileList = true
|
||||
},
|
||||
onClickToggleSignature: {
|
||||
viewModel.isSignatureOn.toggle()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
@ -75,6 +76,7 @@ struct LiveRoomViewV2: View {
|
|||
isOnBg: viewModel.isBgOn,
|
||||
isOnNotice: viewModel.isShowNotice,
|
||||
isOnMenuPan: viewModel.isShowMenuPan,
|
||||
isOnSignature: viewModel.isSignatureOn,
|
||||
isShowMenuPanButton: !liveRoomInfo.menuPan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
creatorId: liveRoomInfo.creatorId,
|
||||
creatorNickname: liveRoomInfo.creatorNickname,
|
||||
|
@ -90,9 +92,6 @@ struct LiveRoomViewV2: View {
|
|||
onClickToggleBg: {
|
||||
viewModel.isBgOn.toggle()
|
||||
},
|
||||
onClickShare: {
|
||||
viewModel.shareRoom()
|
||||
},
|
||||
onClickFollow: {
|
||||
if $0 {
|
||||
viewModel.creatorUnFollow()
|
||||
|
@ -116,6 +115,9 @@ struct LiveRoomViewV2: View {
|
|||
},
|
||||
onClickChangeListener: {
|
||||
viewModel.setListener()
|
||||
},
|
||||
onClickToggleSignature: {
|
||||
viewModel.isSignatureOn.toggle()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -168,72 +170,44 @@ struct LiveRoomViewV2: View {
|
|||
VStack(alignment: .trailing, spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on",
|
||||
onClick: { viewModel.toggleSpeakerMute() }
|
||||
)
|
||||
.padding(.bottom, 40)
|
||||
.padding(.trailing, 13.3)
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
|
||||
Image("ic_roulette_settings")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.isShowRouletteSettings = true
|
||||
}
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: "ic_roulette_settings",
|
||||
onClick: { viewModel.isShowRouletteSettings = true }
|
||||
)
|
||||
} else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette {
|
||||
Image("ic_roulette")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.showRoulette()
|
||||
}
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: "ic_roulette",
|
||||
onClick: { viewModel.showRoulette() }
|
||||
)
|
||||
}
|
||||
|
||||
if viewModel.role == .SPEAKER {
|
||||
Image(viewModel.isMute ? "ic_mic_off" : "ic_mic_on")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.toggleMute()
|
||||
}
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: viewModel.isMute ? "ic_mic_off" : "ic_mic_on",
|
||||
onClick: { viewModel.toggleMute() }
|
||||
)
|
||||
}
|
||||
|
||||
Image(viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.toggleSpeakerMute()
|
||||
}
|
||||
|
||||
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) &&
|
||||
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
|
||||
Image("ic_donation_message_list")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.isShowDonationMessagePopup = true
|
||||
}
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: "ic_donation_message_list",
|
||||
onClick: { viewModel.isShowDonationMessagePopup = true }
|
||||
)
|
||||
} else {
|
||||
Image("ic_donation")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(11)
|
||||
.background(Color(hex: "525252").opacity(0.6))
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
viewModel.isShowDonationPopup = true
|
||||
}
|
||||
LiveRoomRightBottomButton(
|
||||
imageName: "ic_donation",
|
||||
onClick: { viewModel.isShowDonationPopup = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 13.3)
|
||||
|
@ -256,17 +230,33 @@ struct LiveRoomViewV2: View {
|
|||
}.padding(.bottom, 70)
|
||||
}
|
||||
|
||||
if viewModel.signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
|
||||
VStack(spacing: 0) {
|
||||
if viewModel.isSignatureOn && viewModel.signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
AnimatedImage(url: URL(string: viewModel.signatureImageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 300)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 65)
|
||||
.frame(width: screenSize().width - 64)
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
if let signature = viewModel.signature, viewModel.isSignatureOn {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
AnimatedImage(url: URL(string: signature.imageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: screenSize().width - 64)
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,16 +301,17 @@ struct LiveRoomViewV2: View {
|
|||
VStack(alignment: .leading, spacing: 0) {
|
||||
Image("ic_notice_triangle")
|
||||
.padding(.leading, 60)
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("[메뉴판]")
|
||||
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("[메뉴판]")
|
||||
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(liveRoomInfo.menuPan)
|
||||
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||
.foregroundColor(.white)
|
||||
.lineSpacing(4)
|
||||
Text(liveRoomInfo.menuPan)
|
||||
.font(.custom(Font.light.rawValue, size: 11.3))
|
||||
.foregroundColor(.white)
|
||||
.lineSpacing(4)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color.gray33)
|
||||
|
@ -608,12 +599,12 @@ struct LiveRoomViewV2: View {
|
|||
}
|
||||
}
|
||||
|
||||
if let preview = viewModel.roulettePreview, viewModel.isShowRoulettePreview {
|
||||
if !viewModel.roulettePreviewList.isEmpty && viewModel.isShowRoulettePreview {
|
||||
RoulettePreviewDialog(
|
||||
isShowing: $viewModel.isShowRoulettePreview,
|
||||
title: nil,
|
||||
onClickSpin: { viewModel.spinRoulette() },
|
||||
preview: preview
|
||||
onClickSpin: { viewModel.spinRoulette(rouletteId: $0) },
|
||||
previewList: viewModel.roulettePreviewList
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ struct CanChargeView: View {
|
|||
@StateObject var storeManager = StoreManager()
|
||||
@StateObject var viewModel = CanChargeViewModel()
|
||||
|
||||
@State var currentTab: CanChargeCurrentTab = .iap
|
||||
@State var currentTab: CanChargeCurrentTab = .pg
|
||||
|
||||
let refresh: () -> Void
|
||||
let afterCompletionToGoBack: Bool
|
||||
|
@ -146,22 +146,6 @@ struct CanChargeTabView: View {
|
|||
HStack(spacing: 0) {
|
||||
let tabWidth = screenSize().width / 2
|
||||
|
||||
CanChargeTab(
|
||||
title: "인 앱 결제",
|
||||
action: {
|
||||
if currentTab != .iap {
|
||||
currentTab = .iap
|
||||
}
|
||||
},
|
||||
color: {
|
||||
currentTab == .iap ?
|
||||
Color(hex: "eeeeee") :
|
||||
Color(hex: "777777")
|
||||
},
|
||||
width: tabWidth,
|
||||
isShowDivider: { currentTab == .iap }
|
||||
)
|
||||
|
||||
CanChargeTab(
|
||||
title: "PG",
|
||||
action: {
|
||||
|
@ -171,17 +155,33 @@ struct CanChargeTabView: View {
|
|||
},
|
||||
color: {
|
||||
currentTab == .pg ?
|
||||
Color(hex: "eeeeee") :
|
||||
Color(hex: "777777")
|
||||
Color.grayee :
|
||||
Color.gray77
|
||||
},
|
||||
width: tabWidth,
|
||||
isShowDivider: { currentTab == .pg }
|
||||
)
|
||||
|
||||
CanChargeTab(
|
||||
title: "인 앱 결제",
|
||||
action: {
|
||||
if currentTab != .iap {
|
||||
currentTab = .iap
|
||||
}
|
||||
},
|
||||
color: {
|
||||
currentTab == .iap ?
|
||||
Color.grayee :
|
||||
Color.gray77
|
||||
},
|
||||
width: tabWidth,
|
||||
isShowDivider: { currentTab == .iap }
|
||||
)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.frame(width: screenSize().width, height: 1)
|
||||
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||
.foregroundColor(Color.gray90.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ struct CanChargeTab: View {
|
|||
|
||||
Rectangle()
|
||||
.frame(width: width, height: 3)
|
||||
.foregroundColor(Color(hex: "9970ff").opacity(isShowDivider() ? 1 : 0))
|
||||
.foregroundColor(Color.button.opacity(isShowDivider() ? 1 : 0))
|
||||
}
|
||||
.frame(height: 50)
|
||||
}
|
||||
|
|
|
@ -86,11 +86,11 @@ struct CanPgPaymentView: View {
|
|||
|
||||
Text("\(canResponse.price) 원")
|
||||
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 23.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width)
|
||||
|
@ -98,7 +98,7 @@ struct CanPgPaymentView: View {
|
|||
|
||||
Text("결제 수단 선택")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(width: screenSize().width - 26.7, alignment: .leading)
|
||||
.padding(.top, 26.7)
|
||||
|
||||
|
@ -106,18 +106,19 @@ struct CanPgPaymentView: View {
|
|||
HStack(spacing: 13.3) {
|
||||
Text("카드")
|
||||
.font(.custom( viewModel.paymentMethod == .card ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "eeeeee"))
|
||||
.foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "232323")
|
||||
.opacity(viewModel.paymentMethod == .card ? 0.3 : 1)
|
||||
viewModel.paymentMethod == .card ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .card ? "9970ff" : "777777"))
|
||||
.foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .card {
|
||||
|
@ -128,18 +129,19 @@ struct CanPgPaymentView: View {
|
|||
|
||||
Text("계좌이체")
|
||||
.font(.custom( viewModel.paymentMethod == .bank ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "eeeeee"))
|
||||
.foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "232323")
|
||||
.opacity(viewModel.paymentMethod == .bank ? 0.3 : 1)
|
||||
viewModel.paymentMethod == .bank ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .bank ? "9970ff" : "777777"))
|
||||
.foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .bank {
|
||||
|
@ -153,18 +155,19 @@ struct CanPgPaymentView: View {
|
|||
HStack(spacing: 13.3) {
|
||||
Text("휴대폰 결제")
|
||||
.font(.custom( viewModel.paymentMethod == .phone ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "eeeeee"))
|
||||
.foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "232323")
|
||||
.opacity(viewModel.paymentMethod == .phone ? 0.3 : 1)
|
||||
viewModel.paymentMethod == .phone ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: viewModel.paymentMethod == .phone ? "9970ff" : "777777"))
|
||||
.foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .phone {
|
||||
|
@ -184,7 +187,7 @@ struct CanPgPaymentView: View {
|
|||
|
||||
Text("구매조건 확인 및 결제 진행 동의")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
.padding(.top, 16.7)
|
||||
|
@ -196,11 +199,11 @@ struct CanPgPaymentView: View {
|
|||
HStack(alignment: .top, spacing: 0) {
|
||||
Text("- ")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("충전된 캔의 유효기간은 충전 후 5년 입니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
|
@ -209,11 +212,11 @@ struct CanPgPaymentView: View {
|
|||
HStack(alignment: .top, spacing: 0) {
|
||||
Text("- ")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("결제 취소는 결제 후 7일 이내에만 할 수 있습니다.\n단, 캔의 일부를 사용하면 결제 취소를 할 수 없습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
|
@ -221,11 +224,11 @@ struct CanPgPaymentView: View {
|
|||
HStack(alignment: .top, spacing: 0) {
|
||||
Text("- ")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("광고성 이벤트 등 회사가 무료로 지급한 포인트는 환불되지 않습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
|
@ -233,11 +236,11 @@ struct CanPgPaymentView: View {
|
|||
HStack(alignment: .top, spacing: 0) {
|
||||
Text("- ")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
|
||||
Text("자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
|
@ -251,12 +254,12 @@ struct CanPgPaymentView: View {
|
|||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("결제금액")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("\(canResponse.price) 원")
|
||||
.font(.custom(Font.bold.rawValue, size: 23.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,7 +270,7 @@ struct CanPgPaymentView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(minWidth: 200)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod == nil {
|
||||
|
@ -290,12 +293,12 @@ struct CanPgPaymentView: View {
|
|||
.padding(.leading, 22)
|
||||
.padding(.trailing, 13.3)
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +311,7 @@ struct CanPgPaymentView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
@ -326,25 +329,6 @@ struct CanPgPaymentView: View {
|
|||
LoadingView()
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(viewModel.errorMessage)
|
||||
.padding(.vertical, 13.3)
|
||||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(20)
|
||||
.padding(.top, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// CanChargeTempRequest.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/24.
|
||||
//
|
||||
|
||||
struct CanChargeTempRequest: Encodable {
|
||||
let can: Int
|
||||
let price: Int
|
||||
let paymentGateway: PaymentGateway
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// CanPaymentTempRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
class CanPaymentTempRepository {
|
||||
private let api = MoyaProvider<CanTempApi>()
|
||||
|
||||
func chargeCan(request: CanChargeTempRequest) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.chargeCan(request: request))
|
||||
}
|
||||
|
||||
func pgVerify(receiptId: String, orderId: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.verify(request: PgVerifyRequest(receiptId: receiptId, orderId: orderId)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
//
|
||||
// CanPaymentTempView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bootpay
|
||||
import BootpayUI
|
||||
|
||||
struct CanPaymentTempView: View {
|
||||
|
||||
@StateObject var viewModel = CanPaymentTempViewModel()
|
||||
|
||||
let orderType: OrderType
|
||||
let contentId: Int
|
||||
let title: String
|
||||
let can: Int
|
||||
|
||||
init(orderType: OrderType, contentId: Int, title: String, can: Int) {
|
||||
self.orderType = orderType
|
||||
self.contentId = contentId
|
||||
self.title = title
|
||||
self.can = can
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
if viewModel.isShowPaymentView {
|
||||
BootpayUI(payload: viewModel.payload, requestType: BootpayRequest.TYPE_PAYMENT)
|
||||
.onConfirm {
|
||||
DEBUG_LOG("onConfirm: \($0)")
|
||||
return true
|
||||
}
|
||||
.onCancel {
|
||||
DEBUG_LOG("onCancel: \($0)")
|
||||
}
|
||||
.onError {
|
||||
DEBUG_LOG("onError: \($0)")
|
||||
viewModel.isShowPaymentView = false
|
||||
viewModel.errorMessage = "결제 중 오류가 발생했습니다."
|
||||
viewModel.isShowPopup = true
|
||||
}
|
||||
.onDone {
|
||||
DEBUG_LOG("onDone: \($0)")
|
||||
viewModel.verifyPayment($0) {
|
||||
let can = UserDefaults.int(forKey: .can)
|
||||
UserDefaults.set(can + self.can, forKey: .can)
|
||||
|
||||
AppState.shared.purchasedContentId = contentId
|
||||
AppState.shared.purchasedContentOrderType = orderType
|
||||
|
||||
DispatchQueue.main.async {
|
||||
AppState.shared.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onClose {
|
||||
DEBUG_LOG("onClose")
|
||||
viewModel.isShowPaymentView = false
|
||||
}
|
||||
} else {
|
||||
GeometryReader { proxy in
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "결제하기")
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text(self.title)
|
||||
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.leading, 13.3)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(self.can * 110) 원")
|
||||
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 23.3)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
Text("결제 수단 선택")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(width: screenSize().width - 26.7, alignment: .leading)
|
||||
.padding(.top, 26.7)
|
||||
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
Text("카드")
|
||||
.font(.custom( viewModel.paymentMethod == .card ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
viewModel.paymentMethod == .card ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(viewModel.paymentMethod == .card ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .card {
|
||||
viewModel.paymentMethod = .card
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text("계좌이체")
|
||||
.font(.custom( viewModel.paymentMethod == .bank ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
viewModel.paymentMethod == .bank ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(viewModel.paymentMethod == .bank ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .bank {
|
||||
viewModel.paymentMethod = .bank
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.padding(.top, 16.7)
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
Text("휴대폰 결제")
|
||||
.font(.custom( viewModel.paymentMethod == .phone ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.grayee)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.padding(.vertical, 16.7)
|
||||
.background(
|
||||
viewModel.paymentMethod == .phone ?
|
||||
Color.button.opacity(0.3) :
|
||||
Color.gray23
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(viewModel.paymentMethod == .phone ? Color.button : Color.gray77)
|
||||
)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod != .phone {
|
||||
viewModel.paymentMethod = .phone
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.padding(.top, 16.7)
|
||||
|
||||
HStack(spacing: 6.7) {
|
||||
Image(viewModel.isTermsAgree ? "btn_select_checked" : "btn_select_normal")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("구매조건 확인 및 결제 진행 동의")
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
.padding(.top, 16.7)
|
||||
.onTapGesture {
|
||||
viewModel.isTermsAgree.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("결제금액")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("\(self.can * 110) 원")
|
||||
.font(.custom(Font.bold.rawValue, size: 23.3))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("결제하기")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(minWidth: 200)
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
if viewModel.paymentMethod == nil {
|
||||
viewModel.errorMessage = "결제수단을 선택해 주세요."
|
||||
viewModel.isShowPopup = true
|
||||
} else if !viewModel.isTermsAgree {
|
||||
viewModel.errorMessage = "결제진행에 동의하셔야 결제가 가능합니다."
|
||||
viewModel.isShowPopup = true
|
||||
} else {
|
||||
viewModel.chargeCan(can: can, paymentGateway: .PG){
|
||||
viewModel.payload.orderName = self.title
|
||||
viewModel.payload.price = Double(self.can * 110)
|
||||
viewModel.payload.taxFree = 0
|
||||
|
||||
viewModel.isShowPaymentView = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 22)
|
||||
.padding(.trailing, 13.3)
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(viewModel.errorMessage)
|
||||
.padding(.vertical, 13.3)
|
||||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(20)
|
||||
.padding(.top, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CanPaymentTempView(orderType: .KEEP, contentId: 0, title: "콘텐츠 제목", can: 1000)
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// CanPaymentTempViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
import Bootpay
|
||||
|
||||
enum TempPaymentMethod: String {
|
||||
case card = "카드"
|
||||
case bank = "계좌이체"
|
||||
case phone = "휴대폰"
|
||||
}
|
||||
|
||||
final class CanPaymentTempViewModel: ObservableObject {
|
||||
|
||||
private let repository = CanPaymentTempRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var isTermsAgree = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var isShowPaymentView = false
|
||||
@Published var paymentMethod: TempPaymentMethod? = nil
|
||||
|
||||
let payload = Payload()
|
||||
|
||||
func chargeCan(can: Int, paymentGateway: PaymentGateway, onSuccess: @escaping () -> Void) {
|
||||
isLoading = true
|
||||
repository.chargeCan(request: CanChargeTempRequest(can: can, price: can * 110, paymentGateway: paymentGateway))
|
||||
.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<CanChargeResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
let bootUser = BootUser()
|
||||
bootUser.userId = "\(UserDefaults.int(forKey: .userId))"
|
||||
bootUser.username = UserDefaults.string(forKey: .nickname)
|
||||
|
||||
payload.applicationId = BOOTPAY_APP_ID
|
||||
payload.pg = "세틀뱅크"
|
||||
payload.orderId = "\(data.chargeId)"
|
||||
payload.method = paymentMethod!.rawValue
|
||||
payload.user = bootUser
|
||||
|
||||
onSuccess()
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func verifyPayment(_ data: [String: Any], onSuccess: @escaping () -> Void) {
|
||||
isLoading = true
|
||||
|
||||
let _data = data["data"] as? [String: Any]
|
||||
|
||||
if let data = _data {
|
||||
let receiptId = data["receipt_id"] as! String
|
||||
let orderId = data["order_id"] as! String
|
||||
|
||||
repository.pgVerify(receiptId: receiptId, orderId: orderId)
|
||||
.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(ApiResponseWithoutData.self, from: responseData)
|
||||
|
||||
if decoded.success {
|
||||
onSuccess()
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
} else {
|
||||
isLoading = false
|
||||
errorMessage = "본인인증 중 오류가 발생했습니다."
|
||||
isShowPopup = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// CanTempApi.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum CanTempApi {
|
||||
case chargeCan(request: CanChargeTempRequest)
|
||||
case verify(request: PgVerifyRequest)
|
||||
}
|
||||
|
||||
extension CanTempApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .chargeCan:
|
||||
return "/charge/temp"
|
||||
|
||||
case .verify:
|
||||
return "/charge/temp/verify"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .chargeCan, .verify:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
|
||||
var task: Task {
|
||||
switch self {
|
||||
case .chargeCan(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .verify(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String : String]? {
|
||||
switch self {
|
||||
default:
|
||||
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -95,7 +95,7 @@ struct NicknameUpdateView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
|
@ -21,7 +21,7 @@ struct ModifyPasswordView: View {
|
|||
ScrollView(.vertical, showsIndicators: false) {
|
||||
Text("안전한 비밀번호로 내 내 정보를 보호하세요")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.top, 40)
|
||||
|
||||
VStack(spacing: 26.7) {
|
||||
|
@ -51,7 +51,7 @@ struct ModifyPasswordView: View {
|
|||
|
||||
Text("* 영문, 숫자 포함 8자 이상")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "dd4500"))
|
||||
.foregroundColor(Color.mainRed3)
|
||||
.frame(width: screenSize().width - 53.4, alignment: .leading)
|
||||
.padding(.top, 13.7)
|
||||
}
|
||||
|
@ -62,11 +62,11 @@ struct ModifyPasswordView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 13.7)
|
||||
.frame(width: screenSize().width)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
.onTapGesture {
|
||||
hideKeyboard()
|
||||
|
@ -75,7 +75,7 @@ struct ModifyPasswordView: View {
|
|||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ struct ModifyPasswordView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
|
@ -23,18 +23,18 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("이메일")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text(viewModel.email)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Divider()
|
||||
.frame(height: 0.3)
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
.foregroundColor(Color.gray90)
|
||||
.padding(.top, 8.3)
|
||||
}
|
||||
|
||||
|
@ -42,18 +42,18 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("비밀번호")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text("********")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Divider()
|
||||
.frame(height: 0.3)
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
.foregroundColor(Color.gray90)
|
||||
.padding(.top, 8.3)
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ struct ProfileUpdateView: View {
|
|||
.foregroundColor(Color.white)
|
||||
.padding(.vertical, 13.3)
|
||||
.padding(.horizontal, 22.7)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ struct ProfileUpdateView: View {
|
|||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
|
||||
|
@ -82,18 +82,18 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("닉네임")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text(viewModel.nickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Divider()
|
||||
.frame(height: 0.3)
|
||||
.foregroundColor(Color(hex: "909090"))
|
||||
.foregroundColor(Color.gray90)
|
||||
.padding(.top, 8.3)
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ struct ProfileUpdateView: View {
|
|||
.foregroundColor(Color.white)
|
||||
.padding(.vertical, 13.3)
|
||||
.padding(.horizontal, 22.7)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("성별")
|
||||
.font(.custom(Font.bold.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
|
@ -123,7 +123,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
Text("여자")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
Text("남자")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
Text("공개 안 함")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ struct ProfileUpdateView: View {
|
|||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
|
||||
|
@ -201,7 +201,7 @@ struct ProfileUpdateView: View {
|
|||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("관심사")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Button(action: {
|
||||
hideKeyboard()
|
||||
|
@ -218,15 +218,15 @@ struct ProfileUpdateView: View {
|
|||
}) {
|
||||
Text("관심사 선택")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.vertical, 13.7)
|
||||
.frame(width: screenSize().width - 53.4)
|
||||
.background(Color(hex: "9970ff").opacity(0.2))
|
||||
.background(Color.button.opacity(0.2))
|
||||
.cornerRadius(24.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 24.3)
|
||||
.stroke()
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -246,7 +246,7 @@ struct ProfileUpdateView: View {
|
|||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(24.3)
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +256,7 @@ struct ProfileUpdateView: View {
|
|||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
|
||||
|
@ -265,7 +265,7 @@ struct ProfileUpdateView: View {
|
|||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("소개글")
|
||||
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
TextViewWrapper(
|
||||
text: $viewModel.introduce,
|
||||
|
@ -307,7 +307,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
Image("ic_camera")
|
||||
.padding(10)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(30)
|
||||
.offset(x: 25, y: 25)
|
||||
}
|
||||
|
@ -343,12 +343,12 @@ struct ProfileUpdateView: View {
|
|||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.white)
|
||||
.frame(width: screenSize().width - 26.7, height: 50)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 13.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
.padding(.top, 26.7)
|
||||
.onTapGesture {
|
||||
|
@ -357,7 +357,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -373,12 +373,12 @@ struct ProfileUpdateView: View {
|
|||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color.white)
|
||||
.frame(width: screenSize().width - 26.7, height: 50)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 13.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
.padding(.top, 13.3)
|
||||
.onTapGesture {
|
||||
|
@ -387,7 +387,7 @@ struct ProfileUpdateView: View {
|
|||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "222222"))
|
||||
.foregroundColor(Color.gray22)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +443,7 @@ struct ProfileUpdateView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
|
@ -67,8 +67,8 @@ struct MemberTagView: View {
|
|||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(
|
||||
selectedTags.contains(tag.tag) ?
|
||||
Color(hex: "9970ff") :
|
||||
Color(hex: "bbbbbb")
|
||||
Color.button :
|
||||
Color.graybb
|
||||
)
|
||||
}
|
||||
.onTapGesture {
|
||||
|
@ -85,7 +85,7 @@ struct MemberTagView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.padding(.bottom, 26.7)
|
||||
.onTapGesture {
|
||||
|
|
|
@ -40,15 +40,15 @@ struct LiveReservationCancelView: View {
|
|||
HStack(spacing: 13.3) {
|
||||
Text("다른 라이브 예약하기")
|
||||
.font(.custom(Font.medium.rawValue, size: 15))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.vertical, 16)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.background(Color(hex: "9970ff").opacity(0.2))
|
||||
.background(Color.button.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(
|
||||
Color(hex: "9970ff"),
|
||||
Color.button,
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
|
@ -61,7 +61,7 @@ struct LiveReservationCancelView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(width: (screenSize().width - 40) / 2)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .canStatus(refresh: {}))
|
||||
|
@ -84,7 +84,7 @@ struct LiveReservationCancelView: View {
|
|||
|
||||
Text(item.masterNickname)
|
||||
.font(.custom(Font.medium.rawValue, size: 11.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.foregroundColor(Color.graybb)
|
||||
.padding(.top, 10)
|
||||
|
||||
Text(item.title)
|
||||
|
@ -108,7 +108,7 @@ struct LiveReservationCancelView: View {
|
|||
.padding(.top, 20)
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||
.foregroundColor(Color.gray90.opacity(0.5))
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(width: screenSize().width, height: 1)
|
||||
.padding(.top, 13.3)
|
||||
|
@ -121,7 +121,7 @@ struct LiveReservationCancelView: View {
|
|||
|
||||
Text("예약취소 이유를 선택해주세요. 서비스 개선에 중요한 자료로 활용하겠습니다.")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(width: screenSize().width - 26.7, alignment: .leading)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
@ -140,7 +140,7 @@ struct LiveReservationCancelView: View {
|
|||
|
||||
Text(reason)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
if index == viewModel.cancelReasons.count - 1 {
|
||||
VStack(spacing: 6.7) {
|
||||
|
@ -148,12 +148,12 @@ struct LiveReservationCancelView: View {
|
|||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.keyboardType(.webSearch)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(hex: "909090").opacity(0.5))
|
||||
.foregroundColor(Color.gray90.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ struct LiveReservationCancelView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.vertical, 16)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.cornerRadius(6.7)
|
||||
.padding(.top, 90)
|
||||
.padding(.bottom, 13.3)
|
||||
|
@ -202,7 +202,7 @@ struct LiveReservationCancelView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
|
@ -23,11 +23,11 @@ struct FaqView: View {
|
|||
HStack(alignment: .top, spacing: 6.7) {
|
||||
Text("Q")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Text(faq.question)
|
||||
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.foregroundColor(Color.grayee)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer()
|
||||
|
@ -43,16 +43,16 @@ struct FaqView: View {
|
|||
HStack(alignment: .top, spacing: 6.7) {
|
||||
Text("A")
|
||||
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.foregroundColor(Color.button)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
RichText(html: faq.answer)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.foregroundColor(Color.graybb)
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 6.7)
|
||||
.background(Color(hex: "222222"))
|
||||
.background(Color.gray22)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
|
|
@ -18,7 +18,7 @@ struct ServiceCenterCategoryItemView: View {
|
|||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: proxy.size.width, height: 46.7)
|
||||
.background(isSelected ? Color(hex: "9970ff") : Color(hex: "222222"))
|
||||
.background(isSelected ? Color.button : Color.gray22)
|
||||
.cornerRadius(4.7)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ struct ServiceCenterView: View {
|
|||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color(hex: "9970ff"))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
|
@ -156,11 +156,10 @@ struct SettingsView: View {
|
|||
.padding(.top, 13.3)
|
||||
|
||||
Text("""
|
||||
- 회사명 : 주식회사 비비드넥스트
|
||||
- 대표자 : 한지영
|
||||
- 주소 : 서울 강남구 테헤란로 410, 11층 A08호(대치동, 금강타워)
|
||||
- 사업자등록번호 : 508-86-01545
|
||||
- 통신판매업신고 : 제2022-서울강남-00559호
|
||||
- 회사명 : 주식회사 소다라이브
|
||||
- 대표자 : 이재형
|
||||
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호 (서현동, 멜로즈 프라자)
|
||||
- 사업자등록번호 : 870-81-03220
|
||||
- 고객센터 : 02.2055.1477
|
||||
""")
|
||||
.font(.custom(Font.medium.rawValue, size: 11))
|
||||
|
|
|
@ -17,19 +17,19 @@ struct SplashView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
Image("splash_bg_2024_03")
|
||||
Image("splash_bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Image("splash_text_2024_03")
|
||||
.padding(.top, 262)
|
||||
Image("splash_text")
|
||||
.padding(.top, 101)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("splash_text_logo_2024_03")
|
||||
.padding(.bottom, 35)
|
||||
Image("splash_text_2")
|
||||
.padding(.bottom, 65)
|
||||
}
|
||||
|
||||
if isShowUpdatePopup {
|
||||
|
|
|
@ -13,23 +13,51 @@ struct SelectedButtonView: View {
|
|||
let isActive: Bool
|
||||
let isSelected: Bool
|
||||
|
||||
var checkImage = "ic_select_check"
|
||||
var bgDisabledColor = Color.gray55
|
||||
var bgSelectedColor = Color.button
|
||||
var bgDefaultColor = Color.bg
|
||||
var textDisabledColor = Color.gray77
|
||||
var textSelectedColor = Color.white
|
||||
var textDefaultColor = Color.button
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 6.7) {
|
||||
if isSelected {
|
||||
Image("ic_select_check")
|
||||
Image(checkImage)
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||
.foregroundColor(!isActive ? Color.gray77 : isSelected ? .white : Color.button)
|
||||
.foregroundColor(!isActive ? textDisabledColor : isSelected ? textSelectedColor : textDefaultColor)
|
||||
}
|
||||
.padding(.vertical, 14.3)
|
||||
.padding(.vertical, isSelected ? 14.3 : 17)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(!isActive ? Color.gray55 : isSelected ? Color.button : Color.bg)
|
||||
.background(!isActive ? bgDisabledColor : isSelected ? bgSelectedColor : bgDefaultColor)
|
||||
.cornerRadius(6.7)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
#Preview("기본") {
|
||||
SelectedButtonView(title: "테스트", isActive: true, isSelected: true)
|
||||
}
|
||||
|
||||
#Preview("이미지와 컬러 수정 - selected") {
|
||||
SelectedButtonView(
|
||||
title: "테스트",
|
||||
isActive: true,
|
||||
isSelected: true,
|
||||
bgSelectedColor: Color(hex: "ff14d9"),
|
||||
textDefaultColor: Color(hex: "ff14d9")
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("이미지와 컬러 수정 - unselected") {
|
||||
SelectedButtonView(
|
||||
title: "테스트",
|
||||
isActive: true,
|
||||
isSelected: false,
|
||||
textDefaultColor: Color(hex: "ff14d9")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// SeriesDetailTabView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/30/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesDetailTabView: View {
|
||||
|
||||
let title: String
|
||||
let width: CGFloat
|
||||
let isSelected: Bool
|
||||
let onClick: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(title)
|
||||
.font(.custom(isSelected ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7))
|
||||
.foregroundColor(isSelected ? Color.button : Color.gray77)
|
||||
.frame(width: width, height: 50)
|
||||
|
||||
if isSelected {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.button)
|
||||
.frame(width: width, height: 3)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesDetailTabView(
|
||||
title: "홈",
|
||||
width: 180,
|
||||
isSelected: true,
|
||||
onClick: {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// SeriesItemBadgeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesItemBadgeView: View {
|
||||
|
||||
let title: String
|
||||
let backgroundColor: Color
|
||||
|
||||
var body: some View {
|
||||
Text(title)
|
||||
.font(.custom(Font.medium.rawValue, size: 10.3))
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 3.7)
|
||||
.padding(.horizontal, 5.3)
|
||||
.background(backgroundColor)
|
||||
.cornerRadius(13.3)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("신작") {
|
||||
SeriesItemBadgeView(title: "신작", backgroundColor: .button)
|
||||
}
|
||||
|
||||
#Preview("완결") {
|
||||
SeriesItemBadgeView(title: "완결", backgroundColor: Color(hex: "002abd"))
|
||||
}
|
||||
|
||||
#Preview("인기") {
|
||||
SeriesItemBadgeView(title: "인기", backgroundColor: Color(hex: "ec6033"))
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// SeriesKeywordChipView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesKeywordChipView: View {
|
||||
|
||||
let keyword: String
|
||||
|
||||
var body: some View {
|
||||
Text(keyword)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color.grayd2)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 5.3)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(26.7)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesKeywordChipView(keyword: "#로맨스")
|
||||
}
|