Compare commits

...

48 Commits

Author SHA1 Message Date
Yu Sung 043a583985 회사정보 변경 2024-05-21 01:24:27 +09:00
Yu Sung 568d7f2284 콘텐츠 상세, 콘텐츠 구매
- pg 테스트 계정의 경우 캔이 아닌 원으로 표시되도록 하고 콘텐츠 구매시 바로 결제 후 구매 되도록 수정
2024-05-21 00:11:06 +09:00
Yu Sung fd2230dbe1 룰렛 설정
- 확률 총합 추가
2024-05-16 14:21:54 +09:00
Yu Sung 6bf5fcde7f Firebase Library Version 10.24.0 타겟팅 되도록 업데이트 2024-05-15 02:28:48 +09:00
Yu Sung 602ee790eb 시리즈 전체보기
- 아이템 상단 정렬
2024-05-14 23:11:00 +09:00
Yu Sung 523295648b 추천시리즈, 지금 라이브 중 새로고침 영역
- overlay 된 영역 전체가 터치 되도록 수정
2024-05-14 21:10:58 +09:00
Yu Sung 6ba59ae852 라이브 방 생성
- 크리에이터 입장 가능 설정 추가
2024-05-14 21:08:02 +09:00
Yu Sung c459c96aac 시리즈 전체보기
- 아이템 상단 정렬 방법 추가
2024-05-14 18:01:40 +09:00
Yu Sung ec8c1fdb71 시리즈 상세
- 커버이미지 크기 강제처리
2024-05-13 19:43:50 +09:00
Yu Sung 44e9e07716 시리즈 전체보기
- 아이템 사이즈 이상하게 나오던 것 화면 1/3로 나오도록 수정
2024-05-13 19:24:07 +09:00
Yu Sung 0a773ab99f 룰렛 설정
- 룰렛 1, 2, 3 버튼 bg, text 색상 변경
2024-05-11 04:13:44 +09:00
Yu Sung c2172b29ae 라이브 방
- 음소거 버튼 위치 아래로 이동
2024-05-11 04:10:27 +09:00
Yu Sung cab719c774 룰렛 변경
- 확률 수동 설정
- 여러개의 룰렛이 켜져있을 때 선택하여 돌리기
- 후원 히스토리에 룰렛 히스토리
2024-05-11 02:56:52 +09:00
Yu Sung 57abeea432 콘텐츠 메인
- 새로운 콘텐츠 아래 새로고침 버튼 제거
2024-05-08 12:17:22 +09:00
Yu Sung 8abd4f3c87 라이브
- 라이브가 없을 때 문구 수정
- 라이브가 없을 떄 문구 자간 및 폰트 사이즈 수정
2024-05-08 12:05:50 +09:00
Yu Sung 70c478baa9 라이브 메인 - 라이브 없을 때 문구
- 🙀마이페이지에서 본인인증을 하거나 라이브를 예약하고 참여해보세요.
2024-05-07 19:27:39 +09:00
Yu Sung 511bb11550 추천 시리즈, 새로운 콘텐츠, 지금 라이브중
- 새로고침 버튼 추가
2024-05-07 18:50:13 +09:00
Yu Sung bcba83a8a7 콘텐츠 메인
- 추천 시리즈 UI 추가
2024-05-07 18:30:13 +09:00
Yu Sung 83d51a525b 탐색
- 크리에이터가 없으면 섹션제거
2024-05-03 16:50:03 +09:00
Yu Sung 2590f5471b 시리즈 아이템
- 커버이미지 DIM 제거
2024-05-03 16:45:33 +09:00
Yu Sung 250f169b42 라이브
- 공유하기 버튼 제거
2024-05-02 15:20:53 +09:00
Yu Sung 1bf4e59eed 시그니처 ON/OFF => 시그 ON/OFF 2024-05-02 15:11:17 +09:00
Yu Sung 86f0d466fa 시그니처 ON/OFF 버튼 추가 2024-05-02 14:45:54 +09:00
Yu Sung 3d625a4fa0 라이브
- 메뉴판 스크롤 추가
2024-05-02 14:26:30 +09:00
Yu Sung def95286c2 2024년 5월 인트로 적용 2024-05-01 22:42:37 +09:00
Yu Sung ee34c9a0f8 시그니처 후원
- 시그니처 별로 설정된 시간 만큼 GIF가 재생되도록 기능 추가
2024-05-01 22:29:37 +09:00
Yu Sung b55d2c22f8 시리즈 전체보기
- 세로 아이템 간격 33.3으로 수정
2024-04-30 23:48:37 +09:00
Yu Sung 9f3eb8a995 시리즈 아이템
- 상단을 기준으로 정렬이 되도록 수정
2024-04-30 23:33:09 +09:00
Yu Sung f97917f407 시그니처 후원
- 위치 가운데로 수정
2024-04-30 19:55:29 +09:00
Yu Sung 38653247b8 시리즈 상세 작품소개 키워드
- 글자가 2줄로 보이거나 잘리는 버그 수정
2024-04-30 19:24:17 +09:00
Yu Sung feaeb275e4 시리즈 전체보기
- 아이템 크기 수정
2024-04-30 18:37:29 +09:00
Yu Sung 70dae4f646 시리즈 콘텐츠 아이템뷰
- 하단 divider 컬러와 height 수정
2024-04-30 18:01:19 +09:00
Yu Sung 17c827f55e 시리즈 포스터
- 크리에이터명 삭제
2024-04-30 17:46:58 +09:00
Yu Sung 871d03b15b 시리즈 상세 - 연재 주기
뒤에 연재 추가
2024-04-30 16:52:31 +09:00
Yu Sung 1e5ee80ca2 크리에이터 채널 시리즈 - 스크롤뷰 추가
시리즈 전체 보기 - 상단 정렬 방식으로 수정
2024-04-30 16:49:14 +09:00
Yu Sung a08b463c11 시리즈 전체보기 페이지 추가 2024-04-30 15:28:58 +09:00
Yu Sung 93110eff8c 시리즈 상세 추가 2024-04-30 14:58:06 +09:00
Yu Sung 101b04b6a9 시리즈 전체보기 페이지 2024-04-29 19:36:53 +09:00
Yu Sung ffbdbbaa06 크리에이터 채널
- 시리즈 section 추가
2024-04-29 16:15:51 +09:00
Yu Sung 75b9c76987 크리에이터 채널
- 활동요약표 선 색깔 button색으로
2024-04-29 13:36:05 +09:00
Yu Sung f4f8f47bd0 크리에이터 팔로우 버튼과 라이브 제목 사이 간격 = 6.7 2024-04-12 17:09:22 +09:00
Yu Sung ba11b8c842 라이브 시그니처 후원 위치
- 기존 하단 가운데에서 가운데 우측으로 이동
2024-04-12 16:33:21 +09:00
Yu Sung 8505d444e2 라이브
- 스피커 음소거 버튼 우측 상단으로 이동
2024-04-12 16:22:58 +09:00
Yu Sung 51c16d49ec 후원하기 팝업
- 충전하기 버튼 변경
2024-04-12 16:18:42 +09:00
Yu Sung a3415ba8e7 대댓글 padding leading 추가 2024-04-02 15:50:40 +09:00
Yu Sung e77a068a8e 오픈 예정 콘텐츠 상세
- 댓글 창, 좋아요, 공유, 후원 버튼 숨김
2024-04-02 14:49:48 +09:00
Yu Sung 2f96ad1321 캔 충전
- 탭 순서 변경 ( pg, 인 앱 결제 순으로 )
2024-04-02 02:01:26 +09:00
Yu Sung 07b97b987b 인트로
- 4월 인트로 적용
2024-04-01 15:56:21 +09:00
107 changed files with 3234 additions and 807 deletions

View File

@ -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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 996 B

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "btn_minus_round_rect.png",
"filename" : "ic_select_check_black.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "splash_bg_2024_03.png",
"filename" : "splash_bg.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "btn_plus_round_rect.png",
"filename" : "splash_text.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "splash_text_2024_03.png",
"filename" : "splash_text_2.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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 {

View File

@ -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)
}

View File

@ -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()

View File

@ -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,6 +90,7 @@ struct ContentDetailInfoView: View {
}
.padding(.top, 13.3)
if !audioContent.contentUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && audioContent.releaseDate == nil {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
HStack(spacing: 4) {
@ -101,7 +102,7 @@ struct ContentDetailInfoView: View {
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)
@ -114,7 +115,7 @@ struct ContentDetailInfoView: View {
Text("공유")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.foregroundColor(Color.grayd2)
}
.padding(.horizontal, 13.3)
.padding(.vertical, 5.3)
@ -130,7 +131,7 @@ struct ContentDetailInfoView: View {
Text("후원")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.foregroundColor(Color.grayd2)
}
.padding(.horizontal, 13.3)
.padding(.vertical, 5.3)
@ -141,6 +142,7 @@ struct ContentDetailInfoView: View {
}
}
.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)
)
}
}

View File

@ -14,16 +14,18 @@ struct ContentDetailPurchaseButton: View {
var body: some View {
HStack(spacing: 0) {
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)

View File

@ -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,8 +226,20 @@ struct ContentDetailView: View {
orderType: orderType,
isOnlyRental: audioContent.isOnlyRental,
onClickConfirm: {
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)
}
}
)
}
.ignoresSafeArea()
@ -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)
}
}

View File

@ -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 {
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

View File

@ -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,16 +77,26 @@ 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()
if UserDefaults.int(forKey: .userId) != 17958 {
Image("ic_can")
.resizable()
.frame(width: 16.7, height: 16.7)
@ -94,35 +104,47 @@ struct ContentOrderConfirmDialogView: View {
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"))
.foregroundColor(Color.grayee)
} else {
Text("\(audioContent.price)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(Color.grayee)
}
} else {
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)
}

View File

@ -40,17 +40,25 @@ struct ContentOrderDialogView: View {
Spacer()
HStack(spacing: 8) {
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) {
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)
}
}
}

View File

@ -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))

View File

@ -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,13 +116,13 @@ struct ContentMainView: View {
AppState.shared.setAppStep(step: .createContent)
}
}
}
if viewModel.isLoading {
LoadingView()
}
}
}
}
struct ContentMainView_Previews: PreviewProvider {
static var previews: some View {

View File

@ -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"
)
)
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)
)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)
]
)
}

View File

@ -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)"
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))"]
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -123,6 +123,7 @@ struct CreatorCommunityCommentReplyView: View {
}
)
.padding(.horizontal, 26.7)
.padding(.leading, 13.3)
.onAppear {
if index == viewModel.commentList.count - 1 {
viewModel.getCommentList()

View File

@ -17,6 +17,7 @@ struct GetCreatorProfileResponse: Decodable {
let communityPostList: [GetCommunityPostListResponse]
let cheers: GetCheersResponse
let activitySummary: GetCreatorActivitySummary
let seriesList: [SeriesListItem]
let isBlock: Bool
}

View File

@ -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
)
])
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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)
{

View File

@ -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)"

View File

@ -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()
}
}
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -22,4 +22,5 @@ struct CreateLiveRoomRequest: Encodable {
var menuPanId: Int = 0
var menuPan: String = ""
var isActiveMenuPan: Bool = false
var isAvailableJoinCreator: Bool = true
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)
LiveRoomDonationMessageItemView(message: donationMessage) {
viewModel.deleteDonationMessage(uuid: $0)
}
Spacer()
Image("ic_close_white")
.resizable()
.frame(width: 13.3, height: 13.3)
.onTapGesture {
viewModel.deleteDonationMessage(uuid: donationMessage.uuid)
}
}
.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)

View File

@ -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 }
)
}

View File

@ -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
}

View File

@ -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(

View File

@ -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
}
}

View File

@ -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"))
.keyboardType(.decimalPad)
.onChange(of: option.percentage) { newValue in
if newValue.count > 5 {
option.percentage = String(newValue.prefix(5))
}
calculateTotalPercentage()
}
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)
Image("btn_minus_round_rect")
.onTapGesture { onClickSubstract() }
Image("btn_plus_round_rect")
.onTapGesture { onClickPlus() }
}
}
}
@ -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: {}
)
}
}

View File

@ -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)

View File

@ -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,6 +119,7 @@ final class RouletteSettingsViewModel: ObservableObject {
if !isLoading {
isLoading = true
if validationOptions() {
if rouletteId > 0 {
updateRoulette(onSuccess: onSuccess)
} else {
@ -159,18 +127,40 @@ final class RouletteSettingsViewModel: ObservableObject {
}
}
}
}
private func validationOptions() -> Bool {
var totalPercentage = Float(0)
private func createRoulette(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
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
}
}

View File

@ -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
}

View File

@ -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)"
}
}

View File

@ -8,6 +8,7 @@
import Foundation
struct RoulettePreview {
let id: Int
let can: Int
let items: [RoulettePreviewItem]
}

View File

@ -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(
previewList: [
RoulettePreview(
id: 0,
can: 100,
items: [
RoulettePreviewItem(title: "옵션1", percent: "10%"),
RoulettePreviewItem(title: "옵션2", percent: "90%"),
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%"),
]
)
]
)
}
}

View File

@ -9,5 +9,6 @@ import Foundation
struct SpinRouletteRequest: Encodable {
let roomId: Int
let rouletteId: Int
let container: String = "ios"
}

View File

@ -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]
}

View File

@ -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")

View File

@ -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: {}
)
}
}

View File

@ -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: {}
)
}
}

View File

@ -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()
}
}
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()
LiveRoomRightBottomButton(
imageName: viewModel.isMute ? "ic_mic_off" : "ic_mic_on",
onClick: { viewModel.toggleMute() }
)
}
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,7 +301,7 @@ 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))
@ -322,6 +312,7 @@ struct LiveRoomViewV2: View {
.foregroundColor(.white)
.lineSpacing(4)
}
}
.padding(8)
.background(Color.gray33)
.padding(.horizontal, 60)
@ -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
)
}

View File

@ -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)
}

View File

@ -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()
}
}
}
}
}

View File

@ -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
}

View File

@ -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)))
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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))"]
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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))

View File

@ -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 {

View File

@ -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")
)
}

View File

@ -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: {}
)
}

View File

@ -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"))
}

View File

@ -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: "#로맨스")
}

Some files were not shown because too many files have changed in this diff Show More